forked from ebhomengo/niki
feat(niki): dockerize app
This commit is contained in:
parent
bc9a625c03
commit
74c1d223d1
|
@ -0,0 +1,37 @@
|
||||||
|
# Build Stage
|
||||||
|
# First pull Golang image
|
||||||
|
FROM golang:1.21.3-alpine as builder
|
||||||
|
|
||||||
|
# Set environment variable
|
||||||
|
ENV APP_NAME niki
|
||||||
|
ENV CMD_PATH main.go
|
||||||
|
|
||||||
|
# Add a work directory
|
||||||
|
WORKDIR /$APP_NAME
|
||||||
|
|
||||||
|
## Cache and install dependencies
|
||||||
|
#COPY go.mod go.sum ./
|
||||||
|
#RUN go mod download
|
||||||
|
|
||||||
|
# Copy app files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Budild application
|
||||||
|
RUN CGO_ENABLED=0 go build -mod=vendor -v -o $APP_NAME .
|
||||||
|
|
||||||
|
# Run Stage
|
||||||
|
|
||||||
|
FROM alpine:3.18 as development
|
||||||
|
|
||||||
|
|
||||||
|
# Set environment variable
|
||||||
|
ENV APP_NAME niki
|
||||||
|
|
||||||
|
# Copy only required data into this image
|
||||||
|
COPY --from=builder /$APP_NAME .
|
||||||
|
|
||||||
|
# Expose application port
|
||||||
|
EXPOSE 1313
|
||||||
|
|
||||||
|
# Start app
|
||||||
|
CMD ./$APP_NAME
|
4
Makefile
4
Makefile
|
@ -19,3 +19,7 @@ format:
|
||||||
|
|
||||||
build:
|
build:
|
||||||
go build main.go
|
go build main.go
|
||||||
|
|
||||||
|
run-dev:
|
||||||
|
sudo docker compose up
|
||||||
|
|
||||||
|
|
|
@ -9,14 +9,14 @@ http_server:
|
||||||
|
|
||||||
mysql:
|
mysql:
|
||||||
port: 3306
|
port: 3306
|
||||||
host: localhost
|
host: niki-database
|
||||||
db_name: niki_db
|
db_name: niki_db
|
||||||
username: niki
|
username: niki
|
||||||
password: nikiappt0lk2o20
|
password: nikiappt0lk2o20
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
port: 6380
|
port: 6380
|
||||||
host: localhost
|
host: niki-redis
|
||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,34 @@
|
||||||
version: '3.9'
|
version: '3.9'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
niki-core:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: development
|
||||||
|
image: niki-core
|
||||||
|
container_name: niki-core
|
||||||
|
networks:
|
||||||
|
- core
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "1313:1313"
|
||||||
|
links:
|
||||||
|
- "niki-mysql"
|
||||||
|
depends_on:
|
||||||
|
- "niki-mysql"
|
||||||
|
- "niki-redis"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
niki-mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
ports:
|
ports:
|
||||||
- "3306:3306"
|
- "3306:3306"
|
||||||
container_name: niki-database
|
container_name: niki-database
|
||||||
volumes:
|
volumes:
|
||||||
- dbdata:/var/lib/mysql
|
- dbdata:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- core
|
||||||
restart: always
|
restart: always
|
||||||
command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ]
|
command: [ 'mysqld', '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci' ]
|
||||||
environment:
|
environment:
|
||||||
|
@ -26,11 +47,16 @@ services:
|
||||||
command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no
|
command: redis-server --loglevel warning --protected-mode no --save "" --appendonly no
|
||||||
environment:
|
environment:
|
||||||
- ALLOW_EMPTY_PASSWORD=yes
|
- ALLOW_EMPTY_PASSWORD=yes
|
||||||
|
networks:
|
||||||
|
- core
|
||||||
volumes:
|
volumes:
|
||||||
- niki-redis-data:/data
|
- niki-redis-data:/data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
dbdata:
|
dbdata:
|
||||||
niki-redis-data:
|
niki-redis-data:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
core:
|
||||||
|
driver: "bridge"
|
3
go.sum
3
go.sum
|
@ -229,11 +229,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
|
||||||
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
|
||||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
|
||||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
|
||||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
|
||||||
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI=
|
||||||
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
|
||||||
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
bin/
|
||||||
|
.idea/
|
||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
dist: bionic
|
||||||
|
language: go
|
||||||
|
env: GO111MODULE=on GOFLAGS='-mod vendor'
|
||||||
|
install: true
|
||||||
|
email: false
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.10
|
||||||
|
- 1.11
|
||||||
|
- 1.12
|
||||||
|
- 1.13
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- go install github.com/golangci/golangci-lint/cmd/golangci-lint
|
||||||
|
script:
|
||||||
|
- golangci-lint run # run a bunch of code checkers/linters in parallel
|
||||||
|
- go test -v -race ./... # Run all the tests with the race detector enabled
|
|
@ -0,0 +1,63 @@
|
||||||
|
#### Support
|
||||||
|
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||||
|
|
||||||
|
#### What to contribute
|
||||||
|
If you don't know what to do, there are some features and functions that need to be done
|
||||||
|
|
||||||
|
- [ ] Refactor code
|
||||||
|
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||||
|
- [ ] Create actual list of contributors and projects that currently using this package
|
||||||
|
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||||
|
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||||
|
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||||
|
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||||
|
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||||
|
- [ ] Implement fuzzing testing
|
||||||
|
- [ ] Implement some struct/map/array utilities
|
||||||
|
- [ ] Implement map/array validation
|
||||||
|
- [ ] Implement benchmarking
|
||||||
|
- [ ] Implement batch of examples
|
||||||
|
- [ ] Look at forks for new features and fixes
|
||||||
|
|
||||||
|
#### Advice
|
||||||
|
Feel free to create what you want, but keep in mind when you implement new features:
|
||||||
|
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||||
|
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||||
|
- There are must be unit-tests for any new functions and improvements
|
||||||
|
|
||||||
|
## Financial contributions
|
||||||
|
|
||||||
|
We also welcome financial contributions in full transparency on our [open collective](https://opencollective.com/govalidator).
|
||||||
|
Anyone can file an expense. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
|
||||||
|
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
Thank you to all the people who have already contributed to govalidator!
|
||||||
|
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||||
|
|
||||||
|
|
||||||
|
### Backers
|
||||||
|
|
||||||
|
Thank you to all our backers! [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||||
|
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
|
||||||
|
Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/govalidator#sponsor))
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Alex Saskevich
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,605 @@
|
||||||
|
govalidator
|
||||||
|
===========
|
||||||
|
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/asaskevich/govalidator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![GoDoc](https://godoc.org/github.com/asaskevich/govalidator?status.png)](https://godoc.org/github.com/asaskevich/govalidator) [![Coverage Status](https://img.shields.io/coveralls/asaskevich/govalidator.svg)](https://coveralls.io/r/asaskevich/govalidator?branch=master) [![wercker status](https://app.wercker.com/status/1ec990b09ea86c910d5f08b0e02c6043/s "wercker status")](https://app.wercker.com/project/bykey/1ec990b09ea86c910d5f08b0e02c6043)
|
||||||
|
[![Build Status](https://travis-ci.org/asaskevich/govalidator.svg?branch=master)](https://travis-ci.org/asaskevich/govalidator) [![Go Report Card](https://goreportcard.com/badge/github.com/asaskevich/govalidator)](https://goreportcard.com/report/github.com/asaskevich/govalidator) [![GoSearch](http://go-search.org/badge?id=github.com%2Fasaskevich%2Fgovalidator)](http://go-search.org/view?id=github.com%2Fasaskevich%2Fgovalidator) [![Backers on Open Collective](https://opencollective.com/govalidator/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/govalidator/sponsors/badge.svg)](#sponsors) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_shield)
|
||||||
|
|
||||||
|
A package of validators and sanitizers for strings, structs and collections. Based on [validator.js](https://github.com/chriso/validator.js).
|
||||||
|
|
||||||
|
#### Installation
|
||||||
|
Make sure that Go is installed on your computer.
|
||||||
|
Type the following command in your terminal:
|
||||||
|
|
||||||
|
go get github.com/asaskevich/govalidator
|
||||||
|
|
||||||
|
or you can get specified release of the package with `gopkg.in`:
|
||||||
|
|
||||||
|
go get gopkg.in/asaskevich/govalidator.v10
|
||||||
|
|
||||||
|
After it the package is ready to use.
|
||||||
|
|
||||||
|
|
||||||
|
#### Import package in your project
|
||||||
|
Add following line in your `*.go` file:
|
||||||
|
```go
|
||||||
|
import "github.com/asaskevich/govalidator"
|
||||||
|
```
|
||||||
|
If you are unhappy to use long `govalidator`, you can do something like this:
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
valid "github.com/asaskevich/govalidator"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Activate behavior to require all fields have a validation tag by default
|
||||||
|
`SetFieldsRequiredByDefault` causes validation to fail when struct fields do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`). A good place to activate this is a package init function or the main() function.
|
||||||
|
|
||||||
|
`SetNilPtrAllowedByRequired` causes validation to pass when struct fields marked by `required` are set to nil. This is disabled by default for consistency, but some packages that need to be able to determine between `nil` and `zero value` state can use this. If disabled, both `nil` and `zero` values cause validation errors.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/asaskevich/govalidator"
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
govalidator.SetFieldsRequiredByDefault(true)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here's some code to explain it:
|
||||||
|
```go
|
||||||
|
// this struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
|
||||||
|
type exampleStruct struct {
|
||||||
|
Name string ``
|
||||||
|
Email string `valid:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// this, however, will only fail when Email is empty or an invalid email address:
|
||||||
|
type exampleStruct2 struct {
|
||||||
|
Name string `valid:"-"`
|
||||||
|
Email string `valid:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// lastly, this will only fail when Email is an invalid email address but not when it's empty:
|
||||||
|
type exampleStruct2 struct {
|
||||||
|
Name string `valid:"-"`
|
||||||
|
Email string `valid:"email,optional"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Recent breaking changes (see [#123](https://github.com/asaskevich/govalidator/pull/123))
|
||||||
|
##### Custom validator function signature
|
||||||
|
A context was added as the second parameter, for structs this is the object being validated – this makes dependent validation possible.
|
||||||
|
```go
|
||||||
|
import "github.com/asaskevich/govalidator"
|
||||||
|
|
||||||
|
// old signature
|
||||||
|
func(i interface{}) bool
|
||||||
|
|
||||||
|
// new signature
|
||||||
|
func(i interface{}, o interface{}) bool
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Adding a custom validator
|
||||||
|
This was changed to prevent data races when accessing custom validators.
|
||||||
|
```go
|
||||||
|
import "github.com/asaskevich/govalidator"
|
||||||
|
|
||||||
|
// before
|
||||||
|
govalidator.CustomTypeTagMap["customByteArrayValidator"] = CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
// after
|
||||||
|
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, o interface{}) bool {
|
||||||
|
// ...
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
#### List of functions:
|
||||||
|
```go
|
||||||
|
func Abs(value float64) float64
|
||||||
|
func BlackList(str, chars string) string
|
||||||
|
func ByteLength(str string, params ...string) bool
|
||||||
|
func CamelCaseToUnderscore(str string) string
|
||||||
|
func Contains(str, substring string) bool
|
||||||
|
func Count(array []interface{}, iterator ConditionIterator) int
|
||||||
|
func Each(array []interface{}, iterator Iterator)
|
||||||
|
func ErrorByField(e error, field string) string
|
||||||
|
func ErrorsByField(e error) map[string]string
|
||||||
|
func Filter(array []interface{}, iterator ConditionIterator) []interface{}
|
||||||
|
func Find(array []interface{}, iterator ConditionIterator) interface{}
|
||||||
|
func GetLine(s string, index int) (string, error)
|
||||||
|
func GetLines(s string) []string
|
||||||
|
func HasLowerCase(str string) bool
|
||||||
|
func HasUpperCase(str string) bool
|
||||||
|
func HasWhitespace(str string) bool
|
||||||
|
func HasWhitespaceOnly(str string) bool
|
||||||
|
func InRange(value interface{}, left interface{}, right interface{}) bool
|
||||||
|
func InRangeFloat32(value, left, right float32) bool
|
||||||
|
func InRangeFloat64(value, left, right float64) bool
|
||||||
|
func InRangeInt(value, left, right interface{}) bool
|
||||||
|
func IsASCII(str string) bool
|
||||||
|
func IsAlpha(str string) bool
|
||||||
|
func IsAlphanumeric(str string) bool
|
||||||
|
func IsBase64(str string) bool
|
||||||
|
func IsByteLength(str string, min, max int) bool
|
||||||
|
func IsCIDR(str string) bool
|
||||||
|
func IsCRC32(str string) bool
|
||||||
|
func IsCRC32b(str string) bool
|
||||||
|
func IsCreditCard(str string) bool
|
||||||
|
func IsDNSName(str string) bool
|
||||||
|
func IsDataURI(str string) bool
|
||||||
|
func IsDialString(str string) bool
|
||||||
|
func IsDivisibleBy(str, num string) bool
|
||||||
|
func IsEmail(str string) bool
|
||||||
|
func IsExistingEmail(email string) bool
|
||||||
|
func IsFilePath(str string) (bool, int)
|
||||||
|
func IsFloat(str string) bool
|
||||||
|
func IsFullWidth(str string) bool
|
||||||
|
func IsHalfWidth(str string) bool
|
||||||
|
func IsHash(str string, algorithm string) bool
|
||||||
|
func IsHexadecimal(str string) bool
|
||||||
|
func IsHexcolor(str string) bool
|
||||||
|
func IsHost(str string) bool
|
||||||
|
func IsIP(str string) bool
|
||||||
|
func IsIPv4(str string) bool
|
||||||
|
func IsIPv6(str string) bool
|
||||||
|
func IsISBN(str string, version int) bool
|
||||||
|
func IsISBN10(str string) bool
|
||||||
|
func IsISBN13(str string) bool
|
||||||
|
func IsISO3166Alpha2(str string) bool
|
||||||
|
func IsISO3166Alpha3(str string) bool
|
||||||
|
func IsISO4217(str string) bool
|
||||||
|
func IsISO693Alpha2(str string) bool
|
||||||
|
func IsISO693Alpha3b(str string) bool
|
||||||
|
func IsIn(str string, params ...string) bool
|
||||||
|
func IsInRaw(str string, params ...string) bool
|
||||||
|
func IsInt(str string) bool
|
||||||
|
func IsJSON(str string) bool
|
||||||
|
func IsLatitude(str string) bool
|
||||||
|
func IsLongitude(str string) bool
|
||||||
|
func IsLowerCase(str string) bool
|
||||||
|
func IsMAC(str string) bool
|
||||||
|
func IsMD4(str string) bool
|
||||||
|
func IsMD5(str string) bool
|
||||||
|
func IsMagnetURI(str string) bool
|
||||||
|
func IsMongoID(str string) bool
|
||||||
|
func IsMultibyte(str string) bool
|
||||||
|
func IsNatural(value float64) bool
|
||||||
|
func IsNegative(value float64) bool
|
||||||
|
func IsNonNegative(value float64) bool
|
||||||
|
func IsNonPositive(value float64) bool
|
||||||
|
func IsNotNull(str string) bool
|
||||||
|
func IsNull(str string) bool
|
||||||
|
func IsNumeric(str string) bool
|
||||||
|
func IsPort(str string) bool
|
||||||
|
func IsPositive(value float64) bool
|
||||||
|
func IsPrintableASCII(str string) bool
|
||||||
|
func IsRFC3339(str string) bool
|
||||||
|
func IsRFC3339WithoutZone(str string) bool
|
||||||
|
func IsRGBcolor(str string) bool
|
||||||
|
func IsRequestURI(rawurl string) bool
|
||||||
|
func IsRequestURL(rawurl string) bool
|
||||||
|
func IsRipeMD128(str string) bool
|
||||||
|
func IsRipeMD160(str string) bool
|
||||||
|
func IsRsaPub(str string, params ...string) bool
|
||||||
|
func IsRsaPublicKey(str string, keylen int) bool
|
||||||
|
func IsSHA1(str string) bool
|
||||||
|
func IsSHA256(str string) bool
|
||||||
|
func IsSHA384(str string) bool
|
||||||
|
func IsSHA512(str string) bool
|
||||||
|
func IsSSN(str string) bool
|
||||||
|
func IsSemver(str string) bool
|
||||||
|
func IsTiger128(str string) bool
|
||||||
|
func IsTiger160(str string) bool
|
||||||
|
func IsTiger192(str string) bool
|
||||||
|
func IsTime(str string, format string) bool
|
||||||
|
func IsType(v interface{}, params ...string) bool
|
||||||
|
func IsURL(str string) bool
|
||||||
|
func IsUTFDigit(str string) bool
|
||||||
|
func IsUTFLetter(str string) bool
|
||||||
|
func IsUTFLetterNumeric(str string) bool
|
||||||
|
func IsUTFNumeric(str string) bool
|
||||||
|
func IsUUID(str string) bool
|
||||||
|
func IsUUIDv3(str string) bool
|
||||||
|
func IsUUIDv4(str string) bool
|
||||||
|
func IsUUIDv5(str string) bool
|
||||||
|
func IsUnixTime(str string) bool
|
||||||
|
func IsUpperCase(str string) bool
|
||||||
|
func IsVariableWidth(str string) bool
|
||||||
|
func IsWhole(value float64) bool
|
||||||
|
func LeftTrim(str, chars string) string
|
||||||
|
func Map(array []interface{}, iterator ResultIterator) []interface{}
|
||||||
|
func Matches(str, pattern string) bool
|
||||||
|
func MaxStringLength(str string, params ...string) bool
|
||||||
|
func MinStringLength(str string, params ...string) bool
|
||||||
|
func NormalizeEmail(str string) (string, error)
|
||||||
|
func PadBoth(str string, padStr string, padLen int) string
|
||||||
|
func PadLeft(str string, padStr string, padLen int) string
|
||||||
|
func PadRight(str string, padStr string, padLen int) string
|
||||||
|
func PrependPathToErrors(err error, path string) error
|
||||||
|
func Range(str string, params ...string) bool
|
||||||
|
func RemoveTags(s string) string
|
||||||
|
func ReplacePattern(str, pattern, replace string) string
|
||||||
|
func Reverse(s string) string
|
||||||
|
func RightTrim(str, chars string) string
|
||||||
|
func RuneLength(str string, params ...string) bool
|
||||||
|
func SafeFileName(str string) string
|
||||||
|
func SetFieldsRequiredByDefault(value bool)
|
||||||
|
func SetNilPtrAllowedByRequired(value bool)
|
||||||
|
func Sign(value float64) float64
|
||||||
|
func StringLength(str string, params ...string) bool
|
||||||
|
func StringMatches(s string, params ...string) bool
|
||||||
|
func StripLow(str string, keepNewLines bool) string
|
||||||
|
func ToBoolean(str string) (bool, error)
|
||||||
|
func ToFloat(str string) (float64, error)
|
||||||
|
func ToInt(value interface{}) (res int64, err error)
|
||||||
|
func ToJSON(obj interface{}) (string, error)
|
||||||
|
func ToString(obj interface{}) string
|
||||||
|
func Trim(str, chars string) string
|
||||||
|
func Truncate(str string, length int, ending string) string
|
||||||
|
func TruncatingErrorf(str string, args ...interface{}) error
|
||||||
|
func UnderscoreToCamelCase(s string) string
|
||||||
|
func ValidateMap(s map[string]interface{}, m map[string]interface{}) (bool, error)
|
||||||
|
func ValidateStruct(s interface{}) (bool, error)
|
||||||
|
func WhiteList(str, chars string) string
|
||||||
|
type ConditionIterator
|
||||||
|
type CustomTypeValidator
|
||||||
|
type Error
|
||||||
|
func (e Error) Error() string
|
||||||
|
type Errors
|
||||||
|
func (es Errors) Error() string
|
||||||
|
func (es Errors) Errors() []error
|
||||||
|
type ISO3166Entry
|
||||||
|
type ISO693Entry
|
||||||
|
type InterfaceParamValidator
|
||||||
|
type Iterator
|
||||||
|
type ParamValidator
|
||||||
|
type ResultIterator
|
||||||
|
type UnsupportedTypeError
|
||||||
|
func (e *UnsupportedTypeError) Error() string
|
||||||
|
type Validator
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
###### IsURL
|
||||||
|
```go
|
||||||
|
println(govalidator.IsURL(`http://user@pass:domain.com/path/page`))
|
||||||
|
```
|
||||||
|
###### IsType
|
||||||
|
```go
|
||||||
|
println(govalidator.IsType("Bob", "string"))
|
||||||
|
println(govalidator.IsType(1, "int"))
|
||||||
|
i := 1
|
||||||
|
println(govalidator.IsType(&i, "*int"))
|
||||||
|
```
|
||||||
|
|
||||||
|
IsType can be used through the tag `type` which is essential for map validation:
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
Name string `valid:"type(string)"`
|
||||||
|
Age int `valid:"type(int)"`
|
||||||
|
Meta interface{} `valid:"type(string)"`
|
||||||
|
}
|
||||||
|
result, err := govalidator.ValidateStruct(user{"Bob", 20, "meta"})
|
||||||
|
if err != nil {
|
||||||
|
println("error: " + err.Error())
|
||||||
|
}
|
||||||
|
println(result)
|
||||||
|
```
|
||||||
|
###### ToString
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
}
|
||||||
|
|
||||||
|
str := govalidator.ToString(&User{"John", "Juan"})
|
||||||
|
println(str)
|
||||||
|
```
|
||||||
|
###### Each, Map, Filter, Count for slices
|
||||||
|
Each iterates over the slice/array and calls Iterator for every item
|
||||||
|
```go
|
||||||
|
data := []interface{}{1, 2, 3, 4, 5}
|
||||||
|
var fn govalidator.Iterator = func(value interface{}, index int) {
|
||||||
|
println(value.(int))
|
||||||
|
}
|
||||||
|
govalidator.Each(data, fn)
|
||||||
|
```
|
||||||
|
```go
|
||||||
|
data := []interface{}{1, 2, 3, 4, 5}
|
||||||
|
var fn govalidator.ResultIterator = func(value interface{}, index int) interface{} {
|
||||||
|
return value.(int) * 3
|
||||||
|
}
|
||||||
|
_ = govalidator.Map(data, fn) // result = []interface{}{1, 6, 9, 12, 15}
|
||||||
|
```
|
||||||
|
```go
|
||||||
|
data := []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
|
||||||
|
var fn govalidator.ConditionIterator = func(value interface{}, index int) bool {
|
||||||
|
return value.(int)%2 == 0
|
||||||
|
}
|
||||||
|
_ = govalidator.Filter(data, fn) // result = []interface{}{2, 4, 6, 8, 10}
|
||||||
|
_ = govalidator.Count(data, fn) // result = 5
|
||||||
|
```
|
||||||
|
###### ValidateStruct [#2](https://github.com/asaskevich/govalidator/pull/2)
|
||||||
|
If you want to validate structs, you can use tag `valid` for any field in your structure. All validators used with this field in one tag are separated by comma. If you want to skip validation, place `-` in your tag. If you need a validator that is not on the list below, you can add it like this:
|
||||||
|
```go
|
||||||
|
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||||
|
return str == "duck"
|
||||||
|
})
|
||||||
|
```
|
||||||
|
For completely custom validators (interface-based), see below.
|
||||||
|
|
||||||
|
Here is a list of available validators for struct fields (validator - used function):
|
||||||
|
```go
|
||||||
|
"email": IsEmail,
|
||||||
|
"url": IsURL,
|
||||||
|
"dialstring": IsDialString,
|
||||||
|
"requrl": IsRequestURL,
|
||||||
|
"requri": IsRequestURI,
|
||||||
|
"alpha": IsAlpha,
|
||||||
|
"utfletter": IsUTFLetter,
|
||||||
|
"alphanum": IsAlphanumeric,
|
||||||
|
"utfletternum": IsUTFLetterNumeric,
|
||||||
|
"numeric": IsNumeric,
|
||||||
|
"utfnumeric": IsUTFNumeric,
|
||||||
|
"utfdigit": IsUTFDigit,
|
||||||
|
"hexadecimal": IsHexadecimal,
|
||||||
|
"hexcolor": IsHexcolor,
|
||||||
|
"rgbcolor": IsRGBcolor,
|
||||||
|
"lowercase": IsLowerCase,
|
||||||
|
"uppercase": IsUpperCase,
|
||||||
|
"int": IsInt,
|
||||||
|
"float": IsFloat,
|
||||||
|
"null": IsNull,
|
||||||
|
"uuid": IsUUID,
|
||||||
|
"uuidv3": IsUUIDv3,
|
||||||
|
"uuidv4": IsUUIDv4,
|
||||||
|
"uuidv5": IsUUIDv5,
|
||||||
|
"creditcard": IsCreditCard,
|
||||||
|
"isbn10": IsISBN10,
|
||||||
|
"isbn13": IsISBN13,
|
||||||
|
"json": IsJSON,
|
||||||
|
"multibyte": IsMultibyte,
|
||||||
|
"ascii": IsASCII,
|
||||||
|
"printableascii": IsPrintableASCII,
|
||||||
|
"fullwidth": IsFullWidth,
|
||||||
|
"halfwidth": IsHalfWidth,
|
||||||
|
"variablewidth": IsVariableWidth,
|
||||||
|
"base64": IsBase64,
|
||||||
|
"datauri": IsDataURI,
|
||||||
|
"ip": IsIP,
|
||||||
|
"port": IsPort,
|
||||||
|
"ipv4": IsIPv4,
|
||||||
|
"ipv6": IsIPv6,
|
||||||
|
"dns": IsDNSName,
|
||||||
|
"host": IsHost,
|
||||||
|
"mac": IsMAC,
|
||||||
|
"latitude": IsLatitude,
|
||||||
|
"longitude": IsLongitude,
|
||||||
|
"ssn": IsSSN,
|
||||||
|
"semver": IsSemver,
|
||||||
|
"rfc3339": IsRFC3339,
|
||||||
|
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||||
|
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||||
|
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||||
|
```
|
||||||
|
Validators with parameters
|
||||||
|
|
||||||
|
```go
|
||||||
|
"range(min|max)": Range,
|
||||||
|
"length(min|max)": ByteLength,
|
||||||
|
"runelength(min|max)": RuneLength,
|
||||||
|
"stringlength(min|max)": StringLength,
|
||||||
|
"matches(pattern)": StringMatches,
|
||||||
|
"in(string1|string2|...|stringN)": IsIn,
|
||||||
|
"rsapub(keylength)" : IsRsaPub,
|
||||||
|
```
|
||||||
|
Validators with parameters for any type
|
||||||
|
|
||||||
|
```go
|
||||||
|
"type(type)": IsType,
|
||||||
|
```
|
||||||
|
|
||||||
|
And here is small example of usage:
|
||||||
|
```go
|
||||||
|
type Post struct {
|
||||||
|
Title string `valid:"alphanum,required"`
|
||||||
|
Message string `valid:"duck,ascii"`
|
||||||
|
Message2 string `valid:"animal(dog)"`
|
||||||
|
AuthorIP string `valid:"ipv4"`
|
||||||
|
Date string `valid:"-"`
|
||||||
|
}
|
||||||
|
post := &Post{
|
||||||
|
Title: "My Example Post",
|
||||||
|
Message: "duck",
|
||||||
|
Message2: "dog",
|
||||||
|
AuthorIP: "123.234.54.3",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add your own struct validation tags
|
||||||
|
govalidator.TagMap["duck"] = govalidator.Validator(func(str string) bool {
|
||||||
|
return str == "duck"
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add your own struct validation tags with parameter
|
||||||
|
govalidator.ParamTagMap["animal"] = govalidator.ParamValidator(func(str string, params ...string) bool {
|
||||||
|
species := params[0]
|
||||||
|
return str == species
|
||||||
|
})
|
||||||
|
govalidator.ParamTagRegexMap["animal"] = regexp.MustCompile("^animal\\((\\w+)\\)$")
|
||||||
|
|
||||||
|
result, err := govalidator.ValidateStruct(post)
|
||||||
|
if err != nil {
|
||||||
|
println("error: " + err.Error())
|
||||||
|
}
|
||||||
|
println(result)
|
||||||
|
```
|
||||||
|
###### ValidateMap [#2](https://github.com/asaskevich/govalidator/pull/338)
|
||||||
|
If you want to validate maps, you can use the map to be validated and a validation map that contain the same tags used in ValidateStruct, both maps have to be in the form `map[string]interface{}`
|
||||||
|
|
||||||
|
So here is small example of usage:
|
||||||
|
```go
|
||||||
|
var mapTemplate = map[string]interface{}{
|
||||||
|
"name":"required,alpha",
|
||||||
|
"family":"required,alpha",
|
||||||
|
"email":"required,email",
|
||||||
|
"cell-phone":"numeric",
|
||||||
|
"address":map[string]interface{}{
|
||||||
|
"line1":"required,alphanum",
|
||||||
|
"line2":"alphanum",
|
||||||
|
"postal-code":"numeric",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputMap = map[string]interface{}{
|
||||||
|
"name":"Bob",
|
||||||
|
"family":"Smith",
|
||||||
|
"email":"foo@bar.baz",
|
||||||
|
"address":map[string]interface{}{
|
||||||
|
"line1":"",
|
||||||
|
"line2":"",
|
||||||
|
"postal-code":"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := govalidator.ValidateMap(mapTemplate, inputMap)
|
||||||
|
if err != nil {
|
||||||
|
println("error: " + err.Error())
|
||||||
|
}
|
||||||
|
println(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
###### WhiteList
|
||||||
|
```go
|
||||||
|
// Remove all characters from string ignoring characters between "a" and "z"
|
||||||
|
println(govalidator.WhiteList("a3a43a5a4a3a2a23a4a5a4a3a4", "a-z") == "aaaaaaaaaaaa")
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Custom validation functions
|
||||||
|
Custom validation using your own domain specific validators is also available - here's an example of how to use it:
|
||||||
|
```go
|
||||||
|
import "github.com/asaskevich/govalidator"
|
||||||
|
|
||||||
|
type CustomByteArray [6]byte // custom types are supported and can be validated
|
||||||
|
|
||||||
|
type StructWithCustomByteArray struct {
|
||||||
|
ID CustomByteArray `valid:"customByteArrayValidator,customMinLengthValidator"` // multiple custom validators are possible as well and will be evaluated in sequence
|
||||||
|
Email string `valid:"email"`
|
||||||
|
CustomMinLength int `valid:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
govalidator.CustomTypeTagMap.Set("customByteArrayValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||||
|
switch v := context.(type) { // you can type switch on the context interface being validated
|
||||||
|
case StructWithCustomByteArray:
|
||||||
|
// you can check and validate against some other field in the context,
|
||||||
|
// return early or not validate against the context at all – your choice
|
||||||
|
case SomeOtherType:
|
||||||
|
// ...
|
||||||
|
default:
|
||||||
|
// expecting some other type? Throw/panic here or continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := i.(type) { // type switch on the struct field being validated
|
||||||
|
case CustomByteArray:
|
||||||
|
for _, e := range v { // this validator checks that the byte array is not empty, i.e. not all zeroes
|
||||||
|
if e != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}))
|
||||||
|
govalidator.CustomTypeTagMap.Set("customMinLengthValidator", CustomTypeValidator(func(i interface{}, context interface{}) bool {
|
||||||
|
switch v := context.(type) { // this validates a field against the value in another field, i.e. dependent validation
|
||||||
|
case StructWithCustomByteArray:
|
||||||
|
return len(v.ID) >= v.CustomMinLength
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
###### Custom error messages
|
||||||
|
Custom error messages are supported via annotations by adding the `~` separator - here's an example of how to use it:
|
||||||
|
```go
|
||||||
|
type Ticket struct {
|
||||||
|
Id int64 `json:"id"`
|
||||||
|
FirstName string `json:"firstname" valid:"required~First name is blank"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
Documentation is available here: [godoc.org](https://godoc.org/github.com/asaskevich/govalidator).
|
||||||
|
Full information about code coverage is also available here: [govalidator on gocover.io](http://gocover.io/github.com/asaskevich/govalidator).
|
||||||
|
|
||||||
|
#### Support
|
||||||
|
If you do have a contribution to the package, feel free to create a Pull Request or an Issue.
|
||||||
|
|
||||||
|
#### What to contribute
|
||||||
|
If you don't know what to do, there are some features and functions that need to be done
|
||||||
|
|
||||||
|
- [ ] Refactor code
|
||||||
|
- [ ] Edit docs and [README](https://github.com/asaskevich/govalidator/README.md): spellcheck, grammar and typo check
|
||||||
|
- [ ] Create actual list of contributors and projects that currently using this package
|
||||||
|
- [ ] Resolve [issues and bugs](https://github.com/asaskevich/govalidator/issues)
|
||||||
|
- [ ] Update actual [list of functions](https://github.com/asaskevich/govalidator#list-of-functions)
|
||||||
|
- [ ] Update [list of validators](https://github.com/asaskevich/govalidator#validatestruct-2) that available for `ValidateStruct` and add new
|
||||||
|
- [ ] Implement new validators: `IsFQDN`, `IsIMEI`, `IsPostalCode`, `IsISIN`, `IsISRC` etc
|
||||||
|
- [x] Implement [validation by maps](https://github.com/asaskevich/govalidator/issues/224)
|
||||||
|
- [ ] Implement fuzzing testing
|
||||||
|
- [ ] Implement some struct/map/array utilities
|
||||||
|
- [ ] Implement map/array validation
|
||||||
|
- [ ] Implement benchmarking
|
||||||
|
- [ ] Implement batch of examples
|
||||||
|
- [ ] Look at forks for new features and fixes
|
||||||
|
|
||||||
|
#### Advice
|
||||||
|
Feel free to create what you want, but keep in mind when you implement new features:
|
||||||
|
- Code must be clear and readable, names of variables/constants clearly describes what they are doing
|
||||||
|
- Public functions must be documented and described in source file and added to README.md to the list of available functions
|
||||||
|
- There are must be unit-tests for any new functions and improvements
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
### Contributors
|
||||||
|
|
||||||
|
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)].
|
||||||
|
|
||||||
|
#### Special thanks to [contributors](https://github.com/asaskevich/govalidator/graphs/contributors)
|
||||||
|
* [Daniel Lohse](https://github.com/annismckenzie)
|
||||||
|
* [Attila Oláh](https://github.com/attilaolah)
|
||||||
|
* [Daniel Korner](https://github.com/Dadie)
|
||||||
|
* [Steven Wilkin](https://github.com/stevenwilkin)
|
||||||
|
* [Deiwin Sarjas](https://github.com/deiwin)
|
||||||
|
* [Noah Shibley](https://github.com/slugmobile)
|
||||||
|
* [Nathan Davies](https://github.com/nathj07)
|
||||||
|
* [Matt Sanford](https://github.com/mzsanford)
|
||||||
|
* [Simon ccl1115](https://github.com/ccl1115)
|
||||||
|
|
||||||
|
<a href="https://github.com/asaskevich/govalidator/graphs/contributors"><img src="https://opencollective.com/govalidator/contributors.svg?width=890" /></a>
|
||||||
|
|
||||||
|
|
||||||
|
### Backers
|
||||||
|
|
||||||
|
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/govalidator#backer)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/govalidator#backers" target="_blank"><img src="https://opencollective.com/govalidator/backers.svg?width=890"></a>
|
||||||
|
|
||||||
|
|
||||||
|
### Sponsors
|
||||||
|
|
||||||
|
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/govalidator#sponsor)]
|
||||||
|
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/0/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/0/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/1/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/1/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/2/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/2/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/3/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/3/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/4/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/4/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/5/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/5/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/6/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/6/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/7/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/7/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/8/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/8/avatar.svg"></a>
|
||||||
|
<a href="https://opencollective.com/govalidator/sponsor/9/website" target="_blank"><img src="https://opencollective.com/govalidator/sponsor/9/avatar.svg"></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Fasaskevich%2Fgovalidator?ref=badge_large)
|
|
@ -0,0 +1,58 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
// Iterator is the function that accepts element of slice/array and its index
|
||||||
|
type Iterator func(interface{}, int)
|
||||||
|
|
||||||
|
// ResultIterator is the function that accepts element of slice/array and its index and returns any result
|
||||||
|
type ResultIterator func(interface{}, int) interface{}
|
||||||
|
|
||||||
|
// ConditionIterator is the function that accepts element of slice/array and its index and returns boolean
|
||||||
|
type ConditionIterator func(interface{}, int) bool
|
||||||
|
|
||||||
|
// Each iterates over the slice and apply Iterator to every item
|
||||||
|
func Each(array []interface{}, iterator Iterator) {
|
||||||
|
for index, data := range array {
|
||||||
|
iterator(data, index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map iterates over the slice and apply ResultIterator to every item. Returns new slice as a result.
|
||||||
|
func Map(array []interface{}, iterator ResultIterator) []interface{} {
|
||||||
|
var result = make([]interface{}, len(array))
|
||||||
|
for index, data := range array {
|
||||||
|
result[index] = iterator(data, index)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find iterates over the slice and apply ConditionIterator to every item. Returns first item that meet ConditionIterator or nil otherwise.
|
||||||
|
func Find(array []interface{}, iterator ConditionIterator) interface{} {
|
||||||
|
for index, data := range array {
|
||||||
|
if iterator(data, index) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter iterates over the slice and apply ConditionIterator to every item. Returns new slice.
|
||||||
|
func Filter(array []interface{}, iterator ConditionIterator) []interface{} {
|
||||||
|
var result = make([]interface{}, 0)
|
||||||
|
for index, data := range array {
|
||||||
|
if iterator(data, index) {
|
||||||
|
result = append(result, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count iterates over the slice and apply ConditionIterator to every item. Returns count of items that meets ConditionIterator.
|
||||||
|
func Count(array []interface{}, iterator ConditionIterator) int {
|
||||||
|
count := 0
|
||||||
|
for index, data := range array {
|
||||||
|
if iterator(data, index) {
|
||||||
|
count = count + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToString convert the input to a string.
|
||||||
|
func ToString(obj interface{}) string {
|
||||||
|
res := fmt.Sprintf("%v", obj)
|
||||||
|
return string(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON convert the input to a valid JSON string
|
||||||
|
func ToJSON(obj interface{}) (string, error) {
|
||||||
|
res, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
res = []byte("")
|
||||||
|
}
|
||||||
|
return string(res), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
|
||||||
|
func ToFloat(str string) (float64, error) {
|
||||||
|
res, err := strconv.ParseFloat(str, 64)
|
||||||
|
if err != nil {
|
||||||
|
res = 0.0
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
|
||||||
|
func ToInt(value interface{}) (res int64, err error) {
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
res = val.Int()
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
res = int64(val.Uint())
|
||||||
|
case string:
|
||||||
|
if IsInt(val.String()) {
|
||||||
|
res, err = strconv.ParseInt(val.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBoolean convert the input string to a boolean.
|
||||||
|
func ToBoolean(str string) (bool, error) {
|
||||||
|
return strconv.ParseBool(str)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
// A package of validators and sanitizers for strings, structures and collections.
|
|
@ -0,0 +1,43 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// Errors is an array of multiple errors and conforms to the error interface.
|
||||||
|
type Errors []error
|
||||||
|
|
||||||
|
// Errors returns itself.
|
||||||
|
func (es Errors) Errors() []error {
|
||||||
|
return es
|
||||||
|
}
|
||||||
|
|
||||||
|
func (es Errors) Error() string {
|
||||||
|
var errs []string
|
||||||
|
for _, e := range es {
|
||||||
|
errs = append(errs, e.Error())
|
||||||
|
}
|
||||||
|
return strings.Join(errs, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error encapsulates a name, an error and whether there's a custom error message or not.
|
||||||
|
type Error struct {
|
||||||
|
Name string
|
||||||
|
Err error
|
||||||
|
CustomErrorMessageExists bool
|
||||||
|
|
||||||
|
// Validator indicates the name of the validator that failed
|
||||||
|
Validator string
|
||||||
|
Path []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
if e.CustomErrorMessageExists {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
errName := e.Name
|
||||||
|
if len(e.Path) > 0 {
|
||||||
|
errName = strings.Join(append(e.Path, e.Name), ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
return errName + ": " + e.Err.Error()
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Abs returns absolute value of number
|
||||||
|
func Abs(value float64) float64 {
|
||||||
|
return math.Abs(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign returns signum of number: 1 in case of value > 0, -1 in case of value < 0, 0 otherwise
|
||||||
|
func Sign(value float64) float64 {
|
||||||
|
if value > 0 {
|
||||||
|
return 1
|
||||||
|
} else if value < 0 {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNegative returns true if value < 0
|
||||||
|
func IsNegative(value float64) bool {
|
||||||
|
return value < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPositive returns true if value > 0
|
||||||
|
func IsPositive(value float64) bool {
|
||||||
|
return value > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNonNegative returns true if value >= 0
|
||||||
|
func IsNonNegative(value float64) bool {
|
||||||
|
return value >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNonPositive returns true if value <= 0
|
||||||
|
func IsNonPositive(value float64) bool {
|
||||||
|
return value <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// InRange returns true if value lies between left and right border
|
||||||
|
func InRangeInt(value, left, right interface{}) bool {
|
||||||
|
value64, _ := ToInt(value)
|
||||||
|
left64, _ := ToInt(left)
|
||||||
|
right64, _ := ToInt(right)
|
||||||
|
if left64 > right64 {
|
||||||
|
left64, right64 = right64, left64
|
||||||
|
}
|
||||||
|
return value64 >= left64 && value64 <= right64
|
||||||
|
}
|
||||||
|
|
||||||
|
// InRange returns true if value lies between left and right border
|
||||||
|
func InRangeFloat32(value, left, right float32) bool {
|
||||||
|
if left > right {
|
||||||
|
left, right = right, left
|
||||||
|
}
|
||||||
|
return value >= left && value <= right
|
||||||
|
}
|
||||||
|
|
||||||
|
// InRange returns true if value lies between left and right border
|
||||||
|
func InRangeFloat64(value, left, right float64) bool {
|
||||||
|
if left > right {
|
||||||
|
left, right = right, left
|
||||||
|
}
|
||||||
|
return value >= left && value <= right
|
||||||
|
}
|
||||||
|
|
||||||
|
// InRange returns true if value lies between left and right border, generic type to handle int, float32 or float64, all types must the same type
|
||||||
|
func InRange(value interface{}, left interface{}, right interface{}) bool {
|
||||||
|
|
||||||
|
reflectValue := reflect.TypeOf(value).Kind()
|
||||||
|
reflectLeft := reflect.TypeOf(left).Kind()
|
||||||
|
reflectRight := reflect.TypeOf(right).Kind()
|
||||||
|
|
||||||
|
if reflectValue == reflect.Int && reflectLeft == reflect.Int && reflectRight == reflect.Int {
|
||||||
|
return InRangeInt(value.(int), left.(int), right.(int))
|
||||||
|
} else if reflectValue == reflect.Float32 && reflectLeft == reflect.Float32 && reflectRight == reflect.Float32 {
|
||||||
|
return InRangeFloat32(value.(float32), left.(float32), right.(float32))
|
||||||
|
} else if reflectValue == reflect.Float64 && reflectLeft == reflect.Float64 && reflectRight == reflect.Float64 {
|
||||||
|
return InRangeFloat64(value.(float64), left.(float64), right.(float64))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWhole returns true if value is whole number
|
||||||
|
func IsWhole(value float64) bool {
|
||||||
|
return math.Remainder(value, 1) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNatural returns true if value is natural number (positive and whole)
|
||||||
|
func IsNatural(value float64) bool {
|
||||||
|
return IsWhole(value) && IsPositive(value)
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
// Basic regular expressions for validating strings
|
||||||
|
const (
|
||||||
|
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||||
|
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|(222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11}|6[27][0-9]{14})$"
|
||||||
|
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||||
|
ISBN13 string = "^(?:[0-9]{13})$"
|
||||||
|
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||||
|
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||||
|
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||||
|
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||||
|
Alpha string = "^[a-zA-Z]+$"
|
||||||
|
Alphanumeric string = "^[a-zA-Z0-9]+$"
|
||||||
|
Numeric string = "^[0-9]+$"
|
||||||
|
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
|
||||||
|
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
|
||||||
|
Hexadecimal string = "^[0-9a-fA-F]+$"
|
||||||
|
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||||
|
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
|
||||||
|
ASCII string = "^[\x00-\x7F]+$"
|
||||||
|
Multibyte string = "[^\x00-\x7F]"
|
||||||
|
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||||
|
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||||
|
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||||
|
PrintableASCII string = "^[\x20-\x7E]+$"
|
||||||
|
DataURI string = "^data:.+\\/(.+);base64$"
|
||||||
|
MagnetURI string = "^magnet:\\?xt=urn:[a-zA-Z0-9]+:[a-zA-Z0-9]{32,40}&dn=.+&tr=.+$"
|
||||||
|
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||||
|
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||||
|
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
|
||||||
|
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||||
|
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||||
|
URLUsername string = `(\S+(:\S*)?@)`
|
||||||
|
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||||
|
URLPort string = `(:(\d{1,5}))`
|
||||||
|
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
|
||||||
|
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
|
||||||
|
URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
|
||||||
|
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||||
|
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||||
|
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||||
|
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||||
|
tagName string = "valid"
|
||||||
|
hasLowerCase string = ".*[[:lower:]]"
|
||||||
|
hasUpperCase string = ".*[[:upper:]]"
|
||||||
|
hasWhitespace string = ".*[[:space:]]"
|
||||||
|
hasWhitespaceOnly string = "^[[:space:]]+$"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Used by IsFilePath func
|
||||||
|
const (
|
||||||
|
// Unknown is unresolved OS type
|
||||||
|
Unknown = iota
|
||||||
|
// Win is Windows type
|
||||||
|
Win
|
||||||
|
// Unix is *nix OS types
|
||||||
|
Unix
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
userRegexp = regexp.MustCompile("^[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+$")
|
||||||
|
hostRegexp = regexp.MustCompile("^[^\\s]+\\.[^\\s]+$")
|
||||||
|
userDotRegexp = regexp.MustCompile("(^[.]{1})|([.]{1}$)|([.]{2,})")
|
||||||
|
rxEmail = regexp.MustCompile(Email)
|
||||||
|
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||||
|
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||||
|
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||||
|
rxUUID3 = regexp.MustCompile(UUID3)
|
||||||
|
rxUUID4 = regexp.MustCompile(UUID4)
|
||||||
|
rxUUID5 = regexp.MustCompile(UUID5)
|
||||||
|
rxUUID = regexp.MustCompile(UUID)
|
||||||
|
rxAlpha = regexp.MustCompile(Alpha)
|
||||||
|
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
|
||||||
|
rxNumeric = regexp.MustCompile(Numeric)
|
||||||
|
rxInt = regexp.MustCompile(Int)
|
||||||
|
rxFloat = regexp.MustCompile(Float)
|
||||||
|
rxHexadecimal = regexp.MustCompile(Hexadecimal)
|
||||||
|
rxHexcolor = regexp.MustCompile(Hexcolor)
|
||||||
|
rxRGBcolor = regexp.MustCompile(RGBcolor)
|
||||||
|
rxASCII = regexp.MustCompile(ASCII)
|
||||||
|
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
|
||||||
|
rxMultibyte = regexp.MustCompile(Multibyte)
|
||||||
|
rxFullWidth = regexp.MustCompile(FullWidth)
|
||||||
|
rxHalfWidth = regexp.MustCompile(HalfWidth)
|
||||||
|
rxBase64 = regexp.MustCompile(Base64)
|
||||||
|
rxDataURI = regexp.MustCompile(DataURI)
|
||||||
|
rxMagnetURI = regexp.MustCompile(MagnetURI)
|
||||||
|
rxLatitude = regexp.MustCompile(Latitude)
|
||||||
|
rxLongitude = regexp.MustCompile(Longitude)
|
||||||
|
rxDNSName = regexp.MustCompile(DNSName)
|
||||||
|
rxURL = regexp.MustCompile(URL)
|
||||||
|
rxSSN = regexp.MustCompile(SSN)
|
||||||
|
rxWinPath = regexp.MustCompile(WinPath)
|
||||||
|
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||||
|
rxSemver = regexp.MustCompile(Semver)
|
||||||
|
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
|
||||||
|
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
|
||||||
|
rxHasWhitespace = regexp.MustCompile(hasWhitespace)
|
||||||
|
rxHasWhitespaceOnly = regexp.MustCompile(hasWhitespaceOnly)
|
||||||
|
)
|
|
@ -0,0 +1,652 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Validator is a wrapper for a validator function that returns bool and accepts string.
|
||||||
|
type Validator func(str string) bool
|
||||||
|
|
||||||
|
// CustomTypeValidator is a wrapper for validator functions that returns bool and accepts any type.
|
||||||
|
// The second parameter should be the context (in the case of validating a struct: the whole object being validated).
|
||||||
|
type CustomTypeValidator func(i interface{}, o interface{}) bool
|
||||||
|
|
||||||
|
// ParamValidator is a wrapper for validator functions that accepts additional parameters.
|
||||||
|
type ParamValidator func(str string, params ...string) bool
|
||||||
|
type InterfaceParamValidator func(in interface{}, params ...string) bool
|
||||||
|
type tagOptionsMap map[string]tagOption
|
||||||
|
|
||||||
|
func (t tagOptionsMap) orderedKeys() []string {
|
||||||
|
var keys []string
|
||||||
|
for k := range t {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(keys, func(a, b int) bool {
|
||||||
|
return t[keys[a]].order < t[keys[b]].order
|
||||||
|
})
|
||||||
|
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
type tagOption struct {
|
||||||
|
name string
|
||||||
|
customErrorMessage string
|
||||||
|
order int
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsupportedTypeError is a wrapper for reflect.Type
|
||||||
|
type UnsupportedTypeError struct {
|
||||||
|
Type reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringValues is a slice of reflect.Value holding *reflect.StringValue.
|
||||||
|
// It implements the methods to sort by string.
|
||||||
|
type stringValues []reflect.Value
|
||||||
|
|
||||||
|
// InterfaceParamTagMap is a map of functions accept variants parameters for an interface value
|
||||||
|
var InterfaceParamTagMap = map[string]InterfaceParamValidator{
|
||||||
|
"type": IsType,
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceParamTagRegexMap maps interface param tags to their respective regexes.
|
||||||
|
var InterfaceParamTagRegexMap = map[string]*regexp.Regexp{
|
||||||
|
"type": regexp.MustCompile(`^type\((.*)\)$`),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParamTagMap is a map of functions accept variants parameters
|
||||||
|
var ParamTagMap = map[string]ParamValidator{
|
||||||
|
"length": ByteLength,
|
||||||
|
"range": Range,
|
||||||
|
"runelength": RuneLength,
|
||||||
|
"stringlength": StringLength,
|
||||||
|
"matches": StringMatches,
|
||||||
|
"in": IsInRaw,
|
||||||
|
"rsapub": IsRsaPub,
|
||||||
|
"minstringlength": MinStringLength,
|
||||||
|
"maxstringlength": MaxStringLength,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParamTagRegexMap maps param tags to their respective regexes.
|
||||||
|
var ParamTagRegexMap = map[string]*regexp.Regexp{
|
||||||
|
"range": regexp.MustCompile("^range\\((\\d+)\\|(\\d+)\\)$"),
|
||||||
|
"length": regexp.MustCompile("^length\\((\\d+)\\|(\\d+)\\)$"),
|
||||||
|
"runelength": regexp.MustCompile("^runelength\\((\\d+)\\|(\\d+)\\)$"),
|
||||||
|
"stringlength": regexp.MustCompile("^stringlength\\((\\d+)\\|(\\d+)\\)$"),
|
||||||
|
"in": regexp.MustCompile(`^in\((.*)\)`),
|
||||||
|
"matches": regexp.MustCompile(`^matches\((.+)\)$`),
|
||||||
|
"rsapub": regexp.MustCompile("^rsapub\\((\\d+)\\)$"),
|
||||||
|
"minstringlength": regexp.MustCompile("^minstringlength\\((\\d+)\\)$"),
|
||||||
|
"maxstringlength": regexp.MustCompile("^maxstringlength\\((\\d+)\\)$"),
|
||||||
|
}
|
||||||
|
|
||||||
|
type customTypeTagMap struct {
|
||||||
|
validators map[string]CustomTypeValidator
|
||||||
|
|
||||||
|
sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *customTypeTagMap) Get(name string) (CustomTypeValidator, bool) {
|
||||||
|
tm.RLock()
|
||||||
|
defer tm.RUnlock()
|
||||||
|
v, ok := tm.validators[name]
|
||||||
|
return v, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tm *customTypeTagMap) Set(name string, ctv CustomTypeValidator) {
|
||||||
|
tm.Lock()
|
||||||
|
defer tm.Unlock()
|
||||||
|
tm.validators[name] = ctv
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomTypeTagMap is a map of functions that can be used as tags for ValidateStruct function.
|
||||||
|
// Use this to validate compound or custom types that need to be handled as a whole, e.g.
|
||||||
|
// `type UUID [16]byte` (this would be handled as an array of bytes).
|
||||||
|
var CustomTypeTagMap = &customTypeTagMap{validators: make(map[string]CustomTypeValidator)}
|
||||||
|
|
||||||
|
// TagMap is a map of functions, that can be used as tags for ValidateStruct function.
|
||||||
|
var TagMap = map[string]Validator{
|
||||||
|
"email": IsEmail,
|
||||||
|
"url": IsURL,
|
||||||
|
"dialstring": IsDialString,
|
||||||
|
"requrl": IsRequestURL,
|
||||||
|
"requri": IsRequestURI,
|
||||||
|
"alpha": IsAlpha,
|
||||||
|
"utfletter": IsUTFLetter,
|
||||||
|
"alphanum": IsAlphanumeric,
|
||||||
|
"utfletternum": IsUTFLetterNumeric,
|
||||||
|
"numeric": IsNumeric,
|
||||||
|
"utfnumeric": IsUTFNumeric,
|
||||||
|
"utfdigit": IsUTFDigit,
|
||||||
|
"hexadecimal": IsHexadecimal,
|
||||||
|
"hexcolor": IsHexcolor,
|
||||||
|
"rgbcolor": IsRGBcolor,
|
||||||
|
"lowercase": IsLowerCase,
|
||||||
|
"uppercase": IsUpperCase,
|
||||||
|
"int": IsInt,
|
||||||
|
"float": IsFloat,
|
||||||
|
"null": IsNull,
|
||||||
|
"notnull": IsNotNull,
|
||||||
|
"uuid": IsUUID,
|
||||||
|
"uuidv3": IsUUIDv3,
|
||||||
|
"uuidv4": IsUUIDv4,
|
||||||
|
"uuidv5": IsUUIDv5,
|
||||||
|
"creditcard": IsCreditCard,
|
||||||
|
"isbn10": IsISBN10,
|
||||||
|
"isbn13": IsISBN13,
|
||||||
|
"json": IsJSON,
|
||||||
|
"multibyte": IsMultibyte,
|
||||||
|
"ascii": IsASCII,
|
||||||
|
"printableascii": IsPrintableASCII,
|
||||||
|
"fullwidth": IsFullWidth,
|
||||||
|
"halfwidth": IsHalfWidth,
|
||||||
|
"variablewidth": IsVariableWidth,
|
||||||
|
"base64": IsBase64,
|
||||||
|
"datauri": IsDataURI,
|
||||||
|
"ip": IsIP,
|
||||||
|
"port": IsPort,
|
||||||
|
"ipv4": IsIPv4,
|
||||||
|
"ipv6": IsIPv6,
|
||||||
|
"dns": IsDNSName,
|
||||||
|
"host": IsHost,
|
||||||
|
"mac": IsMAC,
|
||||||
|
"latitude": IsLatitude,
|
||||||
|
"longitude": IsLongitude,
|
||||||
|
"ssn": IsSSN,
|
||||||
|
"semver": IsSemver,
|
||||||
|
"rfc3339": IsRFC3339,
|
||||||
|
"rfc3339WithoutZone": IsRFC3339WithoutZone,
|
||||||
|
"ISO3166Alpha2": IsISO3166Alpha2,
|
||||||
|
"ISO3166Alpha3": IsISO3166Alpha3,
|
||||||
|
"ISO4217": IsISO4217,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO3166Entry stores country codes
|
||||||
|
type ISO3166Entry struct {
|
||||||
|
EnglishShortName string
|
||||||
|
FrenchShortName string
|
||||||
|
Alpha2Code string
|
||||||
|
Alpha3Code string
|
||||||
|
Numeric string
|
||||||
|
}
|
||||||
|
|
||||||
|
//ISO3166List based on https://www.iso.org/obp/ui/#search/code/ Code Type "Officially Assigned Codes"
|
||||||
|
var ISO3166List = []ISO3166Entry{
|
||||||
|
{"Afghanistan", "Afghanistan (l')", "AF", "AFG", "004"},
|
||||||
|
{"Albania", "Albanie (l')", "AL", "ALB", "008"},
|
||||||
|
{"Antarctica", "Antarctique (l')", "AQ", "ATA", "010"},
|
||||||
|
{"Algeria", "Algérie (l')", "DZ", "DZA", "012"},
|
||||||
|
{"American Samoa", "Samoa américaines (les)", "AS", "ASM", "016"},
|
||||||
|
{"Andorra", "Andorre (l')", "AD", "AND", "020"},
|
||||||
|
{"Angola", "Angola (l')", "AO", "AGO", "024"},
|
||||||
|
{"Antigua and Barbuda", "Antigua-et-Barbuda", "AG", "ATG", "028"},
|
||||||
|
{"Azerbaijan", "Azerbaïdjan (l')", "AZ", "AZE", "031"},
|
||||||
|
{"Argentina", "Argentine (l')", "AR", "ARG", "032"},
|
||||||
|
{"Australia", "Australie (l')", "AU", "AUS", "036"},
|
||||||
|
{"Austria", "Autriche (l')", "AT", "AUT", "040"},
|
||||||
|
{"Bahamas (the)", "Bahamas (les)", "BS", "BHS", "044"},
|
||||||
|
{"Bahrain", "Bahreïn", "BH", "BHR", "048"},
|
||||||
|
{"Bangladesh", "Bangladesh (le)", "BD", "BGD", "050"},
|
||||||
|
{"Armenia", "Arménie (l')", "AM", "ARM", "051"},
|
||||||
|
{"Barbados", "Barbade (la)", "BB", "BRB", "052"},
|
||||||
|
{"Belgium", "Belgique (la)", "BE", "BEL", "056"},
|
||||||
|
{"Bermuda", "Bermudes (les)", "BM", "BMU", "060"},
|
||||||
|
{"Bhutan", "Bhoutan (le)", "BT", "BTN", "064"},
|
||||||
|
{"Bolivia (Plurinational State of)", "Bolivie (État plurinational de)", "BO", "BOL", "068"},
|
||||||
|
{"Bosnia and Herzegovina", "Bosnie-Herzégovine (la)", "BA", "BIH", "070"},
|
||||||
|
{"Botswana", "Botswana (le)", "BW", "BWA", "072"},
|
||||||
|
{"Bouvet Island", "Bouvet (l'Île)", "BV", "BVT", "074"},
|
||||||
|
{"Brazil", "Brésil (le)", "BR", "BRA", "076"},
|
||||||
|
{"Belize", "Belize (le)", "BZ", "BLZ", "084"},
|
||||||
|
{"British Indian Ocean Territory (the)", "Indien (le Territoire britannique de l'océan)", "IO", "IOT", "086"},
|
||||||
|
{"Solomon Islands", "Salomon (Îles)", "SB", "SLB", "090"},
|
||||||
|
{"Virgin Islands (British)", "Vierges britanniques (les Îles)", "VG", "VGB", "092"},
|
||||||
|
{"Brunei Darussalam", "Brunéi Darussalam (le)", "BN", "BRN", "096"},
|
||||||
|
{"Bulgaria", "Bulgarie (la)", "BG", "BGR", "100"},
|
||||||
|
{"Myanmar", "Myanmar (le)", "MM", "MMR", "104"},
|
||||||
|
{"Burundi", "Burundi (le)", "BI", "BDI", "108"},
|
||||||
|
{"Belarus", "Bélarus (le)", "BY", "BLR", "112"},
|
||||||
|
{"Cambodia", "Cambodge (le)", "KH", "KHM", "116"},
|
||||||
|
{"Cameroon", "Cameroun (le)", "CM", "CMR", "120"},
|
||||||
|
{"Canada", "Canada (le)", "CA", "CAN", "124"},
|
||||||
|
{"Cabo Verde", "Cabo Verde", "CV", "CPV", "132"},
|
||||||
|
{"Cayman Islands (the)", "Caïmans (les Îles)", "KY", "CYM", "136"},
|
||||||
|
{"Central African Republic (the)", "République centrafricaine (la)", "CF", "CAF", "140"},
|
||||||
|
{"Sri Lanka", "Sri Lanka", "LK", "LKA", "144"},
|
||||||
|
{"Chad", "Tchad (le)", "TD", "TCD", "148"},
|
||||||
|
{"Chile", "Chili (le)", "CL", "CHL", "152"},
|
||||||
|
{"China", "Chine (la)", "CN", "CHN", "156"},
|
||||||
|
{"Taiwan (Province of China)", "Taïwan (Province de Chine)", "TW", "TWN", "158"},
|
||||||
|
{"Christmas Island", "Christmas (l'Île)", "CX", "CXR", "162"},
|
||||||
|
{"Cocos (Keeling) Islands (the)", "Cocos (les Îles)/ Keeling (les Îles)", "CC", "CCK", "166"},
|
||||||
|
{"Colombia", "Colombie (la)", "CO", "COL", "170"},
|
||||||
|
{"Comoros (the)", "Comores (les)", "KM", "COM", "174"},
|
||||||
|
{"Mayotte", "Mayotte", "YT", "MYT", "175"},
|
||||||
|
{"Congo (the)", "Congo (le)", "CG", "COG", "178"},
|
||||||
|
{"Congo (the Democratic Republic of the)", "Congo (la République démocratique du)", "CD", "COD", "180"},
|
||||||
|
{"Cook Islands (the)", "Cook (les Îles)", "CK", "COK", "184"},
|
||||||
|
{"Costa Rica", "Costa Rica (le)", "CR", "CRI", "188"},
|
||||||
|
{"Croatia", "Croatie (la)", "HR", "HRV", "191"},
|
||||||
|
{"Cuba", "Cuba", "CU", "CUB", "192"},
|
||||||
|
{"Cyprus", "Chypre", "CY", "CYP", "196"},
|
||||||
|
{"Czech Republic (the)", "tchèque (la République)", "CZ", "CZE", "203"},
|
||||||
|
{"Benin", "Bénin (le)", "BJ", "BEN", "204"},
|
||||||
|
{"Denmark", "Danemark (le)", "DK", "DNK", "208"},
|
||||||
|
{"Dominica", "Dominique (la)", "DM", "DMA", "212"},
|
||||||
|
{"Dominican Republic (the)", "dominicaine (la République)", "DO", "DOM", "214"},
|
||||||
|
{"Ecuador", "Équateur (l')", "EC", "ECU", "218"},
|
||||||
|
{"El Salvador", "El Salvador", "SV", "SLV", "222"},
|
||||||
|
{"Equatorial Guinea", "Guinée équatoriale (la)", "GQ", "GNQ", "226"},
|
||||||
|
{"Ethiopia", "Éthiopie (l')", "ET", "ETH", "231"},
|
||||||
|
{"Eritrea", "Érythrée (l')", "ER", "ERI", "232"},
|
||||||
|
{"Estonia", "Estonie (l')", "EE", "EST", "233"},
|
||||||
|
{"Faroe Islands (the)", "Féroé (les Îles)", "FO", "FRO", "234"},
|
||||||
|
{"Falkland Islands (the) [Malvinas]", "Falkland (les Îles)/Malouines (les Îles)", "FK", "FLK", "238"},
|
||||||
|
{"South Georgia and the South Sandwich Islands", "Géorgie du Sud-et-les Îles Sandwich du Sud (la)", "GS", "SGS", "239"},
|
||||||
|
{"Fiji", "Fidji (les)", "FJ", "FJI", "242"},
|
||||||
|
{"Finland", "Finlande (la)", "FI", "FIN", "246"},
|
||||||
|
{"Åland Islands", "Åland(les Îles)", "AX", "ALA", "248"},
|
||||||
|
{"France", "France (la)", "FR", "FRA", "250"},
|
||||||
|
{"French Guiana", "Guyane française (la )", "GF", "GUF", "254"},
|
||||||
|
{"French Polynesia", "Polynésie française (la)", "PF", "PYF", "258"},
|
||||||
|
{"French Southern Territories (the)", "Terres australes françaises (les)", "TF", "ATF", "260"},
|
||||||
|
{"Djibouti", "Djibouti", "DJ", "DJI", "262"},
|
||||||
|
{"Gabon", "Gabon (le)", "GA", "GAB", "266"},
|
||||||
|
{"Georgia", "Géorgie (la)", "GE", "GEO", "268"},
|
||||||
|
{"Gambia (the)", "Gambie (la)", "GM", "GMB", "270"},
|
||||||
|
{"Palestine, State of", "Palestine, État de", "PS", "PSE", "275"},
|
||||||
|
{"Germany", "Allemagne (l')", "DE", "DEU", "276"},
|
||||||
|
{"Ghana", "Ghana (le)", "GH", "GHA", "288"},
|
||||||
|
{"Gibraltar", "Gibraltar", "GI", "GIB", "292"},
|
||||||
|
{"Kiribati", "Kiribati", "KI", "KIR", "296"},
|
||||||
|
{"Greece", "Grèce (la)", "GR", "GRC", "300"},
|
||||||
|
{"Greenland", "Groenland (le)", "GL", "GRL", "304"},
|
||||||
|
{"Grenada", "Grenade (la)", "GD", "GRD", "308"},
|
||||||
|
{"Guadeloupe", "Guadeloupe (la)", "GP", "GLP", "312"},
|
||||||
|
{"Guam", "Guam", "GU", "GUM", "316"},
|
||||||
|
{"Guatemala", "Guatemala (le)", "GT", "GTM", "320"},
|
||||||
|
{"Guinea", "Guinée (la)", "GN", "GIN", "324"},
|
||||||
|
{"Guyana", "Guyana (le)", "GY", "GUY", "328"},
|
||||||
|
{"Haiti", "Haïti", "HT", "HTI", "332"},
|
||||||
|
{"Heard Island and McDonald Islands", "Heard-et-Îles MacDonald (l'Île)", "HM", "HMD", "334"},
|
||||||
|
{"Holy See (the)", "Saint-Siège (le)", "VA", "VAT", "336"},
|
||||||
|
{"Honduras", "Honduras (le)", "HN", "HND", "340"},
|
||||||
|
{"Hong Kong", "Hong Kong", "HK", "HKG", "344"},
|
||||||
|
{"Hungary", "Hongrie (la)", "HU", "HUN", "348"},
|
||||||
|
{"Iceland", "Islande (l')", "IS", "ISL", "352"},
|
||||||
|
{"India", "Inde (l')", "IN", "IND", "356"},
|
||||||
|
{"Indonesia", "Indonésie (l')", "ID", "IDN", "360"},
|
||||||
|
{"Iran (Islamic Republic of)", "Iran (République Islamique d')", "IR", "IRN", "364"},
|
||||||
|
{"Iraq", "Iraq (l')", "IQ", "IRQ", "368"},
|
||||||
|
{"Ireland", "Irlande (l')", "IE", "IRL", "372"},
|
||||||
|
{"Israel", "Israël", "IL", "ISR", "376"},
|
||||||
|
{"Italy", "Italie (l')", "IT", "ITA", "380"},
|
||||||
|
{"Côte d'Ivoire", "Côte d'Ivoire (la)", "CI", "CIV", "384"},
|
||||||
|
{"Jamaica", "Jamaïque (la)", "JM", "JAM", "388"},
|
||||||
|
{"Japan", "Japon (le)", "JP", "JPN", "392"},
|
||||||
|
{"Kazakhstan", "Kazakhstan (le)", "KZ", "KAZ", "398"},
|
||||||
|
{"Jordan", "Jordanie (la)", "JO", "JOR", "400"},
|
||||||
|
{"Kenya", "Kenya (le)", "KE", "KEN", "404"},
|
||||||
|
{"Korea (the Democratic People's Republic of)", "Corée (la République populaire démocratique de)", "KP", "PRK", "408"},
|
||||||
|
{"Korea (the Republic of)", "Corée (la République de)", "KR", "KOR", "410"},
|
||||||
|
{"Kuwait", "Koweït (le)", "KW", "KWT", "414"},
|
||||||
|
{"Kyrgyzstan", "Kirghizistan (le)", "KG", "KGZ", "417"},
|
||||||
|
{"Lao People's Democratic Republic (the)", "Lao, République démocratique populaire", "LA", "LAO", "418"},
|
||||||
|
{"Lebanon", "Liban (le)", "LB", "LBN", "422"},
|
||||||
|
{"Lesotho", "Lesotho (le)", "LS", "LSO", "426"},
|
||||||
|
{"Latvia", "Lettonie (la)", "LV", "LVA", "428"},
|
||||||
|
{"Liberia", "Libéria (le)", "LR", "LBR", "430"},
|
||||||
|
{"Libya", "Libye (la)", "LY", "LBY", "434"},
|
||||||
|
{"Liechtenstein", "Liechtenstein (le)", "LI", "LIE", "438"},
|
||||||
|
{"Lithuania", "Lituanie (la)", "LT", "LTU", "440"},
|
||||||
|
{"Luxembourg", "Luxembourg (le)", "LU", "LUX", "442"},
|
||||||
|
{"Macao", "Macao", "MO", "MAC", "446"},
|
||||||
|
{"Madagascar", "Madagascar", "MG", "MDG", "450"},
|
||||||
|
{"Malawi", "Malawi (le)", "MW", "MWI", "454"},
|
||||||
|
{"Malaysia", "Malaisie (la)", "MY", "MYS", "458"},
|
||||||
|
{"Maldives", "Maldives (les)", "MV", "MDV", "462"},
|
||||||
|
{"Mali", "Mali (le)", "ML", "MLI", "466"},
|
||||||
|
{"Malta", "Malte", "MT", "MLT", "470"},
|
||||||
|
{"Martinique", "Martinique (la)", "MQ", "MTQ", "474"},
|
||||||
|
{"Mauritania", "Mauritanie (la)", "MR", "MRT", "478"},
|
||||||
|
{"Mauritius", "Maurice", "MU", "MUS", "480"},
|
||||||
|
{"Mexico", "Mexique (le)", "MX", "MEX", "484"},
|
||||||
|
{"Monaco", "Monaco", "MC", "MCO", "492"},
|
||||||
|
{"Mongolia", "Mongolie (la)", "MN", "MNG", "496"},
|
||||||
|
{"Moldova (the Republic of)", "Moldova , République de", "MD", "MDA", "498"},
|
||||||
|
{"Montenegro", "Monténégro (le)", "ME", "MNE", "499"},
|
||||||
|
{"Montserrat", "Montserrat", "MS", "MSR", "500"},
|
||||||
|
{"Morocco", "Maroc (le)", "MA", "MAR", "504"},
|
||||||
|
{"Mozambique", "Mozambique (le)", "MZ", "MOZ", "508"},
|
||||||
|
{"Oman", "Oman", "OM", "OMN", "512"},
|
||||||
|
{"Namibia", "Namibie (la)", "NA", "NAM", "516"},
|
||||||
|
{"Nauru", "Nauru", "NR", "NRU", "520"},
|
||||||
|
{"Nepal", "Népal (le)", "NP", "NPL", "524"},
|
||||||
|
{"Netherlands (the)", "Pays-Bas (les)", "NL", "NLD", "528"},
|
||||||
|
{"Curaçao", "Curaçao", "CW", "CUW", "531"},
|
||||||
|
{"Aruba", "Aruba", "AW", "ABW", "533"},
|
||||||
|
{"Sint Maarten (Dutch part)", "Saint-Martin (partie néerlandaise)", "SX", "SXM", "534"},
|
||||||
|
{"Bonaire, Sint Eustatius and Saba", "Bonaire, Saint-Eustache et Saba", "BQ", "BES", "535"},
|
||||||
|
{"New Caledonia", "Nouvelle-Calédonie (la)", "NC", "NCL", "540"},
|
||||||
|
{"Vanuatu", "Vanuatu (le)", "VU", "VUT", "548"},
|
||||||
|
{"New Zealand", "Nouvelle-Zélande (la)", "NZ", "NZL", "554"},
|
||||||
|
{"Nicaragua", "Nicaragua (le)", "NI", "NIC", "558"},
|
||||||
|
{"Niger (the)", "Niger (le)", "NE", "NER", "562"},
|
||||||
|
{"Nigeria", "Nigéria (le)", "NG", "NGA", "566"},
|
||||||
|
{"Niue", "Niue", "NU", "NIU", "570"},
|
||||||
|
{"Norfolk Island", "Norfolk (l'Île)", "NF", "NFK", "574"},
|
||||||
|
{"Norway", "Norvège (la)", "NO", "NOR", "578"},
|
||||||
|
{"Northern Mariana Islands (the)", "Mariannes du Nord (les Îles)", "MP", "MNP", "580"},
|
||||||
|
{"United States Minor Outlying Islands (the)", "Îles mineures éloignées des États-Unis (les)", "UM", "UMI", "581"},
|
||||||
|
{"Micronesia (Federated States of)", "Micronésie (États fédérés de)", "FM", "FSM", "583"},
|
||||||
|
{"Marshall Islands (the)", "Marshall (Îles)", "MH", "MHL", "584"},
|
||||||
|
{"Palau", "Palaos (les)", "PW", "PLW", "585"},
|
||||||
|
{"Pakistan", "Pakistan (le)", "PK", "PAK", "586"},
|
||||||
|
{"Panama", "Panama (le)", "PA", "PAN", "591"},
|
||||||
|
{"Papua New Guinea", "Papouasie-Nouvelle-Guinée (la)", "PG", "PNG", "598"},
|
||||||
|
{"Paraguay", "Paraguay (le)", "PY", "PRY", "600"},
|
||||||
|
{"Peru", "Pérou (le)", "PE", "PER", "604"},
|
||||||
|
{"Philippines (the)", "Philippines (les)", "PH", "PHL", "608"},
|
||||||
|
{"Pitcairn", "Pitcairn", "PN", "PCN", "612"},
|
||||||
|
{"Poland", "Pologne (la)", "PL", "POL", "616"},
|
||||||
|
{"Portugal", "Portugal (le)", "PT", "PRT", "620"},
|
||||||
|
{"Guinea-Bissau", "Guinée-Bissau (la)", "GW", "GNB", "624"},
|
||||||
|
{"Timor-Leste", "Timor-Leste (le)", "TL", "TLS", "626"},
|
||||||
|
{"Puerto Rico", "Porto Rico", "PR", "PRI", "630"},
|
||||||
|
{"Qatar", "Qatar (le)", "QA", "QAT", "634"},
|
||||||
|
{"Réunion", "Réunion (La)", "RE", "REU", "638"},
|
||||||
|
{"Romania", "Roumanie (la)", "RO", "ROU", "642"},
|
||||||
|
{"Russian Federation (the)", "Russie (la Fédération de)", "RU", "RUS", "643"},
|
||||||
|
{"Rwanda", "Rwanda (le)", "RW", "RWA", "646"},
|
||||||
|
{"Saint Barthélemy", "Saint-Barthélemy", "BL", "BLM", "652"},
|
||||||
|
{"Saint Helena, Ascension and Tristan da Cunha", "Sainte-Hélène, Ascension et Tristan da Cunha", "SH", "SHN", "654"},
|
||||||
|
{"Saint Kitts and Nevis", "Saint-Kitts-et-Nevis", "KN", "KNA", "659"},
|
||||||
|
{"Anguilla", "Anguilla", "AI", "AIA", "660"},
|
||||||
|
{"Saint Lucia", "Sainte-Lucie", "LC", "LCA", "662"},
|
||||||
|
{"Saint Martin (French part)", "Saint-Martin (partie française)", "MF", "MAF", "663"},
|
||||||
|
{"Saint Pierre and Miquelon", "Saint-Pierre-et-Miquelon", "PM", "SPM", "666"},
|
||||||
|
{"Saint Vincent and the Grenadines", "Saint-Vincent-et-les Grenadines", "VC", "VCT", "670"},
|
||||||
|
{"San Marino", "Saint-Marin", "SM", "SMR", "674"},
|
||||||
|
{"Sao Tome and Principe", "Sao Tomé-et-Principe", "ST", "STP", "678"},
|
||||||
|
{"Saudi Arabia", "Arabie saoudite (l')", "SA", "SAU", "682"},
|
||||||
|
{"Senegal", "Sénégal (le)", "SN", "SEN", "686"},
|
||||||
|
{"Serbia", "Serbie (la)", "RS", "SRB", "688"},
|
||||||
|
{"Seychelles", "Seychelles (les)", "SC", "SYC", "690"},
|
||||||
|
{"Sierra Leone", "Sierra Leone (la)", "SL", "SLE", "694"},
|
||||||
|
{"Singapore", "Singapour", "SG", "SGP", "702"},
|
||||||
|
{"Slovakia", "Slovaquie (la)", "SK", "SVK", "703"},
|
||||||
|
{"Viet Nam", "Viet Nam (le)", "VN", "VNM", "704"},
|
||||||
|
{"Slovenia", "Slovénie (la)", "SI", "SVN", "705"},
|
||||||
|
{"Somalia", "Somalie (la)", "SO", "SOM", "706"},
|
||||||
|
{"South Africa", "Afrique du Sud (l')", "ZA", "ZAF", "710"},
|
||||||
|
{"Zimbabwe", "Zimbabwe (le)", "ZW", "ZWE", "716"},
|
||||||
|
{"Spain", "Espagne (l')", "ES", "ESP", "724"},
|
||||||
|
{"South Sudan", "Soudan du Sud (le)", "SS", "SSD", "728"},
|
||||||
|
{"Sudan (the)", "Soudan (le)", "SD", "SDN", "729"},
|
||||||
|
{"Western Sahara*", "Sahara occidental (le)*", "EH", "ESH", "732"},
|
||||||
|
{"Suriname", "Suriname (le)", "SR", "SUR", "740"},
|
||||||
|
{"Svalbard and Jan Mayen", "Svalbard et l'Île Jan Mayen (le)", "SJ", "SJM", "744"},
|
||||||
|
{"Swaziland", "Swaziland (le)", "SZ", "SWZ", "748"},
|
||||||
|
{"Sweden", "Suède (la)", "SE", "SWE", "752"},
|
||||||
|
{"Switzerland", "Suisse (la)", "CH", "CHE", "756"},
|
||||||
|
{"Syrian Arab Republic", "République arabe syrienne (la)", "SY", "SYR", "760"},
|
||||||
|
{"Tajikistan", "Tadjikistan (le)", "TJ", "TJK", "762"},
|
||||||
|
{"Thailand", "Thaïlande (la)", "TH", "THA", "764"},
|
||||||
|
{"Togo", "Togo (le)", "TG", "TGO", "768"},
|
||||||
|
{"Tokelau", "Tokelau (les)", "TK", "TKL", "772"},
|
||||||
|
{"Tonga", "Tonga (les)", "TO", "TON", "776"},
|
||||||
|
{"Trinidad and Tobago", "Trinité-et-Tobago (la)", "TT", "TTO", "780"},
|
||||||
|
{"United Arab Emirates (the)", "Émirats arabes unis (les)", "AE", "ARE", "784"},
|
||||||
|
{"Tunisia", "Tunisie (la)", "TN", "TUN", "788"},
|
||||||
|
{"Turkey", "Turquie (la)", "TR", "TUR", "792"},
|
||||||
|
{"Turkmenistan", "Turkménistan (le)", "TM", "TKM", "795"},
|
||||||
|
{"Turks and Caicos Islands (the)", "Turks-et-Caïcos (les Îles)", "TC", "TCA", "796"},
|
||||||
|
{"Tuvalu", "Tuvalu (les)", "TV", "TUV", "798"},
|
||||||
|
{"Uganda", "Ouganda (l')", "UG", "UGA", "800"},
|
||||||
|
{"Ukraine", "Ukraine (l')", "UA", "UKR", "804"},
|
||||||
|
{"Macedonia (the former Yugoslav Republic of)", "Macédoine (l'ex‑République yougoslave de)", "MK", "MKD", "807"},
|
||||||
|
{"Egypt", "Égypte (l')", "EG", "EGY", "818"},
|
||||||
|
{"United Kingdom of Great Britain and Northern Ireland (the)", "Royaume-Uni de Grande-Bretagne et d'Irlande du Nord (le)", "GB", "GBR", "826"},
|
||||||
|
{"Guernsey", "Guernesey", "GG", "GGY", "831"},
|
||||||
|
{"Jersey", "Jersey", "JE", "JEY", "832"},
|
||||||
|
{"Isle of Man", "Île de Man", "IM", "IMN", "833"},
|
||||||
|
{"Tanzania, United Republic of", "Tanzanie, République-Unie de", "TZ", "TZA", "834"},
|
||||||
|
{"United States of America (the)", "États-Unis d'Amérique (les)", "US", "USA", "840"},
|
||||||
|
{"Virgin Islands (U.S.)", "Vierges des États-Unis (les Îles)", "VI", "VIR", "850"},
|
||||||
|
{"Burkina Faso", "Burkina Faso (le)", "BF", "BFA", "854"},
|
||||||
|
{"Uruguay", "Uruguay (l')", "UY", "URY", "858"},
|
||||||
|
{"Uzbekistan", "Ouzbékistan (l')", "UZ", "UZB", "860"},
|
||||||
|
{"Venezuela (Bolivarian Republic of)", "Venezuela (République bolivarienne du)", "VE", "VEN", "862"},
|
||||||
|
{"Wallis and Futuna", "Wallis-et-Futuna", "WF", "WLF", "876"},
|
||||||
|
{"Samoa", "Samoa (le)", "WS", "WSM", "882"},
|
||||||
|
{"Yemen", "Yémen (le)", "YE", "YEM", "887"},
|
||||||
|
{"Zambia", "Zambie (la)", "ZM", "ZMB", "894"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO4217List is the list of ISO currency codes
|
||||||
|
var ISO4217List = []string{
|
||||||
|
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN",
|
||||||
|
"BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BOV", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD",
|
||||||
|
"CAD", "CDF", "CHE", "CHF", "CHW", "CLF", "CLP", "CNY", "COP", "COU", "CRC", "CUC", "CUP", "CVE", "CZK",
|
||||||
|
"DJF", "DKK", "DOP", "DZD",
|
||||||
|
"EGP", "ERN", "ETB", "EUR",
|
||||||
|
"FJD", "FKP",
|
||||||
|
"GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD",
|
||||||
|
"HKD", "HNL", "HRK", "HTG", "HUF",
|
||||||
|
"IDR", "ILS", "INR", "IQD", "IRR", "ISK",
|
||||||
|
"JMD", "JOD", "JPY",
|
||||||
|
"KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT",
|
||||||
|
"LAK", "LBP", "LKR", "LRD", "LSL", "LYD",
|
||||||
|
"MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRO", "MUR", "MVR", "MWK", "MXN", "MXV", "MYR", "MZN",
|
||||||
|
"NAD", "NGN", "NIO", "NOK", "NPR", "NZD",
|
||||||
|
"OMR",
|
||||||
|
"PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG",
|
||||||
|
"QAR",
|
||||||
|
"RON", "RSD", "RUB", "RWF",
|
||||||
|
"SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SRD", "SSP", "STD", "SVC", "SYP", "SZL",
|
||||||
|
"THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS",
|
||||||
|
"UAH", "UGX", "USD", "USN", "UYI", "UYU", "UZS",
|
||||||
|
"VEF", "VND", "VUV",
|
||||||
|
"WST",
|
||||||
|
"XAF", "XAG", "XAU", "XBA", "XBB", "XBC", "XBD", "XCD", "XDR", "XOF", "XPD", "XPF", "XPT", "XSU", "XTS", "XUA", "XXX",
|
||||||
|
"YER",
|
||||||
|
"ZAR", "ZMW", "ZWL",
|
||||||
|
}
|
||||||
|
|
||||||
|
// ISO693Entry stores ISO language codes
|
||||||
|
type ISO693Entry struct {
|
||||||
|
Alpha3bCode string
|
||||||
|
Alpha2Code string
|
||||||
|
English string
|
||||||
|
}
|
||||||
|
|
||||||
|
//ISO693List based on http://data.okfn.org/data/core/language-codes/r/language-codes-3b2.json
|
||||||
|
var ISO693List = []ISO693Entry{
|
||||||
|
{Alpha3bCode: "aar", Alpha2Code: "aa", English: "Afar"},
|
||||||
|
{Alpha3bCode: "abk", Alpha2Code: "ab", English: "Abkhazian"},
|
||||||
|
{Alpha3bCode: "afr", Alpha2Code: "af", English: "Afrikaans"},
|
||||||
|
{Alpha3bCode: "aka", Alpha2Code: "ak", English: "Akan"},
|
||||||
|
{Alpha3bCode: "alb", Alpha2Code: "sq", English: "Albanian"},
|
||||||
|
{Alpha3bCode: "amh", Alpha2Code: "am", English: "Amharic"},
|
||||||
|
{Alpha3bCode: "ara", Alpha2Code: "ar", English: "Arabic"},
|
||||||
|
{Alpha3bCode: "arg", Alpha2Code: "an", English: "Aragonese"},
|
||||||
|
{Alpha3bCode: "arm", Alpha2Code: "hy", English: "Armenian"},
|
||||||
|
{Alpha3bCode: "asm", Alpha2Code: "as", English: "Assamese"},
|
||||||
|
{Alpha3bCode: "ava", Alpha2Code: "av", English: "Avaric"},
|
||||||
|
{Alpha3bCode: "ave", Alpha2Code: "ae", English: "Avestan"},
|
||||||
|
{Alpha3bCode: "aym", Alpha2Code: "ay", English: "Aymara"},
|
||||||
|
{Alpha3bCode: "aze", Alpha2Code: "az", English: "Azerbaijani"},
|
||||||
|
{Alpha3bCode: "bak", Alpha2Code: "ba", English: "Bashkir"},
|
||||||
|
{Alpha3bCode: "bam", Alpha2Code: "bm", English: "Bambara"},
|
||||||
|
{Alpha3bCode: "baq", Alpha2Code: "eu", English: "Basque"},
|
||||||
|
{Alpha3bCode: "bel", Alpha2Code: "be", English: "Belarusian"},
|
||||||
|
{Alpha3bCode: "ben", Alpha2Code: "bn", English: "Bengali"},
|
||||||
|
{Alpha3bCode: "bih", Alpha2Code: "bh", English: "Bihari languages"},
|
||||||
|
{Alpha3bCode: "bis", Alpha2Code: "bi", English: "Bislama"},
|
||||||
|
{Alpha3bCode: "bos", Alpha2Code: "bs", English: "Bosnian"},
|
||||||
|
{Alpha3bCode: "bre", Alpha2Code: "br", English: "Breton"},
|
||||||
|
{Alpha3bCode: "bul", Alpha2Code: "bg", English: "Bulgarian"},
|
||||||
|
{Alpha3bCode: "bur", Alpha2Code: "my", English: "Burmese"},
|
||||||
|
{Alpha3bCode: "cat", Alpha2Code: "ca", English: "Catalan; Valencian"},
|
||||||
|
{Alpha3bCode: "cha", Alpha2Code: "ch", English: "Chamorro"},
|
||||||
|
{Alpha3bCode: "che", Alpha2Code: "ce", English: "Chechen"},
|
||||||
|
{Alpha3bCode: "chi", Alpha2Code: "zh", English: "Chinese"},
|
||||||
|
{Alpha3bCode: "chu", Alpha2Code: "cu", English: "Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic"},
|
||||||
|
{Alpha3bCode: "chv", Alpha2Code: "cv", English: "Chuvash"},
|
||||||
|
{Alpha3bCode: "cor", Alpha2Code: "kw", English: "Cornish"},
|
||||||
|
{Alpha3bCode: "cos", Alpha2Code: "co", English: "Corsican"},
|
||||||
|
{Alpha3bCode: "cre", Alpha2Code: "cr", English: "Cree"},
|
||||||
|
{Alpha3bCode: "cze", Alpha2Code: "cs", English: "Czech"},
|
||||||
|
{Alpha3bCode: "dan", Alpha2Code: "da", English: "Danish"},
|
||||||
|
{Alpha3bCode: "div", Alpha2Code: "dv", English: "Divehi; Dhivehi; Maldivian"},
|
||||||
|
{Alpha3bCode: "dut", Alpha2Code: "nl", English: "Dutch; Flemish"},
|
||||||
|
{Alpha3bCode: "dzo", Alpha2Code: "dz", English: "Dzongkha"},
|
||||||
|
{Alpha3bCode: "eng", Alpha2Code: "en", English: "English"},
|
||||||
|
{Alpha3bCode: "epo", Alpha2Code: "eo", English: "Esperanto"},
|
||||||
|
{Alpha3bCode: "est", Alpha2Code: "et", English: "Estonian"},
|
||||||
|
{Alpha3bCode: "ewe", Alpha2Code: "ee", English: "Ewe"},
|
||||||
|
{Alpha3bCode: "fao", Alpha2Code: "fo", English: "Faroese"},
|
||||||
|
{Alpha3bCode: "fij", Alpha2Code: "fj", English: "Fijian"},
|
||||||
|
{Alpha3bCode: "fin", Alpha2Code: "fi", English: "Finnish"},
|
||||||
|
{Alpha3bCode: "fre", Alpha2Code: "fr", English: "French"},
|
||||||
|
{Alpha3bCode: "fry", Alpha2Code: "fy", English: "Western Frisian"},
|
||||||
|
{Alpha3bCode: "ful", Alpha2Code: "ff", English: "Fulah"},
|
||||||
|
{Alpha3bCode: "geo", Alpha2Code: "ka", English: "Georgian"},
|
||||||
|
{Alpha3bCode: "ger", Alpha2Code: "de", English: "German"},
|
||||||
|
{Alpha3bCode: "gla", Alpha2Code: "gd", English: "Gaelic; Scottish Gaelic"},
|
||||||
|
{Alpha3bCode: "gle", Alpha2Code: "ga", English: "Irish"},
|
||||||
|
{Alpha3bCode: "glg", Alpha2Code: "gl", English: "Galician"},
|
||||||
|
{Alpha3bCode: "glv", Alpha2Code: "gv", English: "Manx"},
|
||||||
|
{Alpha3bCode: "gre", Alpha2Code: "el", English: "Greek, Modern (1453-)"},
|
||||||
|
{Alpha3bCode: "grn", Alpha2Code: "gn", English: "Guarani"},
|
||||||
|
{Alpha3bCode: "guj", Alpha2Code: "gu", English: "Gujarati"},
|
||||||
|
{Alpha3bCode: "hat", Alpha2Code: "ht", English: "Haitian; Haitian Creole"},
|
||||||
|
{Alpha3bCode: "hau", Alpha2Code: "ha", English: "Hausa"},
|
||||||
|
{Alpha3bCode: "heb", Alpha2Code: "he", English: "Hebrew"},
|
||||||
|
{Alpha3bCode: "her", Alpha2Code: "hz", English: "Herero"},
|
||||||
|
{Alpha3bCode: "hin", Alpha2Code: "hi", English: "Hindi"},
|
||||||
|
{Alpha3bCode: "hmo", Alpha2Code: "ho", English: "Hiri Motu"},
|
||||||
|
{Alpha3bCode: "hrv", Alpha2Code: "hr", English: "Croatian"},
|
||||||
|
{Alpha3bCode: "hun", Alpha2Code: "hu", English: "Hungarian"},
|
||||||
|
{Alpha3bCode: "ibo", Alpha2Code: "ig", English: "Igbo"},
|
||||||
|
{Alpha3bCode: "ice", Alpha2Code: "is", English: "Icelandic"},
|
||||||
|
{Alpha3bCode: "ido", Alpha2Code: "io", English: "Ido"},
|
||||||
|
{Alpha3bCode: "iii", Alpha2Code: "ii", English: "Sichuan Yi; Nuosu"},
|
||||||
|
{Alpha3bCode: "iku", Alpha2Code: "iu", English: "Inuktitut"},
|
||||||
|
{Alpha3bCode: "ile", Alpha2Code: "ie", English: "Interlingue; Occidental"},
|
||||||
|
{Alpha3bCode: "ina", Alpha2Code: "ia", English: "Interlingua (International Auxiliary Language Association)"},
|
||||||
|
{Alpha3bCode: "ind", Alpha2Code: "id", English: "Indonesian"},
|
||||||
|
{Alpha3bCode: "ipk", Alpha2Code: "ik", English: "Inupiaq"},
|
||||||
|
{Alpha3bCode: "ita", Alpha2Code: "it", English: "Italian"},
|
||||||
|
{Alpha3bCode: "jav", Alpha2Code: "jv", English: "Javanese"},
|
||||||
|
{Alpha3bCode: "jpn", Alpha2Code: "ja", English: "Japanese"},
|
||||||
|
{Alpha3bCode: "kal", Alpha2Code: "kl", English: "Kalaallisut; Greenlandic"},
|
||||||
|
{Alpha3bCode: "kan", Alpha2Code: "kn", English: "Kannada"},
|
||||||
|
{Alpha3bCode: "kas", Alpha2Code: "ks", English: "Kashmiri"},
|
||||||
|
{Alpha3bCode: "kau", Alpha2Code: "kr", English: "Kanuri"},
|
||||||
|
{Alpha3bCode: "kaz", Alpha2Code: "kk", English: "Kazakh"},
|
||||||
|
{Alpha3bCode: "khm", Alpha2Code: "km", English: "Central Khmer"},
|
||||||
|
{Alpha3bCode: "kik", Alpha2Code: "ki", English: "Kikuyu; Gikuyu"},
|
||||||
|
{Alpha3bCode: "kin", Alpha2Code: "rw", English: "Kinyarwanda"},
|
||||||
|
{Alpha3bCode: "kir", Alpha2Code: "ky", English: "Kirghiz; Kyrgyz"},
|
||||||
|
{Alpha3bCode: "kom", Alpha2Code: "kv", English: "Komi"},
|
||||||
|
{Alpha3bCode: "kon", Alpha2Code: "kg", English: "Kongo"},
|
||||||
|
{Alpha3bCode: "kor", Alpha2Code: "ko", English: "Korean"},
|
||||||
|
{Alpha3bCode: "kua", Alpha2Code: "kj", English: "Kuanyama; Kwanyama"},
|
||||||
|
{Alpha3bCode: "kur", Alpha2Code: "ku", English: "Kurdish"},
|
||||||
|
{Alpha3bCode: "lao", Alpha2Code: "lo", English: "Lao"},
|
||||||
|
{Alpha3bCode: "lat", Alpha2Code: "la", English: "Latin"},
|
||||||
|
{Alpha3bCode: "lav", Alpha2Code: "lv", English: "Latvian"},
|
||||||
|
{Alpha3bCode: "lim", Alpha2Code: "li", English: "Limburgan; Limburger; Limburgish"},
|
||||||
|
{Alpha3bCode: "lin", Alpha2Code: "ln", English: "Lingala"},
|
||||||
|
{Alpha3bCode: "lit", Alpha2Code: "lt", English: "Lithuanian"},
|
||||||
|
{Alpha3bCode: "ltz", Alpha2Code: "lb", English: "Luxembourgish; Letzeburgesch"},
|
||||||
|
{Alpha3bCode: "lub", Alpha2Code: "lu", English: "Luba-Katanga"},
|
||||||
|
{Alpha3bCode: "lug", Alpha2Code: "lg", English: "Ganda"},
|
||||||
|
{Alpha3bCode: "mac", Alpha2Code: "mk", English: "Macedonian"},
|
||||||
|
{Alpha3bCode: "mah", Alpha2Code: "mh", English: "Marshallese"},
|
||||||
|
{Alpha3bCode: "mal", Alpha2Code: "ml", English: "Malayalam"},
|
||||||
|
{Alpha3bCode: "mao", Alpha2Code: "mi", English: "Maori"},
|
||||||
|
{Alpha3bCode: "mar", Alpha2Code: "mr", English: "Marathi"},
|
||||||
|
{Alpha3bCode: "may", Alpha2Code: "ms", English: "Malay"},
|
||||||
|
{Alpha3bCode: "mlg", Alpha2Code: "mg", English: "Malagasy"},
|
||||||
|
{Alpha3bCode: "mlt", Alpha2Code: "mt", English: "Maltese"},
|
||||||
|
{Alpha3bCode: "mon", Alpha2Code: "mn", English: "Mongolian"},
|
||||||
|
{Alpha3bCode: "nau", Alpha2Code: "na", English: "Nauru"},
|
||||||
|
{Alpha3bCode: "nav", Alpha2Code: "nv", English: "Navajo; Navaho"},
|
||||||
|
{Alpha3bCode: "nbl", Alpha2Code: "nr", English: "Ndebele, South; South Ndebele"},
|
||||||
|
{Alpha3bCode: "nde", Alpha2Code: "nd", English: "Ndebele, North; North Ndebele"},
|
||||||
|
{Alpha3bCode: "ndo", Alpha2Code: "ng", English: "Ndonga"},
|
||||||
|
{Alpha3bCode: "nep", Alpha2Code: "ne", English: "Nepali"},
|
||||||
|
{Alpha3bCode: "nno", Alpha2Code: "nn", English: "Norwegian Nynorsk; Nynorsk, Norwegian"},
|
||||||
|
{Alpha3bCode: "nob", Alpha2Code: "nb", English: "Bokmål, Norwegian; Norwegian Bokmål"},
|
||||||
|
{Alpha3bCode: "nor", Alpha2Code: "no", English: "Norwegian"},
|
||||||
|
{Alpha3bCode: "nya", Alpha2Code: "ny", English: "Chichewa; Chewa; Nyanja"},
|
||||||
|
{Alpha3bCode: "oci", Alpha2Code: "oc", English: "Occitan (post 1500); Provençal"},
|
||||||
|
{Alpha3bCode: "oji", Alpha2Code: "oj", English: "Ojibwa"},
|
||||||
|
{Alpha3bCode: "ori", Alpha2Code: "or", English: "Oriya"},
|
||||||
|
{Alpha3bCode: "orm", Alpha2Code: "om", English: "Oromo"},
|
||||||
|
{Alpha3bCode: "oss", Alpha2Code: "os", English: "Ossetian; Ossetic"},
|
||||||
|
{Alpha3bCode: "pan", Alpha2Code: "pa", English: "Panjabi; Punjabi"},
|
||||||
|
{Alpha3bCode: "per", Alpha2Code: "fa", English: "Persian"},
|
||||||
|
{Alpha3bCode: "pli", Alpha2Code: "pi", English: "Pali"},
|
||||||
|
{Alpha3bCode: "pol", Alpha2Code: "pl", English: "Polish"},
|
||||||
|
{Alpha3bCode: "por", Alpha2Code: "pt", English: "Portuguese"},
|
||||||
|
{Alpha3bCode: "pus", Alpha2Code: "ps", English: "Pushto; Pashto"},
|
||||||
|
{Alpha3bCode: "que", Alpha2Code: "qu", English: "Quechua"},
|
||||||
|
{Alpha3bCode: "roh", Alpha2Code: "rm", English: "Romansh"},
|
||||||
|
{Alpha3bCode: "rum", Alpha2Code: "ro", English: "Romanian; Moldavian; Moldovan"},
|
||||||
|
{Alpha3bCode: "run", Alpha2Code: "rn", English: "Rundi"},
|
||||||
|
{Alpha3bCode: "rus", Alpha2Code: "ru", English: "Russian"},
|
||||||
|
{Alpha3bCode: "sag", Alpha2Code: "sg", English: "Sango"},
|
||||||
|
{Alpha3bCode: "san", Alpha2Code: "sa", English: "Sanskrit"},
|
||||||
|
{Alpha3bCode: "sin", Alpha2Code: "si", English: "Sinhala; Sinhalese"},
|
||||||
|
{Alpha3bCode: "slo", Alpha2Code: "sk", English: "Slovak"},
|
||||||
|
{Alpha3bCode: "slv", Alpha2Code: "sl", English: "Slovenian"},
|
||||||
|
{Alpha3bCode: "sme", Alpha2Code: "se", English: "Northern Sami"},
|
||||||
|
{Alpha3bCode: "smo", Alpha2Code: "sm", English: "Samoan"},
|
||||||
|
{Alpha3bCode: "sna", Alpha2Code: "sn", English: "Shona"},
|
||||||
|
{Alpha3bCode: "snd", Alpha2Code: "sd", English: "Sindhi"},
|
||||||
|
{Alpha3bCode: "som", Alpha2Code: "so", English: "Somali"},
|
||||||
|
{Alpha3bCode: "sot", Alpha2Code: "st", English: "Sotho, Southern"},
|
||||||
|
{Alpha3bCode: "spa", Alpha2Code: "es", English: "Spanish; Castilian"},
|
||||||
|
{Alpha3bCode: "srd", Alpha2Code: "sc", English: "Sardinian"},
|
||||||
|
{Alpha3bCode: "srp", Alpha2Code: "sr", English: "Serbian"},
|
||||||
|
{Alpha3bCode: "ssw", Alpha2Code: "ss", English: "Swati"},
|
||||||
|
{Alpha3bCode: "sun", Alpha2Code: "su", English: "Sundanese"},
|
||||||
|
{Alpha3bCode: "swa", Alpha2Code: "sw", English: "Swahili"},
|
||||||
|
{Alpha3bCode: "swe", Alpha2Code: "sv", English: "Swedish"},
|
||||||
|
{Alpha3bCode: "tah", Alpha2Code: "ty", English: "Tahitian"},
|
||||||
|
{Alpha3bCode: "tam", Alpha2Code: "ta", English: "Tamil"},
|
||||||
|
{Alpha3bCode: "tat", Alpha2Code: "tt", English: "Tatar"},
|
||||||
|
{Alpha3bCode: "tel", Alpha2Code: "te", English: "Telugu"},
|
||||||
|
{Alpha3bCode: "tgk", Alpha2Code: "tg", English: "Tajik"},
|
||||||
|
{Alpha3bCode: "tgl", Alpha2Code: "tl", English: "Tagalog"},
|
||||||
|
{Alpha3bCode: "tha", Alpha2Code: "th", English: "Thai"},
|
||||||
|
{Alpha3bCode: "tib", Alpha2Code: "bo", English: "Tibetan"},
|
||||||
|
{Alpha3bCode: "tir", Alpha2Code: "ti", English: "Tigrinya"},
|
||||||
|
{Alpha3bCode: "ton", Alpha2Code: "to", English: "Tonga (Tonga Islands)"},
|
||||||
|
{Alpha3bCode: "tsn", Alpha2Code: "tn", English: "Tswana"},
|
||||||
|
{Alpha3bCode: "tso", Alpha2Code: "ts", English: "Tsonga"},
|
||||||
|
{Alpha3bCode: "tuk", Alpha2Code: "tk", English: "Turkmen"},
|
||||||
|
{Alpha3bCode: "tur", Alpha2Code: "tr", English: "Turkish"},
|
||||||
|
{Alpha3bCode: "twi", Alpha2Code: "tw", English: "Twi"},
|
||||||
|
{Alpha3bCode: "uig", Alpha2Code: "ug", English: "Uighur; Uyghur"},
|
||||||
|
{Alpha3bCode: "ukr", Alpha2Code: "uk", English: "Ukrainian"},
|
||||||
|
{Alpha3bCode: "urd", Alpha2Code: "ur", English: "Urdu"},
|
||||||
|
{Alpha3bCode: "uzb", Alpha2Code: "uz", English: "Uzbek"},
|
||||||
|
{Alpha3bCode: "ven", Alpha2Code: "ve", English: "Venda"},
|
||||||
|
{Alpha3bCode: "vie", Alpha2Code: "vi", English: "Vietnamese"},
|
||||||
|
{Alpha3bCode: "vol", Alpha2Code: "vo", English: "Volapük"},
|
||||||
|
{Alpha3bCode: "wel", Alpha2Code: "cy", English: "Welsh"},
|
||||||
|
{Alpha3bCode: "wln", Alpha2Code: "wa", English: "Walloon"},
|
||||||
|
{Alpha3bCode: "wol", Alpha2Code: "wo", English: "Wolof"},
|
||||||
|
{Alpha3bCode: "xho", Alpha2Code: "xh", English: "Xhosa"},
|
||||||
|
{Alpha3bCode: "yid", Alpha2Code: "yi", English: "Yiddish"},
|
||||||
|
{Alpha3bCode: "yor", Alpha2Code: "yo", English: "Yoruba"},
|
||||||
|
{Alpha3bCode: "zha", Alpha2Code: "za", English: "Zhuang; Chuang"},
|
||||||
|
{Alpha3bCode: "zul", Alpha2Code: "zu", English: "Zulu"},
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
package govalidator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"math"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contains checks if the string contains the substring.
|
||||||
|
func Contains(str, substring string) bool {
|
||||||
|
return strings.Contains(str, substring)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matches checks if string matches the pattern (pattern is regular expression)
|
||||||
|
// In case of error return false
|
||||||
|
func Matches(str, pattern string) bool {
|
||||||
|
match, _ := regexp.MatchString(pattern, str)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeftTrim trims characters from the left side of the input.
|
||||||
|
// If second argument is empty, it will remove leading spaces.
|
||||||
|
func LeftTrim(str, chars string) string {
|
||||||
|
if chars == "" {
|
||||||
|
return strings.TrimLeftFunc(str, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
r, _ := regexp.Compile("^[" + chars + "]+")
|
||||||
|
return r.ReplaceAllString(str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RightTrim trims characters from the right side of the input.
|
||||||
|
// If second argument is empty, it will remove trailing spaces.
|
||||||
|
func RightTrim(str, chars string) string {
|
||||||
|
if chars == "" {
|
||||||
|
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||||
|
}
|
||||||
|
r, _ := regexp.Compile("[" + chars + "]+$")
|
||||||
|
return r.ReplaceAllString(str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim trims characters from both sides of the input.
|
||||||
|
// If second argument is empty, it will remove spaces.
|
||||||
|
func Trim(str, chars string) string {
|
||||||
|
return LeftTrim(RightTrim(str, chars), chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WhiteList removes characters that do not appear in the whitelist.
|
||||||
|
func WhiteList(str, chars string) string {
|
||||||
|
pattern := "[^" + chars + "]+"
|
||||||
|
r, _ := regexp.Compile(pattern)
|
||||||
|
return r.ReplaceAllString(str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlackList removes characters that appear in the blacklist.
|
||||||
|
func BlackList(str, chars string) string {
|
||||||
|
pattern := "[" + chars + "]+"
|
||||||
|
r, _ := regexp.Compile(pattern)
|
||||||
|
return r.ReplaceAllString(str, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StripLow removes characters with a numerical value < 32 and 127, mostly control characters.
|
||||||
|
// If keep_new_lines is true, newline characters are preserved (\n and \r, hex 0xA and 0xD).
|
||||||
|
func StripLow(str string, keepNewLines bool) string {
|
||||||
|
chars := ""
|
||||||
|
if keepNewLines {
|
||||||
|
chars = "\x00-\x09\x0B\x0C\x0E-\x1F\x7F"
|
||||||
|
} else {
|
||||||
|
chars = "\x00-\x1F\x7F"
|
||||||
|
}
|
||||||
|
return BlackList(str, chars)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplacePattern replaces regular expression pattern in string
|
||||||
|
func ReplacePattern(str, pattern, replace string) string {
|
||||||
|
r, _ := regexp.Compile(pattern)
|
||||||
|
return r.ReplaceAllString(str, replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape replaces <, >, & and " with HTML entities.
|
||||||
|
var Escape = html.EscapeString
|
||||||
|
|
||||||
|
func addSegment(inrune, segment []rune) []rune {
|
||||||
|
if len(segment) == 0 {
|
||||||
|
return inrune
|
||||||
|
}
|
||||||
|
if len(inrune) != 0 {
|
||||||
|
inrune = append(inrune, '_')
|
||||||
|
}
|
||||||
|
inrune = append(inrune, segment...)
|
||||||
|
return inrune
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnderscoreToCamelCase converts from underscore separated form to camel case form.
|
||||||
|
// Ex.: my_func => MyFunc
|
||||||
|
func UnderscoreToCamelCase(s string) string {
|
||||||
|
return strings.Replace(strings.Title(strings.Replace(strings.ToLower(s), "_", " ", -1)), " ", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CamelCaseToUnderscore converts from camel case form to underscore separated form.
|
||||||
|
// Ex.: MyFunc => my_func
|
||||||
|
func CamelCaseToUnderscore(str string) string {
|
||||||
|
var output []rune
|
||||||
|
var segment []rune
|
||||||
|
for _, r := range str {
|
||||||
|
|
||||||
|
// not treat number as separate segment
|
||||||
|
if !unicode.IsLower(r) && string(r) != "_" && !unicode.IsNumber(r) {
|
||||||
|
output = addSegment(output, segment)
|
||||||
|
segment = nil
|
||||||
|
}
|
||||||
|
segment = append(segment, unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
output = addSegment(output, segment)
|
||||||
|
return string(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse returns reversed string
|
||||||
|
func Reverse(s string) string {
|
||||||
|
r := []rune(s)
|
||||||
|
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
|
||||||
|
r[i], r[j] = r[j], r[i]
|
||||||
|
}
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLines splits string by "\n" and return array of lines
|
||||||
|
func GetLines(s string) []string {
|
||||||
|
return strings.Split(s, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLine returns specified line of multiline string
|
||||||
|
func GetLine(s string, index int) (string, error) {
|
||||||
|
lines := GetLines(s)
|
||||||
|
if index < 0 || index >= len(lines) {
|
||||||
|
return "", errors.New("line index out of bounds")
|
||||||
|
}
|
||||||
|
return lines[index], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveTags removes all tags from HTML string
|
||||||
|
func RemoveTags(s string) string {
|
||||||
|
return ReplacePattern(s, "<[^>]*>", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SafeFileName returns safe string that can be used in file names
|
||||||
|
func SafeFileName(str string) string {
|
||||||
|
name := strings.ToLower(str)
|
||||||
|
name = path.Clean(path.Base(name))
|
||||||
|
name = strings.Trim(name, " ")
|
||||||
|
separators, err := regexp.Compile(`[ &_=+:]`)
|
||||||
|
if err == nil {
|
||||||
|
name = separators.ReplaceAllString(name, "-")
|
||||||
|
}
|
||||||
|
legal, err := regexp.Compile(`[^[:alnum:]-.]`)
|
||||||
|
if err == nil {
|
||||||
|
name = legal.ReplaceAllString(name, "")
|
||||||
|
}
|
||||||
|
for strings.Contains(name, "--") {
|
||||||
|
name = strings.Replace(name, "--", "-", -1)
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeEmail canonicalize an email address.
|
||||||
|
// The local part of the email address is lowercased for all domains; the hostname is always lowercased and
|
||||||
|
// the local part of the email address is always lowercased for hosts that are known to be case-insensitive (currently only GMail).
|
||||||
|
// Normalization follows special rules for known providers: currently, GMail addresses have dots removed in the local part and
|
||||||
|
// are stripped of tags (e.g. some.one+tag@gmail.com becomes someone@gmail.com) and all @googlemail.com addresses are
|
||||||
|
// normalized to @gmail.com.
|
||||||
|
func NormalizeEmail(str string) (string, error) {
|
||||||
|
if !IsEmail(str) {
|
||||||
|
return "", fmt.Errorf("%s is not an email", str)
|
||||||
|
}
|
||||||
|
parts := strings.Split(str, "@")
|
||||||
|
parts[0] = strings.ToLower(parts[0])
|
||||||
|
parts[1] = strings.ToLower(parts[1])
|
||||||
|
if parts[1] == "gmail.com" || parts[1] == "googlemail.com" {
|
||||||
|
parts[1] = "gmail.com"
|
||||||
|
parts[0] = strings.Split(ReplacePattern(parts[0], `\.`, ""), "+")[0]
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "@"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate a string to the closest length without breaking words.
|
||||||
|
func Truncate(str string, length int, ending string) string {
|
||||||
|
var aftstr, befstr string
|
||||||
|
if len(str) > length {
|
||||||
|
words := strings.Fields(str)
|
||||||
|
before, present := 0, 0
|
||||||
|
for i := range words {
|
||||||
|
befstr = aftstr
|
||||||
|
before = present
|
||||||
|
aftstr = aftstr + words[i] + " "
|
||||||
|
present = len(aftstr)
|
||||||
|
if present > length && i != 0 {
|
||||||
|
if (length - before) < (present - length) {
|
||||||
|
return Trim(befstr, " /\\.,\"'#!?&@+-") + ending
|
||||||
|
}
|
||||||
|
return Trim(aftstr, " /\\.,\"'#!?&@+-") + ending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadLeft pads left side of a string if size of string is less then indicated pad length
|
||||||
|
func PadLeft(str string, padStr string, padLen int) string {
|
||||||
|
return buildPadStr(str, padStr, padLen, true, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadRight pads right side of a string if size of string is less then indicated pad length
|
||||||
|
func PadRight(str string, padStr string, padLen int) string {
|
||||||
|
return buildPadStr(str, padStr, padLen, false, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadBoth pads both sides of a string if size of string is less then indicated pad length
|
||||||
|
func PadBoth(str string, padStr string, padLen int) string {
|
||||||
|
return buildPadStr(str, padStr, padLen, true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadString either left, right or both sides.
|
||||||
|
// Note that padding string can be unicode and more then one character
|
||||||
|
func buildPadStr(str string, padStr string, padLen int, padLeft bool, padRight bool) string {
|
||||||
|
|
||||||
|
// When padded length is less then the current string size
|
||||||
|
if padLen < utf8.RuneCountInString(str) {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
padLen -= utf8.RuneCountInString(str)
|
||||||
|
|
||||||
|
targetLen := padLen
|
||||||
|
|
||||||
|
targetLenLeft := targetLen
|
||||||
|
targetLenRight := targetLen
|
||||||
|
if padLeft && padRight {
|
||||||
|
targetLenLeft = padLen / 2
|
||||||
|
targetLenRight = padLen - targetLenLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
strToRepeatLen := utf8.RuneCountInString(padStr)
|
||||||
|
|
||||||
|
repeatTimes := int(math.Ceil(float64(targetLen) / float64(strToRepeatLen)))
|
||||||
|
repeatedString := strings.Repeat(padStr, repeatTimes)
|
||||||
|
|
||||||
|
leftSide := ""
|
||||||
|
if padLeft {
|
||||||
|
leftSide = repeatedString[0:targetLenLeft]
|
||||||
|
}
|
||||||
|
|
||||||
|
rightSide := ""
|
||||||
|
if padRight {
|
||||||
|
rightSide = repeatedString[0:targetLenRight]
|
||||||
|
}
|
||||||
|
|
||||||
|
return leftSide + str + rightSide
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncatingErrorf removes extra args from fmt.Errorf if not formatted in the str object
|
||||||
|
func TruncatingErrorf(str string, args ...interface{}) error {
|
||||||
|
n := strings.Count(str, "%s")
|
||||||
|
return fmt.Errorf(str, args[:n]...)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
||||||
|
box: golang
|
||||||
|
build:
|
||||||
|
steps:
|
||||||
|
- setup-go-workspace
|
||||||
|
|
||||||
|
- script:
|
||||||
|
name: go get
|
||||||
|
code: |
|
||||||
|
go version
|
||||||
|
go get -t ./...
|
||||||
|
|
||||||
|
- script:
|
||||||
|
name: go test
|
||||||
|
code: |
|
||||||
|
go test -race -v ./...
|
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright (c) 2016 Caleb Spare
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,72 @@
|
||||||
|
# xxhash
|
||||||
|
|
||||||
|
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
|
||||||
|
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
|
||||||
|
|
||||||
|
xxhash is a Go implementation of the 64-bit [xxHash] algorithm, XXH64. This is a
|
||||||
|
high-quality hashing algorithm that is much faster than anything in the Go
|
||||||
|
standard library.
|
||||||
|
|
||||||
|
This package provides a straightforward API:
|
||||||
|
|
||||||
|
```
|
||||||
|
func Sum64(b []byte) uint64
|
||||||
|
func Sum64String(s string) uint64
|
||||||
|
type Digest struct{ ... }
|
||||||
|
func New() *Digest
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Digest` type implements hash.Hash64. Its key methods are:
|
||||||
|
|
||||||
|
```
|
||||||
|
func (*Digest) Write([]byte) (int, error)
|
||||||
|
func (*Digest) WriteString(string) (int, error)
|
||||||
|
func (*Digest) Sum64() uint64
|
||||||
|
```
|
||||||
|
|
||||||
|
The package is written with optimized pure Go and also contains even faster
|
||||||
|
assembly implementations for amd64 and arm64. If desired, the `purego` build tag
|
||||||
|
opts into using the Go code even on those architectures.
|
||||||
|
|
||||||
|
[xxHash]: http://cyan4973.github.io/xxHash/
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
This package is in a module and the latest code is in version 2 of the module.
|
||||||
|
You need a version of Go with at least "minimal module compatibility" to use
|
||||||
|
github.com/cespare/xxhash/v2:
|
||||||
|
|
||||||
|
* 1.9.7+ for Go 1.9
|
||||||
|
* 1.10.3+ for Go 1.10
|
||||||
|
* Go 1.11 or later
|
||||||
|
|
||||||
|
I recommend using the latest release of Go.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
Here are some quick benchmarks comparing the pure-Go and assembly
|
||||||
|
implementations of Sum64.
|
||||||
|
|
||||||
|
| input size | purego | asm |
|
||||||
|
| ---------- | --------- | --------- |
|
||||||
|
| 4 B | 1.3 GB/s | 1.2 GB/s |
|
||||||
|
| 16 B | 2.9 GB/s | 3.5 GB/s |
|
||||||
|
| 100 B | 6.9 GB/s | 8.1 GB/s |
|
||||||
|
| 4 KB | 11.7 GB/s | 16.7 GB/s |
|
||||||
|
| 10 MB | 12.0 GB/s | 17.3 GB/s |
|
||||||
|
|
||||||
|
These numbers were generated on Ubuntu 20.04 with an Intel Xeon Platinum 8252C
|
||||||
|
CPU using the following commands under Go 1.19.2:
|
||||||
|
|
||||||
|
```
|
||||||
|
benchstat <(go test -tags purego -benchtime 500ms -count 15 -bench 'Sum64$')
|
||||||
|
benchstat <(go test -benchtime 500ms -count 15 -bench 'Sum64$')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projects using this package
|
||||||
|
|
||||||
|
- [InfluxDB](https://github.com/influxdata/influxdb)
|
||||||
|
- [Prometheus](https://github.com/prometheus/prometheus)
|
||||||
|
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
||||||
|
- [FreeCache](https://github.com/coocood/freecache)
|
||||||
|
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
|
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
# Small convenience script for running the tests with various combinations of
|
||||||
|
# arch/tags. This assumes we're running on amd64 and have qemu available.
|
||||||
|
|
||||||
|
go test ./...
|
||||||
|
go test -tags purego ./...
|
||||||
|
GOARCH=arm64 go test
|
||||||
|
GOARCH=arm64 go test -tags purego
|
|
@ -0,0 +1,228 @@
|
||||||
|
// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described
|
||||||
|
// at http://cyan4973.github.io/xxHash/.
|
||||||
|
package xxhash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"math/bits"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
prime1 uint64 = 11400714785074694791
|
||||||
|
prime2 uint64 = 14029467366897019727
|
||||||
|
prime3 uint64 = 1609587929392839161
|
||||||
|
prime4 uint64 = 9650029242287828579
|
||||||
|
prime5 uint64 = 2870177450012600261
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store the primes in an array as well.
|
||||||
|
//
|
||||||
|
// The consts are used when possible in Go code to avoid MOVs but we need a
|
||||||
|
// contiguous array of the assembly code.
|
||||||
|
var primes = [...]uint64{prime1, prime2, prime3, prime4, prime5}
|
||||||
|
|
||||||
|
// Digest implements hash.Hash64.
|
||||||
|
type Digest struct {
|
||||||
|
v1 uint64
|
||||||
|
v2 uint64
|
||||||
|
v3 uint64
|
||||||
|
v4 uint64
|
||||||
|
total uint64
|
||||||
|
mem [32]byte
|
||||||
|
n int // how much of mem is used
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Digest that computes the 64-bit xxHash algorithm.
|
||||||
|
func New() *Digest {
|
||||||
|
var d Digest
|
||||||
|
d.Reset()
|
||||||
|
return &d
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the Digest's state so that it can be reused.
|
||||||
|
func (d *Digest) Reset() {
|
||||||
|
d.v1 = primes[0] + prime2
|
||||||
|
d.v2 = prime2
|
||||||
|
d.v3 = 0
|
||||||
|
d.v4 = -primes[0]
|
||||||
|
d.total = 0
|
||||||
|
d.n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size always returns 8 bytes.
|
||||||
|
func (d *Digest) Size() int { return 8 }
|
||||||
|
|
||||||
|
// BlockSize always returns 32 bytes.
|
||||||
|
func (d *Digest) BlockSize() int { return 32 }
|
||||||
|
|
||||||
|
// Write adds more data to d. It always returns len(b), nil.
|
||||||
|
func (d *Digest) Write(b []byte) (n int, err error) {
|
||||||
|
n = len(b)
|
||||||
|
d.total += uint64(n)
|
||||||
|
|
||||||
|
memleft := d.mem[d.n&(len(d.mem)-1):]
|
||||||
|
|
||||||
|
if d.n+n < 32 {
|
||||||
|
// This new data doesn't even fill the current block.
|
||||||
|
copy(memleft, b)
|
||||||
|
d.n += n
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.n > 0 {
|
||||||
|
// Finish off the partial block.
|
||||||
|
c := copy(memleft, b)
|
||||||
|
d.v1 = round(d.v1, u64(d.mem[0:8]))
|
||||||
|
d.v2 = round(d.v2, u64(d.mem[8:16]))
|
||||||
|
d.v3 = round(d.v3, u64(d.mem[16:24]))
|
||||||
|
d.v4 = round(d.v4, u64(d.mem[24:32]))
|
||||||
|
b = b[c:]
|
||||||
|
d.n = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b) >= 32 {
|
||||||
|
// One or more full blocks left.
|
||||||
|
nw := writeBlocks(d, b)
|
||||||
|
b = b[nw:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store any remaining partial block.
|
||||||
|
copy(d.mem[:], b)
|
||||||
|
d.n = len(b)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum appends the current hash to b and returns the resulting slice.
|
||||||
|
func (d *Digest) Sum(b []byte) []byte {
|
||||||
|
s := d.Sum64()
|
||||||
|
return append(
|
||||||
|
b,
|
||||||
|
byte(s>>56),
|
||||||
|
byte(s>>48),
|
||||||
|
byte(s>>40),
|
||||||
|
byte(s>>32),
|
||||||
|
byte(s>>24),
|
||||||
|
byte(s>>16),
|
||||||
|
byte(s>>8),
|
||||||
|
byte(s),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum64 returns the current hash.
|
||||||
|
func (d *Digest) Sum64() uint64 {
|
||||||
|
var h uint64
|
||||||
|
|
||||||
|
if d.total >= 32 {
|
||||||
|
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
|
||||||
|
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
|
||||||
|
h = mergeRound(h, v1)
|
||||||
|
h = mergeRound(h, v2)
|
||||||
|
h = mergeRound(h, v3)
|
||||||
|
h = mergeRound(h, v4)
|
||||||
|
} else {
|
||||||
|
h = d.v3 + prime5
|
||||||
|
}
|
||||||
|
|
||||||
|
h += d.total
|
||||||
|
|
||||||
|
b := d.mem[:d.n&(len(d.mem)-1)]
|
||||||
|
for ; len(b) >= 8; b = b[8:] {
|
||||||
|
k1 := round(0, u64(b[:8]))
|
||||||
|
h ^= k1
|
||||||
|
h = rol27(h)*prime1 + prime4
|
||||||
|
}
|
||||||
|
if len(b) >= 4 {
|
||||||
|
h ^= uint64(u32(b[:4])) * prime1
|
||||||
|
h = rol23(h)*prime2 + prime3
|
||||||
|
b = b[4:]
|
||||||
|
}
|
||||||
|
for ; len(b) > 0; b = b[1:] {
|
||||||
|
h ^= uint64(b[0]) * prime5
|
||||||
|
h = rol11(h) * prime1
|
||||||
|
}
|
||||||
|
|
||||||
|
h ^= h >> 33
|
||||||
|
h *= prime2
|
||||||
|
h ^= h >> 29
|
||||||
|
h *= prime3
|
||||||
|
h ^= h >> 32
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
magic = "xxh\x06"
|
||||||
|
marshaledSize = len(magic) + 8*5 + 32
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
||||||
|
func (d *Digest) MarshalBinary() ([]byte, error) {
|
||||||
|
b := make([]byte, 0, marshaledSize)
|
||||||
|
b = append(b, magic...)
|
||||||
|
b = appendUint64(b, d.v1)
|
||||||
|
b = appendUint64(b, d.v2)
|
||||||
|
b = appendUint64(b, d.v3)
|
||||||
|
b = appendUint64(b, d.v4)
|
||||||
|
b = appendUint64(b, d.total)
|
||||||
|
b = append(b, d.mem[:d.n]...)
|
||||||
|
b = b[:len(b)+len(d.mem)-d.n]
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
||||||
|
func (d *Digest) UnmarshalBinary(b []byte) error {
|
||||||
|
if len(b) < len(magic) || string(b[:len(magic)]) != magic {
|
||||||
|
return errors.New("xxhash: invalid hash state identifier")
|
||||||
|
}
|
||||||
|
if len(b) != marshaledSize {
|
||||||
|
return errors.New("xxhash: invalid hash state size")
|
||||||
|
}
|
||||||
|
b = b[len(magic):]
|
||||||
|
b, d.v1 = consumeUint64(b)
|
||||||
|
b, d.v2 = consumeUint64(b)
|
||||||
|
b, d.v3 = consumeUint64(b)
|
||||||
|
b, d.v4 = consumeUint64(b)
|
||||||
|
b, d.total = consumeUint64(b)
|
||||||
|
copy(d.mem[:], b)
|
||||||
|
d.n = int(d.total % uint64(len(d.mem)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendUint64(b []byte, x uint64) []byte {
|
||||||
|
var a [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(a[:], x)
|
||||||
|
return append(b, a[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeUint64(b []byte) ([]byte, uint64) {
|
||||||
|
x := u64(b)
|
||||||
|
return b[8:], x
|
||||||
|
}
|
||||||
|
|
||||||
|
func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) }
|
||||||
|
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
|
||||||
|
|
||||||
|
func round(acc, input uint64) uint64 {
|
||||||
|
acc += input * prime2
|
||||||
|
acc = rol31(acc)
|
||||||
|
acc *= prime1
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeRound(acc, val uint64) uint64 {
|
||||||
|
val = round(0, val)
|
||||||
|
acc ^= val
|
||||||
|
acc = acc*prime1 + prime4
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) }
|
||||||
|
func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) }
|
||||||
|
func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) }
|
||||||
|
func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) }
|
||||||
|
func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) }
|
||||||
|
func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) }
|
||||||
|
func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) }
|
||||||
|
func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) }
|
|
@ -0,0 +1,209 @@
|
||||||
|
//go:build !appengine && gc && !purego
|
||||||
|
// +build !appengine
|
||||||
|
// +build gc
|
||||||
|
// +build !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// Registers:
|
||||||
|
#define h AX
|
||||||
|
#define d AX
|
||||||
|
#define p SI // pointer to advance through b
|
||||||
|
#define n DX
|
||||||
|
#define end BX // loop end
|
||||||
|
#define v1 R8
|
||||||
|
#define v2 R9
|
||||||
|
#define v3 R10
|
||||||
|
#define v4 R11
|
||||||
|
#define x R12
|
||||||
|
#define prime1 R13
|
||||||
|
#define prime2 R14
|
||||||
|
#define prime4 DI
|
||||||
|
|
||||||
|
#define round(acc, x) \
|
||||||
|
IMULQ prime2, x \
|
||||||
|
ADDQ x, acc \
|
||||||
|
ROLQ $31, acc \
|
||||||
|
IMULQ prime1, acc
|
||||||
|
|
||||||
|
// round0 performs the operation x = round(0, x).
|
||||||
|
#define round0(x) \
|
||||||
|
IMULQ prime2, x \
|
||||||
|
ROLQ $31, x \
|
||||||
|
IMULQ prime1, x
|
||||||
|
|
||||||
|
// mergeRound applies a merge round on the two registers acc and x.
|
||||||
|
// It assumes that prime1, prime2, and prime4 have been loaded.
|
||||||
|
#define mergeRound(acc, x) \
|
||||||
|
round0(x) \
|
||||||
|
XORQ x, acc \
|
||||||
|
IMULQ prime1, acc \
|
||||||
|
ADDQ prime4, acc
|
||||||
|
|
||||||
|
// blockLoop processes as many 32-byte blocks as possible,
|
||||||
|
// updating v1, v2, v3, and v4. It assumes that there is at least one block
|
||||||
|
// to process.
|
||||||
|
#define blockLoop() \
|
||||||
|
loop: \
|
||||||
|
MOVQ +0(p), x \
|
||||||
|
round(v1, x) \
|
||||||
|
MOVQ +8(p), x \
|
||||||
|
round(v2, x) \
|
||||||
|
MOVQ +16(p), x \
|
||||||
|
round(v3, x) \
|
||||||
|
MOVQ +24(p), x \
|
||||||
|
round(v4, x) \
|
||||||
|
ADDQ $32, p \
|
||||||
|
CMPQ p, end \
|
||||||
|
JLE loop
|
||||||
|
|
||||||
|
// func Sum64(b []byte) uint64
|
||||||
|
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
|
||||||
|
// Load fixed primes.
|
||||||
|
MOVQ ·primes+0(SB), prime1
|
||||||
|
MOVQ ·primes+8(SB), prime2
|
||||||
|
MOVQ ·primes+24(SB), prime4
|
||||||
|
|
||||||
|
// Load slice.
|
||||||
|
MOVQ b_base+0(FP), p
|
||||||
|
MOVQ b_len+8(FP), n
|
||||||
|
LEAQ (p)(n*1), end
|
||||||
|
|
||||||
|
// The first loop limit will be len(b)-32.
|
||||||
|
SUBQ $32, end
|
||||||
|
|
||||||
|
// Check whether we have at least one block.
|
||||||
|
CMPQ n, $32
|
||||||
|
JLT noBlocks
|
||||||
|
|
||||||
|
// Set up initial state (v1, v2, v3, v4).
|
||||||
|
MOVQ prime1, v1
|
||||||
|
ADDQ prime2, v1
|
||||||
|
MOVQ prime2, v2
|
||||||
|
XORQ v3, v3
|
||||||
|
XORQ v4, v4
|
||||||
|
SUBQ prime1, v4
|
||||||
|
|
||||||
|
blockLoop()
|
||||||
|
|
||||||
|
MOVQ v1, h
|
||||||
|
ROLQ $1, h
|
||||||
|
MOVQ v2, x
|
||||||
|
ROLQ $7, x
|
||||||
|
ADDQ x, h
|
||||||
|
MOVQ v3, x
|
||||||
|
ROLQ $12, x
|
||||||
|
ADDQ x, h
|
||||||
|
MOVQ v4, x
|
||||||
|
ROLQ $18, x
|
||||||
|
ADDQ x, h
|
||||||
|
|
||||||
|
mergeRound(h, v1)
|
||||||
|
mergeRound(h, v2)
|
||||||
|
mergeRound(h, v3)
|
||||||
|
mergeRound(h, v4)
|
||||||
|
|
||||||
|
JMP afterBlocks
|
||||||
|
|
||||||
|
noBlocks:
|
||||||
|
MOVQ ·primes+32(SB), h
|
||||||
|
|
||||||
|
afterBlocks:
|
||||||
|
ADDQ n, h
|
||||||
|
|
||||||
|
ADDQ $24, end
|
||||||
|
CMPQ p, end
|
||||||
|
JG try4
|
||||||
|
|
||||||
|
loop8:
|
||||||
|
MOVQ (p), x
|
||||||
|
ADDQ $8, p
|
||||||
|
round0(x)
|
||||||
|
XORQ x, h
|
||||||
|
ROLQ $27, h
|
||||||
|
IMULQ prime1, h
|
||||||
|
ADDQ prime4, h
|
||||||
|
|
||||||
|
CMPQ p, end
|
||||||
|
JLE loop8
|
||||||
|
|
||||||
|
try4:
|
||||||
|
ADDQ $4, end
|
||||||
|
CMPQ p, end
|
||||||
|
JG try1
|
||||||
|
|
||||||
|
MOVL (p), x
|
||||||
|
ADDQ $4, p
|
||||||
|
IMULQ prime1, x
|
||||||
|
XORQ x, h
|
||||||
|
|
||||||
|
ROLQ $23, h
|
||||||
|
IMULQ prime2, h
|
||||||
|
ADDQ ·primes+16(SB), h
|
||||||
|
|
||||||
|
try1:
|
||||||
|
ADDQ $4, end
|
||||||
|
CMPQ p, end
|
||||||
|
JGE finalize
|
||||||
|
|
||||||
|
loop1:
|
||||||
|
MOVBQZX (p), x
|
||||||
|
ADDQ $1, p
|
||||||
|
IMULQ ·primes+32(SB), x
|
||||||
|
XORQ x, h
|
||||||
|
ROLQ $11, h
|
||||||
|
IMULQ prime1, h
|
||||||
|
|
||||||
|
CMPQ p, end
|
||||||
|
JL loop1
|
||||||
|
|
||||||
|
finalize:
|
||||||
|
MOVQ h, x
|
||||||
|
SHRQ $33, x
|
||||||
|
XORQ x, h
|
||||||
|
IMULQ prime2, h
|
||||||
|
MOVQ h, x
|
||||||
|
SHRQ $29, x
|
||||||
|
XORQ x, h
|
||||||
|
IMULQ ·primes+16(SB), h
|
||||||
|
MOVQ h, x
|
||||||
|
SHRQ $32, x
|
||||||
|
XORQ x, h
|
||||||
|
|
||||||
|
MOVQ h, ret+24(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func writeBlocks(d *Digest, b []byte) int
|
||||||
|
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
||||||
|
// Load fixed primes needed for round.
|
||||||
|
MOVQ ·primes+0(SB), prime1
|
||||||
|
MOVQ ·primes+8(SB), prime2
|
||||||
|
|
||||||
|
// Load slice.
|
||||||
|
MOVQ b_base+8(FP), p
|
||||||
|
MOVQ b_len+16(FP), n
|
||||||
|
LEAQ (p)(n*1), end
|
||||||
|
SUBQ $32, end
|
||||||
|
|
||||||
|
// Load vN from d.
|
||||||
|
MOVQ s+0(FP), d
|
||||||
|
MOVQ 0(d), v1
|
||||||
|
MOVQ 8(d), v2
|
||||||
|
MOVQ 16(d), v3
|
||||||
|
MOVQ 24(d), v4
|
||||||
|
|
||||||
|
// We don't need to check the loop condition here; this function is
|
||||||
|
// always called with at least one block of data to process.
|
||||||
|
blockLoop()
|
||||||
|
|
||||||
|
// Copy vN back to d.
|
||||||
|
MOVQ v1, 0(d)
|
||||||
|
MOVQ v2, 8(d)
|
||||||
|
MOVQ v3, 16(d)
|
||||||
|
MOVQ v4, 24(d)
|
||||||
|
|
||||||
|
// The number of bytes written is p minus the old base pointer.
|
||||||
|
SUBQ b_base+8(FP), p
|
||||||
|
MOVQ p, ret+32(FP)
|
||||||
|
|
||||||
|
RET
|
|
@ -0,0 +1,183 @@
|
||||||
|
//go:build !appengine && gc && !purego
|
||||||
|
// +build !appengine
|
||||||
|
// +build gc
|
||||||
|
// +build !purego
|
||||||
|
|
||||||
|
#include "textflag.h"
|
||||||
|
|
||||||
|
// Registers:
|
||||||
|
#define digest R1
|
||||||
|
#define h R2 // return value
|
||||||
|
#define p R3 // input pointer
|
||||||
|
#define n R4 // input length
|
||||||
|
#define nblocks R5 // n / 32
|
||||||
|
#define prime1 R7
|
||||||
|
#define prime2 R8
|
||||||
|
#define prime3 R9
|
||||||
|
#define prime4 R10
|
||||||
|
#define prime5 R11
|
||||||
|
#define v1 R12
|
||||||
|
#define v2 R13
|
||||||
|
#define v3 R14
|
||||||
|
#define v4 R15
|
||||||
|
#define x1 R20
|
||||||
|
#define x2 R21
|
||||||
|
#define x3 R22
|
||||||
|
#define x4 R23
|
||||||
|
|
||||||
|
#define round(acc, x) \
|
||||||
|
MADD prime2, acc, x, acc \
|
||||||
|
ROR $64-31, acc \
|
||||||
|
MUL prime1, acc
|
||||||
|
|
||||||
|
// round0 performs the operation x = round(0, x).
|
||||||
|
#define round0(x) \
|
||||||
|
MUL prime2, x \
|
||||||
|
ROR $64-31, x \
|
||||||
|
MUL prime1, x
|
||||||
|
|
||||||
|
#define mergeRound(acc, x) \
|
||||||
|
round0(x) \
|
||||||
|
EOR x, acc \
|
||||||
|
MADD acc, prime4, prime1, acc
|
||||||
|
|
||||||
|
// blockLoop processes as many 32-byte blocks as possible,
|
||||||
|
// updating v1, v2, v3, and v4. It assumes that n >= 32.
|
||||||
|
#define blockLoop() \
|
||||||
|
LSR $5, n, nblocks \
|
||||||
|
PCALIGN $16 \
|
||||||
|
loop: \
|
||||||
|
LDP.P 16(p), (x1, x2) \
|
||||||
|
LDP.P 16(p), (x3, x4) \
|
||||||
|
round(v1, x1) \
|
||||||
|
round(v2, x2) \
|
||||||
|
round(v3, x3) \
|
||||||
|
round(v4, x4) \
|
||||||
|
SUB $1, nblocks \
|
||||||
|
CBNZ nblocks, loop
|
||||||
|
|
||||||
|
// func Sum64(b []byte) uint64
|
||||||
|
TEXT ·Sum64(SB), NOSPLIT|NOFRAME, $0-32
|
||||||
|
LDP b_base+0(FP), (p, n)
|
||||||
|
|
||||||
|
LDP ·primes+0(SB), (prime1, prime2)
|
||||||
|
LDP ·primes+16(SB), (prime3, prime4)
|
||||||
|
MOVD ·primes+32(SB), prime5
|
||||||
|
|
||||||
|
CMP $32, n
|
||||||
|
CSEL LT, prime5, ZR, h // if n < 32 { h = prime5 } else { h = 0 }
|
||||||
|
BLT afterLoop
|
||||||
|
|
||||||
|
ADD prime1, prime2, v1
|
||||||
|
MOVD prime2, v2
|
||||||
|
MOVD $0, v3
|
||||||
|
NEG prime1, v4
|
||||||
|
|
||||||
|
blockLoop()
|
||||||
|
|
||||||
|
ROR $64-1, v1, x1
|
||||||
|
ROR $64-7, v2, x2
|
||||||
|
ADD x1, x2
|
||||||
|
ROR $64-12, v3, x3
|
||||||
|
ROR $64-18, v4, x4
|
||||||
|
ADD x3, x4
|
||||||
|
ADD x2, x4, h
|
||||||
|
|
||||||
|
mergeRound(h, v1)
|
||||||
|
mergeRound(h, v2)
|
||||||
|
mergeRound(h, v3)
|
||||||
|
mergeRound(h, v4)
|
||||||
|
|
||||||
|
afterLoop:
|
||||||
|
ADD n, h
|
||||||
|
|
||||||
|
TBZ $4, n, try8
|
||||||
|
LDP.P 16(p), (x1, x2)
|
||||||
|
|
||||||
|
round0(x1)
|
||||||
|
|
||||||
|
// NOTE: here and below, sequencing the EOR after the ROR (using a
|
||||||
|
// rotated register) is worth a small but measurable speedup for small
|
||||||
|
// inputs.
|
||||||
|
ROR $64-27, h
|
||||||
|
EOR x1 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h
|
||||||
|
|
||||||
|
round0(x2)
|
||||||
|
ROR $64-27, h
|
||||||
|
EOR x2 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h
|
||||||
|
|
||||||
|
try8:
|
||||||
|
TBZ $3, n, try4
|
||||||
|
MOVD.P 8(p), x1
|
||||||
|
|
||||||
|
round0(x1)
|
||||||
|
ROR $64-27, h
|
||||||
|
EOR x1 @> 64-27, h, h
|
||||||
|
MADD h, prime4, prime1, h
|
||||||
|
|
||||||
|
try4:
|
||||||
|
TBZ $2, n, try2
|
||||||
|
MOVWU.P 4(p), x2
|
||||||
|
|
||||||
|
MUL prime1, x2
|
||||||
|
ROR $64-23, h
|
||||||
|
EOR x2 @> 64-23, h, h
|
||||||
|
MADD h, prime3, prime2, h
|
||||||
|
|
||||||
|
try2:
|
||||||
|
TBZ $1, n, try1
|
||||||
|
MOVHU.P 2(p), x3
|
||||||
|
AND $255, x3, x1
|
||||||
|
LSR $8, x3, x2
|
||||||
|
|
||||||
|
MUL prime5, x1
|
||||||
|
ROR $64-11, h
|
||||||
|
EOR x1 @> 64-11, h, h
|
||||||
|
MUL prime1, h
|
||||||
|
|
||||||
|
MUL prime5, x2
|
||||||
|
ROR $64-11, h
|
||||||
|
EOR x2 @> 64-11, h, h
|
||||||
|
MUL prime1, h
|
||||||
|
|
||||||
|
try1:
|
||||||
|
TBZ $0, n, finalize
|
||||||
|
MOVBU (p), x4
|
||||||
|
|
||||||
|
MUL prime5, x4
|
||||||
|
ROR $64-11, h
|
||||||
|
EOR x4 @> 64-11, h, h
|
||||||
|
MUL prime1, h
|
||||||
|
|
||||||
|
finalize:
|
||||||
|
EOR h >> 33, h
|
||||||
|
MUL prime2, h
|
||||||
|
EOR h >> 29, h
|
||||||
|
MUL prime3, h
|
||||||
|
EOR h >> 32, h
|
||||||
|
|
||||||
|
MOVD h, ret+24(FP)
|
||||||
|
RET
|
||||||
|
|
||||||
|
// func writeBlocks(d *Digest, b []byte) int
|
||||||
|
TEXT ·writeBlocks(SB), NOSPLIT|NOFRAME, $0-40
|
||||||
|
LDP ·primes+0(SB), (prime1, prime2)
|
||||||
|
|
||||||
|
// Load state. Assume v[1-4] are stored contiguously.
|
||||||
|
MOVD d+0(FP), digest
|
||||||
|
LDP 0(digest), (v1, v2)
|
||||||
|
LDP 16(digest), (v3, v4)
|
||||||
|
|
||||||
|
LDP b_base+8(FP), (p, n)
|
||||||
|
|
||||||
|
blockLoop()
|
||||||
|
|
||||||
|
// Store updated state.
|
||||||
|
STP (v1, v2), 0(digest)
|
||||||
|
STP (v3, v4), 16(digest)
|
||||||
|
|
||||||
|
BIC $31, n
|
||||||
|
MOVD n, ret+32(FP)
|
||||||
|
RET
|
|
@ -0,0 +1,15 @@
|
||||||
|
//go:build (amd64 || arm64) && !appengine && gc && !purego
|
||||||
|
// +build amd64 arm64
|
||||||
|
// +build !appengine
|
||||||
|
// +build gc
|
||||||
|
// +build !purego
|
||||||
|
|
||||||
|
package xxhash
|
||||||
|
|
||||||
|
// Sum64 computes the 64-bit xxHash digest of b.
|
||||||
|
//
|
||||||
|
//go:noescape
|
||||||
|
func Sum64(b []byte) uint64
|
||||||
|
|
||||||
|
//go:noescape
|
||||||
|
func writeBlocks(d *Digest, b []byte) int
|
|
@ -0,0 +1,76 @@
|
||||||
|
//go:build (!amd64 && !arm64) || appengine || !gc || purego
|
||||||
|
// +build !amd64,!arm64 appengine !gc purego
|
||||||
|
|
||||||
|
package xxhash
|
||||||
|
|
||||||
|
// Sum64 computes the 64-bit xxHash digest of b.
|
||||||
|
func Sum64(b []byte) uint64 {
|
||||||
|
// A simpler version would be
|
||||||
|
// d := New()
|
||||||
|
// d.Write(b)
|
||||||
|
// return d.Sum64()
|
||||||
|
// but this is faster, particularly for small inputs.
|
||||||
|
|
||||||
|
n := len(b)
|
||||||
|
var h uint64
|
||||||
|
|
||||||
|
if n >= 32 {
|
||||||
|
v1 := primes[0] + prime2
|
||||||
|
v2 := prime2
|
||||||
|
v3 := uint64(0)
|
||||||
|
v4 := -primes[0]
|
||||||
|
for len(b) >= 32 {
|
||||||
|
v1 = round(v1, u64(b[0:8:len(b)]))
|
||||||
|
v2 = round(v2, u64(b[8:16:len(b)]))
|
||||||
|
v3 = round(v3, u64(b[16:24:len(b)]))
|
||||||
|
v4 = round(v4, u64(b[24:32:len(b)]))
|
||||||
|
b = b[32:len(b):len(b)]
|
||||||
|
}
|
||||||
|
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
|
||||||
|
h = mergeRound(h, v1)
|
||||||
|
h = mergeRound(h, v2)
|
||||||
|
h = mergeRound(h, v3)
|
||||||
|
h = mergeRound(h, v4)
|
||||||
|
} else {
|
||||||
|
h = prime5
|
||||||
|
}
|
||||||
|
|
||||||
|
h += uint64(n)
|
||||||
|
|
||||||
|
for ; len(b) >= 8; b = b[8:] {
|
||||||
|
k1 := round(0, u64(b[:8]))
|
||||||
|
h ^= k1
|
||||||
|
h = rol27(h)*prime1 + prime4
|
||||||
|
}
|
||||||
|
if len(b) >= 4 {
|
||||||
|
h ^= uint64(u32(b[:4])) * prime1
|
||||||
|
h = rol23(h)*prime2 + prime3
|
||||||
|
b = b[4:]
|
||||||
|
}
|
||||||
|
for ; len(b) > 0; b = b[1:] {
|
||||||
|
h ^= uint64(b[0]) * prime5
|
||||||
|
h = rol11(h) * prime1
|
||||||
|
}
|
||||||
|
|
||||||
|
h ^= h >> 33
|
||||||
|
h *= prime2
|
||||||
|
h ^= h >> 29
|
||||||
|
h *= prime3
|
||||||
|
h ^= h >> 32
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBlocks(d *Digest, b []byte) int {
|
||||||
|
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
|
||||||
|
n := len(b)
|
||||||
|
for len(b) >= 32 {
|
||||||
|
v1 = round(v1, u64(b[0:8:len(b)]))
|
||||||
|
v2 = round(v2, u64(b[8:16:len(b)]))
|
||||||
|
v3 = round(v3, u64(b[16:24:len(b)]))
|
||||||
|
v4 = round(v4, u64(b[24:32:len(b)]))
|
||||||
|
b = b[32:len(b):len(b)]
|
||||||
|
}
|
||||||
|
d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4
|
||||||
|
return n - len(b)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
//go:build appengine
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
// This file contains the safe implementations of otherwise unsafe-using code.
|
||||||
|
|
||||||
|
package xxhash
|
||||||
|
|
||||||
|
// Sum64String computes the 64-bit xxHash digest of s.
|
||||||
|
func Sum64String(s string) uint64 {
|
||||||
|
return Sum64([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString adds more data to d. It always returns len(s), nil.
|
||||||
|
func (d *Digest) WriteString(s string) (n int, err error) {
|
||||||
|
return d.Write([]byte(s))
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
//go:build !appengine
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
// This file encapsulates usage of unsafe.
|
||||||
|
// xxhash_safe.go contains the safe implementations.
|
||||||
|
|
||||||
|
package xxhash
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// In the future it's possible that compiler optimizations will make these
|
||||||
|
// XxxString functions unnecessary by realizing that calls such as
|
||||||
|
// Sum64([]byte(s)) don't need to copy s. See https://go.dev/issue/2205.
|
||||||
|
// If that happens, even if we keep these functions they can be replaced with
|
||||||
|
// the trivial safe code.
|
||||||
|
|
||||||
|
// NOTE: The usual way of doing an unsafe string-to-[]byte conversion is:
|
||||||
|
//
|
||||||
|
// var b []byte
|
||||||
|
// bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||||
|
// bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
|
||||||
|
// bh.Len = len(s)
|
||||||
|
// bh.Cap = len(s)
|
||||||
|
//
|
||||||
|
// Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough
|
||||||
|
// weight to this sequence of expressions that any function that uses it will
|
||||||
|
// not be inlined. Instead, the functions below use a different unsafe
|
||||||
|
// conversion designed to minimize the inliner weight and allow both to be
|
||||||
|
// inlined. There is also a test (TestInlining) which verifies that these are
|
||||||
|
// inlined.
|
||||||
|
//
|
||||||
|
// See https://github.com/golang/go/issues/42739 for discussion.
|
||||||
|
|
||||||
|
// Sum64String computes the 64-bit xxHash digest of s.
|
||||||
|
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
|
||||||
|
func Sum64String(s string) uint64 {
|
||||||
|
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
|
||||||
|
return Sum64(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString adds more data to d. It always returns len(s), nil.
|
||||||
|
// It may be faster than Write([]byte(s)) by avoiding a copy.
|
||||||
|
func (d *Digest) WriteString(s string) (n int, err error) {
|
||||||
|
d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})))
|
||||||
|
// d.Write always returns len(s), nil.
|
||||||
|
// Ignoring the return output and returning these fixed values buys a
|
||||||
|
// savings of 6 in the inliner's cost model.
|
||||||
|
return len(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout
|
||||||
|
// of the first two words is the same as the layout of a string.
|
||||||
|
type sliceHeader struct {
|
||||||
|
s string
|
||||||
|
cap int
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2017-2020 Damian Gryski <damian@gryski.com>
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,79 @@
|
||||||
|
package rendezvous
|
||||||
|
|
||||||
|
type Rendezvous struct {
|
||||||
|
nodes map[string]int
|
||||||
|
nstr []string
|
||||||
|
nhash []uint64
|
||||||
|
hash Hasher
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hasher func(s string) uint64
|
||||||
|
|
||||||
|
func New(nodes []string, hash Hasher) *Rendezvous {
|
||||||
|
r := &Rendezvous{
|
||||||
|
nodes: make(map[string]int, len(nodes)),
|
||||||
|
nstr: make([]string, len(nodes)),
|
||||||
|
nhash: make([]uint64, len(nodes)),
|
||||||
|
hash: hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, n := range nodes {
|
||||||
|
r.nodes[n] = i
|
||||||
|
r.nstr[i] = n
|
||||||
|
r.nhash[i] = hash(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rendezvous) Lookup(k string) string {
|
||||||
|
// short-circuit if we're empty
|
||||||
|
if len(r.nodes) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
khash := r.hash(k)
|
||||||
|
|
||||||
|
var midx int
|
||||||
|
var mhash = xorshiftMult64(khash ^ r.nhash[0])
|
||||||
|
|
||||||
|
for i, nhash := range r.nhash[1:] {
|
||||||
|
if h := xorshiftMult64(khash ^ nhash); h > mhash {
|
||||||
|
midx = i + 1
|
||||||
|
mhash = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.nstr[midx]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rendezvous) Add(node string) {
|
||||||
|
r.nodes[node] = len(r.nstr)
|
||||||
|
r.nstr = append(r.nstr, node)
|
||||||
|
r.nhash = append(r.nhash, r.hash(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Rendezvous) Remove(node string) {
|
||||||
|
// find index of node to remove
|
||||||
|
nidx := r.nodes[node]
|
||||||
|
|
||||||
|
// remove from the slices
|
||||||
|
l := len(r.nstr)
|
||||||
|
r.nstr[nidx] = r.nstr[l]
|
||||||
|
r.nstr = r.nstr[:l]
|
||||||
|
|
||||||
|
r.nhash[nidx] = r.nhash[l]
|
||||||
|
r.nhash = r.nhash[:l]
|
||||||
|
|
||||||
|
// update the map
|
||||||
|
delete(r.nodes, node)
|
||||||
|
moved := r.nstr[nidx]
|
||||||
|
r.nodes[moved] = nidx
|
||||||
|
}
|
||||||
|
|
||||||
|
func xorshiftMult64(x uint64) uint64 {
|
||||||
|
x ^= x >> 12 // a
|
||||||
|
x ^= x << 25 // b
|
||||||
|
x ^= x >> 27 // c
|
||||||
|
return x * 2685821657736338717
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
|
@ -0,0 +1,13 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- 1.9.x
|
||||||
|
- tip
|
||||||
|
sudo: false
|
||||||
|
before_install:
|
||||||
|
- go get github.com/axw/gocov/gocov
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -service=travis-ci
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Fatih Arslan
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,163 @@
|
||||||
|
# Structs [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/structs) [![Build Status](http://img.shields.io/travis/fatih/structs.svg?style=flat-square)](https://travis-ci.org/fatih/structs) [![Coverage Status](http://img.shields.io/coveralls/fatih/structs.svg?style=flat-square)](https://coveralls.io/r/fatih/structs)
|
||||||
|
|
||||||
|
Structs contains various utilities to work with Go (Golang) structs. It was
|
||||||
|
initially used by me to convert a struct into a `map[string]interface{}`. With
|
||||||
|
time I've added other utilities for structs. It's basically a high level
|
||||||
|
package based on primitives from the reflect package. Feel free to add new
|
||||||
|
functions or improve the existing code.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/fatih/structs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage and Examples
|
||||||
|
|
||||||
|
Just like the standard lib `strings`, `bytes` and co packages, `structs` has
|
||||||
|
many global functions to manipulate or organize your struct data. Lets define
|
||||||
|
and declare a struct:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Server struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ID int
|
||||||
|
Enabled bool
|
||||||
|
users []string // not exported
|
||||||
|
http.Server // embedded
|
||||||
|
}
|
||||||
|
|
||||||
|
server := &Server{
|
||||||
|
Name: "gopher",
|
||||||
|
ID: 123456,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Convert a struct to a map[string]interface{}
|
||||||
|
// => {"Name":"gopher", "ID":123456, "Enabled":true}
|
||||||
|
m := structs.Map(server)
|
||||||
|
|
||||||
|
// Convert the values of a struct to a []interface{}
|
||||||
|
// => ["gopher", 123456, true]
|
||||||
|
v := structs.Values(server)
|
||||||
|
|
||||||
|
// Convert the names of a struct to a []string
|
||||||
|
// (see "Names methods" for more info about fields)
|
||||||
|
n := structs.Names(server)
|
||||||
|
|
||||||
|
// Convert the values of a struct to a []*Field
|
||||||
|
// (see "Field methods" for more info about fields)
|
||||||
|
f := structs.Fields(server)
|
||||||
|
|
||||||
|
// Return the struct name => "Server"
|
||||||
|
n := structs.Name(server)
|
||||||
|
|
||||||
|
// Check if any field of a struct is initialized or not.
|
||||||
|
h := structs.HasZero(server)
|
||||||
|
|
||||||
|
// Check if all fields of a struct is initialized or not.
|
||||||
|
z := structs.IsZero(server)
|
||||||
|
|
||||||
|
// Check if server is a struct or a pointer to struct
|
||||||
|
i := structs.IsStruct(server)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Struct methods
|
||||||
|
|
||||||
|
The structs functions can be also used as independent methods by creating a new
|
||||||
|
`*structs.Struct`. This is handy if you want to have more control over the
|
||||||
|
structs (such as retrieving a single Field).
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a new struct type:
|
||||||
|
s := structs.New(server)
|
||||||
|
|
||||||
|
m := s.Map() // Get a map[string]interface{}
|
||||||
|
v := s.Values() // Get a []interface{}
|
||||||
|
f := s.Fields() // Get a []*Field
|
||||||
|
n := s.Names() // Get a []string
|
||||||
|
f := s.Field(name) // Get a *Field based on the given field name
|
||||||
|
f, ok := s.FieldOk(name) // Get a *Field based on the given field name
|
||||||
|
n := s.Name() // Get the struct name
|
||||||
|
h := s.HasZero() // Check if any field is uninitialized
|
||||||
|
z := s.IsZero() // Check if all fields are uninitialized
|
||||||
|
```
|
||||||
|
|
||||||
|
### Field methods
|
||||||
|
|
||||||
|
We can easily examine a single Field for more detail. Below you can see how we
|
||||||
|
get and interact with various field methods:
|
||||||
|
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := structs.New(server)
|
||||||
|
|
||||||
|
// Get the Field struct for the "Name" field
|
||||||
|
name := s.Field("Name")
|
||||||
|
|
||||||
|
// Get the underlying value, value => "gopher"
|
||||||
|
value := name.Value().(string)
|
||||||
|
|
||||||
|
// Set the field's value
|
||||||
|
name.Set("another gopher")
|
||||||
|
|
||||||
|
// Get the field's kind, kind => "string"
|
||||||
|
name.Kind()
|
||||||
|
|
||||||
|
// Check if the field is exported or not
|
||||||
|
if name.IsExported() {
|
||||||
|
fmt.Println("Name field is exported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the value is a zero value, such as "" for string, 0 for int
|
||||||
|
if !name.IsZero() {
|
||||||
|
fmt.Println("Name is initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field is an anonymous (embedded) field
|
||||||
|
if !name.IsEmbedded() {
|
||||||
|
fmt.Println("Name is not an embedded field")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Field's tag value for tag name "json", tag value => "name,omitempty"
|
||||||
|
tagValue := name.Tag("json")
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested structs are supported too:
|
||||||
|
|
||||||
|
```go
|
||||||
|
addrField := s.Field("Server").Field("Addr")
|
||||||
|
|
||||||
|
// Get the value for addr
|
||||||
|
a := addrField.Value().(string)
|
||||||
|
|
||||||
|
// Or get all fields
|
||||||
|
httpServer := s.Field("Server").Fields()
|
||||||
|
```
|
||||||
|
|
||||||
|
We can also get a slice of Fields from the Struct type to iterate over all
|
||||||
|
fields. This is handy if you wish to examine all fields:
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := structs.New(server)
|
||||||
|
|
||||||
|
for _, f := range s.Fields() {
|
||||||
|
fmt.Printf("field name: %+v\n", f.Name())
|
||||||
|
|
||||||
|
if f.IsExported() {
|
||||||
|
fmt.Printf("value : %+v\n", f.Value())
|
||||||
|
fmt.Printf("is zero : %+v\n", f.IsZero())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [Fatih Arslan](https://github.com/fatih)
|
||||||
|
* [Cihangir Savas](https://github.com/cihangir)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The MIT License (MIT) - see LICENSE.md for more details
|
|
@ -0,0 +1,141 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNotExported = errors.New("field is not exported")
|
||||||
|
errNotSettable = errors.New("field is not settable")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field represents a single struct field that encapsulates high level
|
||||||
|
// functions around the field.
|
||||||
|
type Field struct {
|
||||||
|
value reflect.Value
|
||||||
|
field reflect.StructField
|
||||||
|
defaultTag string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag returns the value associated with key in the tag string. If there is no
|
||||||
|
// such key in the tag, Tag returns the empty string.
|
||||||
|
func (f *Field) Tag(key string) string {
|
||||||
|
return f.field.Tag.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value returns the underlying value of the field. It panics if the field
|
||||||
|
// is not exported.
|
||||||
|
func (f *Field) Value() interface{} {
|
||||||
|
return f.value.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmbedded returns true if the given field is an anonymous field (embedded)
|
||||||
|
func (f *Field) IsEmbedded() bool {
|
||||||
|
return f.field.Anonymous
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExported returns true if the given field is exported.
|
||||||
|
func (f *Field) IsExported() bool {
|
||||||
|
return f.field.PkgPath == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns true if the given field is not initialized (has a zero value).
|
||||||
|
// It panics if the field is not exported.
|
||||||
|
func (f *Field) IsZero() bool {
|
||||||
|
zero := reflect.Zero(f.value.Type()).Interface()
|
||||||
|
current := f.Value()
|
||||||
|
|
||||||
|
return reflect.DeepEqual(current, zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the given field
|
||||||
|
func (f *Field) Name() string {
|
||||||
|
return f.field.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind returns the fields kind, such as "string", "map", "bool", etc ..
|
||||||
|
func (f *Field) Kind() reflect.Kind {
|
||||||
|
return f.value.Kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the field to given value v. It returns an error if the field is not
|
||||||
|
// settable (not addressable or not exported) or if the given value's type
|
||||||
|
// doesn't match the fields type.
|
||||||
|
func (f *Field) Set(val interface{}) error {
|
||||||
|
// we can't set unexported fields, so be sure this field is exported
|
||||||
|
if !f.IsExported() {
|
||||||
|
return errNotExported
|
||||||
|
}
|
||||||
|
|
||||||
|
// do we get here? not sure...
|
||||||
|
if !f.value.CanSet() {
|
||||||
|
return errNotSettable
|
||||||
|
}
|
||||||
|
|
||||||
|
given := reflect.ValueOf(val)
|
||||||
|
|
||||||
|
if f.value.Kind() != given.Kind() {
|
||||||
|
return fmt.Errorf("wrong kind. got: %s want: %s", given.Kind(), f.value.Kind())
|
||||||
|
}
|
||||||
|
|
||||||
|
f.value.Set(given)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero sets the field to its zero value. It returns an error if the field is not
|
||||||
|
// settable (not addressable or not exported).
|
||||||
|
func (f *Field) Zero() error {
|
||||||
|
zero := reflect.Zero(f.value.Type()).Interface()
|
||||||
|
return f.Set(zero)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns a slice of Fields. This is particular handy to get the fields
|
||||||
|
// of a nested struct . A struct tag with the content of "-" ignores the
|
||||||
|
// checking of that particular field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field *http.Request `structs:"-"`
|
||||||
|
//
|
||||||
|
// It panics if field is not exported or if field's kind is not struct
|
||||||
|
func (f *Field) Fields() []*Field {
|
||||||
|
return getFields(f.value, f.defaultTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field returns the field from a nested struct. It panics if the nested struct
|
||||||
|
// is not exported or if the field was not found.
|
||||||
|
func (f *Field) Field(name string) *Field {
|
||||||
|
field, ok := f.FieldOk(name)
|
||||||
|
if !ok {
|
||||||
|
panic("field not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldOk returns the field from a nested struct. The boolean returns whether
|
||||||
|
// the field was found (true) or not (false).
|
||||||
|
func (f *Field) FieldOk(name string) (*Field, bool) {
|
||||||
|
value := &f.value
|
||||||
|
// value must be settable so we need to make sure it holds the address of the
|
||||||
|
// variable and not a copy, so we can pass the pointer to strctVal instead of a
|
||||||
|
// copy (which is not assigned to any variable, hence not settable).
|
||||||
|
// see "https://blog.golang.org/laws-of-reflection#TOC_8."
|
||||||
|
if f.value.Kind() != reflect.Ptr {
|
||||||
|
a := f.value.Addr()
|
||||||
|
value = &a
|
||||||
|
}
|
||||||
|
v := strctVal(value.Interface())
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
field, ok := t.FieldByName(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Field{
|
||||||
|
field: field,
|
||||||
|
value: v.FieldByName(name),
|
||||||
|
}, true
|
||||||
|
}
|
|
@ -0,0 +1,584 @@
|
||||||
|
// Package structs contains various utilities functions to work with structs.
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// DefaultTagName is the default tag name for struct fields which provides
|
||||||
|
// a more granular to tweak certain structs. Lookup the necessary functions
|
||||||
|
// for more info.
|
||||||
|
DefaultTagName = "structs" // struct's field default tag name
|
||||||
|
)
|
||||||
|
|
||||||
|
// Struct encapsulates a struct type to provide several high level functions
|
||||||
|
// around the struct.
|
||||||
|
type Struct struct {
|
||||||
|
raw interface{}
|
||||||
|
value reflect.Value
|
||||||
|
TagName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new *Struct with the struct s. It panics if the s's kind is
|
||||||
|
// not struct.
|
||||||
|
func New(s interface{}) *Struct {
|
||||||
|
return &Struct{
|
||||||
|
raw: s,
|
||||||
|
value: strctVal(s),
|
||||||
|
TagName: DefaultTagName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map converts the given struct to a map[string]interface{}, where the keys
|
||||||
|
// of the map are the field names and the values of the map the associated
|
||||||
|
// values of the fields. The default key string is the struct field name but
|
||||||
|
// can be changed in the struct field's tag value. The "structs" key in the
|
||||||
|
// struct's field tag value is the key name. Example:
|
||||||
|
//
|
||||||
|
// // Field appears in map as key "myName".
|
||||||
|
// Name string `structs:"myName"`
|
||||||
|
//
|
||||||
|
// A tag value with the content of "-" ignores that particular field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field bool `structs:"-"`
|
||||||
|
//
|
||||||
|
// A tag value with the content of "string" uses the stringer to get the value. Example:
|
||||||
|
//
|
||||||
|
// // The value will be output of Animal's String() func.
|
||||||
|
// // Map will panic if Animal does not implement String().
|
||||||
|
// Field *Animal `structs:"field,string"`
|
||||||
|
//
|
||||||
|
// A tag value with the option of "flatten" used in a struct field is to flatten its fields
|
||||||
|
// in the output map. Example:
|
||||||
|
//
|
||||||
|
// // The FieldStruct's fields will be flattened into the output map.
|
||||||
|
// FieldStruct time.Time `structs:",flatten"`
|
||||||
|
//
|
||||||
|
// A tag value with the option of "omitnested" stops iterating further if the type
|
||||||
|
// is a struct. Example:
|
||||||
|
//
|
||||||
|
// // Field is not processed further by this package.
|
||||||
|
// Field time.Time `structs:"myName,omitnested"`
|
||||||
|
// Field *http.Request `structs:",omitnested"`
|
||||||
|
//
|
||||||
|
// A tag value with the option of "omitempty" ignores that particular field if
|
||||||
|
// the field value is empty. Example:
|
||||||
|
//
|
||||||
|
// // Field appears in map as key "myName", but the field is
|
||||||
|
// // skipped if empty.
|
||||||
|
// Field string `structs:"myName,omitempty"`
|
||||||
|
//
|
||||||
|
// // Field appears in map as key "Field" (the default), but
|
||||||
|
// // the field is skipped if empty.
|
||||||
|
// Field string `structs:",omitempty"`
|
||||||
|
//
|
||||||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||||||
|
// fields will be neglected.
|
||||||
|
func (s *Struct) Map() map[string]interface{} {
|
||||||
|
out := make(map[string]interface{})
|
||||||
|
s.FillMap(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillMap is the same as Map. Instead of returning the output, it fills the
|
||||||
|
// given map.
|
||||||
|
func (s *Struct) FillMap(out map[string]interface{}) {
|
||||||
|
if out == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := s.structFields()
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
name := field.Name
|
||||||
|
val := s.value.FieldByName(name)
|
||||||
|
isSubStruct := false
|
||||||
|
var finalVal interface{}
|
||||||
|
|
||||||
|
tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||||
|
if tagName != "" {
|
||||||
|
name = tagName
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the value is a zero value and the field is marked as omitempty do
|
||||||
|
// not include
|
||||||
|
if tagOpts.Has("omitempty") {
|
||||||
|
zero := reflect.Zero(val.Type()).Interface()
|
||||||
|
current := val.Interface()
|
||||||
|
|
||||||
|
if reflect.DeepEqual(current, zero) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tagOpts.Has("omitnested") {
|
||||||
|
finalVal = s.nested(val)
|
||||||
|
|
||||||
|
v := reflect.ValueOf(val.Interface())
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Map, reflect.Struct:
|
||||||
|
isSubStruct = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalVal = val.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagOpts.Has("string") {
|
||||||
|
s, ok := val.Interface().(fmt.Stringer)
|
||||||
|
if ok {
|
||||||
|
out[name] = s.String()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if isSubStruct && (tagOpts.Has("flatten")) {
|
||||||
|
for k := range finalVal.(map[string]interface{}) {
|
||||||
|
out[k] = finalVal.(map[string]interface{})[k]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out[name] = finalVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values converts the given s struct's field values to a []interface{}. A
|
||||||
|
// struct tag with the content of "-" ignores the that particular field.
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field int `structs:"-"`
|
||||||
|
//
|
||||||
|
// A value with the option of "omitnested" stops iterating further if the type
|
||||||
|
// is a struct. Example:
|
||||||
|
//
|
||||||
|
// // Fields is not processed further by this package.
|
||||||
|
// Field time.Time `structs:",omitnested"`
|
||||||
|
// Field *http.Request `structs:",omitnested"`
|
||||||
|
//
|
||||||
|
// A tag value with the option of "omitempty" ignores that particular field and
|
||||||
|
// is not added to the values if the field value is empty. Example:
|
||||||
|
//
|
||||||
|
// // Field is skipped if empty
|
||||||
|
// Field string `structs:",omitempty"`
|
||||||
|
//
|
||||||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||||||
|
// fields will be neglected.
|
||||||
|
func (s *Struct) Values() []interface{} {
|
||||||
|
fields := s.structFields()
|
||||||
|
|
||||||
|
var t []interface{}
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
val := s.value.FieldByName(field.Name)
|
||||||
|
|
||||||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||||
|
|
||||||
|
// if the value is a zero value and the field is marked as omitempty do
|
||||||
|
// not include
|
||||||
|
if tagOpts.Has("omitempty") {
|
||||||
|
zero := reflect.Zero(val.Type()).Interface()
|
||||||
|
current := val.Interface()
|
||||||
|
|
||||||
|
if reflect.DeepEqual(current, zero) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tagOpts.Has("string") {
|
||||||
|
s, ok := val.Interface().(fmt.Stringer)
|
||||||
|
if ok {
|
||||||
|
t = append(t, s.String())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||||
|
// look out for embedded structs, and convert them to a
|
||||||
|
// []interface{} to be added to the final values slice
|
||||||
|
t = append(t, Values(val.Interface())...)
|
||||||
|
} else {
|
||||||
|
t = append(t, val.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns a slice of Fields. A struct tag with the content of "-"
|
||||||
|
// ignores the checking of that particular field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field bool `structs:"-"`
|
||||||
|
//
|
||||||
|
// It panics if s's kind is not struct.
|
||||||
|
func (s *Struct) Fields() []*Field {
|
||||||
|
return getFields(s.value, s.TagName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names returns a slice of field names. A struct tag with the content of "-"
|
||||||
|
// ignores the checking of that particular field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field bool `structs:"-"`
|
||||||
|
//
|
||||||
|
// It panics if s's kind is not struct.
|
||||||
|
func (s *Struct) Names() []string {
|
||||||
|
fields := getFields(s.value, s.TagName)
|
||||||
|
|
||||||
|
names := make([]string, len(fields))
|
||||||
|
|
||||||
|
for i, field := range fields {
|
||||||
|
names[i] = field.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFields(v reflect.Value, tagName string) []*Field {
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
t := v.Type()
|
||||||
|
|
||||||
|
var fields []*Field
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
|
||||||
|
if tag := field.Tag.Get(tagName); tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f := &Field{
|
||||||
|
field: field,
|
||||||
|
value: v.FieldByName(field.Name),
|
||||||
|
}
|
||||||
|
|
||||||
|
fields = append(fields, f)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field returns a new Field struct that provides several high level functions
|
||||||
|
// around a single struct field entity. It panics if the field is not found.
|
||||||
|
func (s *Struct) Field(name string) *Field {
|
||||||
|
f, ok := s.FieldOk(name)
|
||||||
|
if !ok {
|
||||||
|
panic("field not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldOk returns a new Field struct that provides several high level functions
|
||||||
|
// around a single struct field entity. The boolean returns true if the field
|
||||||
|
// was found.
|
||||||
|
func (s *Struct) FieldOk(name string) (*Field, bool) {
|
||||||
|
t := s.value.Type()
|
||||||
|
|
||||||
|
field, ok := t.FieldByName(name)
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Field{
|
||||||
|
field: field,
|
||||||
|
value: s.value.FieldByName(name),
|
||||||
|
defaultTag: s.TagName,
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns true if all fields in a struct is a zero value (not
|
||||||
|
// initialized) A struct tag with the content of "-" ignores the checking of
|
||||||
|
// that particular field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field bool `structs:"-"`
|
||||||
|
//
|
||||||
|
// A value with the option of "omitnested" stops iterating further if the type
|
||||||
|
// is a struct. Example:
|
||||||
|
//
|
||||||
|
// // Field is not processed further by this package.
|
||||||
|
// Field time.Time `structs:"myName,omitnested"`
|
||||||
|
// Field *http.Request `structs:",omitnested"`
|
||||||
|
//
|
||||||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||||||
|
// fields will be neglected. It panics if s's kind is not struct.
|
||||||
|
func (s *Struct) IsZero() bool {
|
||||||
|
fields := s.structFields()
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
val := s.value.FieldByName(field.Name)
|
||||||
|
|
||||||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||||
|
|
||||||
|
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||||
|
ok := IsZero(val.Interface())
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero value of the given field, such as "" for string, 0 for int
|
||||||
|
zero := reflect.Zero(val.Type()).Interface()
|
||||||
|
|
||||||
|
// current value of the given field
|
||||||
|
current := val.Interface()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(current, zero) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasZero returns true if a field in a struct is not initialized (zero value).
|
||||||
|
// A struct tag with the content of "-" ignores the checking of that particular
|
||||||
|
// field. Example:
|
||||||
|
//
|
||||||
|
// // Field is ignored by this package.
|
||||||
|
// Field bool `structs:"-"`
|
||||||
|
//
|
||||||
|
// A value with the option of "omitnested" stops iterating further if the type
|
||||||
|
// is a struct. Example:
|
||||||
|
//
|
||||||
|
// // Field is not processed further by this package.
|
||||||
|
// Field time.Time `structs:"myName,omitnested"`
|
||||||
|
// Field *http.Request `structs:",omitnested"`
|
||||||
|
//
|
||||||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||||||
|
// fields will be neglected. It panics if s's kind is not struct.
|
||||||
|
func (s *Struct) HasZero() bool {
|
||||||
|
fields := s.structFields()
|
||||||
|
|
||||||
|
for _, field := range fields {
|
||||||
|
val := s.value.FieldByName(field.Name)
|
||||||
|
|
||||||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||||||
|
|
||||||
|
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||||||
|
ok := HasZero(val.Interface())
|
||||||
|
if ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// zero value of the given field, such as "" for string, 0 for int
|
||||||
|
zero := reflect.Zero(val.Type()).Interface()
|
||||||
|
|
||||||
|
// current value of the given field
|
||||||
|
current := val.Interface()
|
||||||
|
|
||||||
|
if reflect.DeepEqual(current, zero) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the structs's type name within its package. For more info refer
|
||||||
|
// to Name() function.
|
||||||
|
func (s *Struct) Name() string {
|
||||||
|
return s.value.Type().Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// structFields returns the exported struct fields for a given s struct. This
|
||||||
|
// is a convenient helper method to avoid duplicate code in some of the
|
||||||
|
// functions.
|
||||||
|
func (s *Struct) structFields() []reflect.StructField {
|
||||||
|
t := s.value.Type()
|
||||||
|
|
||||||
|
var f []reflect.StructField
|
||||||
|
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
field := t.Field(i)
|
||||||
|
// we can't access the value of unexported fields
|
||||||
|
if field.PkgPath != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't check if it's omitted
|
||||||
|
if tag := field.Tag.Get(s.TagName); tag == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f = append(f, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func strctVal(s interface{}) reflect.Value {
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
|
||||||
|
// if pointer get the underlying element≤
|
||||||
|
for v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.Kind() != reflect.Struct {
|
||||||
|
panic("not struct")
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map converts the given struct to a map[string]interface{}. For more info
|
||||||
|
// refer to Struct types Map() method. It panics if s's kind is not struct.
|
||||||
|
func Map(s interface{}) map[string]interface{} {
|
||||||
|
return New(s).Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FillMap is the same as Map. Instead of returning the output, it fills the
|
||||||
|
// given map.
|
||||||
|
func FillMap(s interface{}, out map[string]interface{}) {
|
||||||
|
New(s).FillMap(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values converts the given struct to a []interface{}. For more info refer to
|
||||||
|
// Struct types Values() method. It panics if s's kind is not struct.
|
||||||
|
func Values(s interface{}) []interface{} {
|
||||||
|
return New(s).Values()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fields returns a slice of *Field. For more info refer to Struct types
|
||||||
|
// Fields() method. It panics if s's kind is not struct.
|
||||||
|
func Fields(s interface{}) []*Field {
|
||||||
|
return New(s).Fields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Names returns a slice of field names. For more info refer to Struct types
|
||||||
|
// Names() method. It panics if s's kind is not struct.
|
||||||
|
func Names(s interface{}) []string {
|
||||||
|
return New(s).Names()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns true if all fields is equal to a zero value. For more info
|
||||||
|
// refer to Struct types IsZero() method. It panics if s's kind is not struct.
|
||||||
|
func IsZero(s interface{}) bool {
|
||||||
|
return New(s).IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasZero returns true if any field is equal to a zero value. For more info
|
||||||
|
// refer to Struct types HasZero() method. It panics if s's kind is not struct.
|
||||||
|
func HasZero(s interface{}) bool {
|
||||||
|
return New(s).HasZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsStruct returns true if the given variable is a struct or a pointer to
|
||||||
|
// struct.
|
||||||
|
func IsStruct(s interface{}) bool {
|
||||||
|
v := reflect.ValueOf(s)
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// uninitialized zero value of a struct
|
||||||
|
if v.Kind() == reflect.Invalid {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the structs's type name within its package. It returns an
|
||||||
|
// empty string for unnamed types. It panics if s's kind is not struct.
|
||||||
|
func Name(s interface{}) string {
|
||||||
|
return New(s).Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// nested retrieves recursively all types for the given value and returns the
|
||||||
|
// nested value.
|
||||||
|
func (s *Struct) nested(val reflect.Value) interface{} {
|
||||||
|
var finalVal interface{}
|
||||||
|
|
||||||
|
v := reflect.ValueOf(val.Interface())
|
||||||
|
if v.Kind() == reflect.Ptr {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Struct:
|
||||||
|
n := New(val.Interface())
|
||||||
|
n.TagName = s.TagName
|
||||||
|
m := n.Map()
|
||||||
|
|
||||||
|
// do not add the converted value if there are no exported fields, ie:
|
||||||
|
// time.Time
|
||||||
|
if len(m) == 0 {
|
||||||
|
finalVal = val.Interface()
|
||||||
|
} else {
|
||||||
|
finalVal = m
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
// get the element type of the map
|
||||||
|
mapElem := val.Type()
|
||||||
|
switch val.Type().Kind() {
|
||||||
|
case reflect.Ptr, reflect.Array, reflect.Map,
|
||||||
|
reflect.Slice, reflect.Chan:
|
||||||
|
mapElem = val.Type().Elem()
|
||||||
|
if mapElem.Kind() == reflect.Ptr {
|
||||||
|
mapElem = mapElem.Elem()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// only iterate over struct types, ie: map[string]StructType,
|
||||||
|
// map[string][]StructType,
|
||||||
|
if mapElem.Kind() == reflect.Struct ||
|
||||||
|
(mapElem.Kind() == reflect.Slice &&
|
||||||
|
mapElem.Elem().Kind() == reflect.Struct) {
|
||||||
|
m := make(map[string]interface{}, val.Len())
|
||||||
|
for _, k := range val.MapKeys() {
|
||||||
|
m[k.String()] = s.nested(val.MapIndex(k))
|
||||||
|
}
|
||||||
|
finalVal = m
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(arslan): should this be optional?
|
||||||
|
finalVal = val.Interface()
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
if val.Type().Kind() == reflect.Interface {
|
||||||
|
finalVal = val.Interface()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(arslan): should this be optional?
|
||||||
|
// do not iterate of non struct types, just pass the value. Ie: []int,
|
||||||
|
// []string, co... We only iterate further if it's a struct.
|
||||||
|
// i.e []foo or []*foo
|
||||||
|
if val.Type().Elem().Kind() != reflect.Struct &&
|
||||||
|
!(val.Type().Elem().Kind() == reflect.Ptr &&
|
||||||
|
val.Type().Elem().Elem().Kind() == reflect.Struct) {
|
||||||
|
finalVal = val.Interface()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
slices := make([]interface{}, val.Len())
|
||||||
|
for x := 0; x < val.Len(); x++ {
|
||||||
|
slices[x] = s.nested(val.Index(x))
|
||||||
|
}
|
||||||
|
finalVal = slices
|
||||||
|
default:
|
||||||
|
finalVal = val.Interface()
|
||||||
|
}
|
||||||
|
|
||||||
|
return finalVal
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
// tagOptions contains a slice of tag options
|
||||||
|
type tagOptions []string
|
||||||
|
|
||||||
|
// Has returns true if the given option is available in tagOptions
|
||||||
|
func (t tagOptions) Has(opt string) bool {
|
||||||
|
for _, tagOpt := range t {
|
||||||
|
if tagOpt == opt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseTag splits a struct field's tag into its name and a list of options
|
||||||
|
// which comes after a name. A tag is in the form of: "name,option1,option2".
|
||||||
|
// The name can be neglectected.
|
||||||
|
func parseTag(tag string) (string, tagOptions) {
|
||||||
|
// tag is one of followings:
|
||||||
|
// ""
|
||||||
|
// "name"
|
||||||
|
// "name,opt"
|
||||||
|
// "name,opt,opt2"
|
||||||
|
// ",opt"
|
||||||
|
|
||||||
|
res := strings.Split(tag, ",")
|
||||||
|
return res[0], res[1:]
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
freebsd_task:
|
||||||
|
name: 'FreeBSD'
|
||||||
|
freebsd_instance:
|
||||||
|
image_family: freebsd-13-2
|
||||||
|
install_script:
|
||||||
|
- pkg update -f
|
||||||
|
- pkg install -y go
|
||||||
|
test_script:
|
||||||
|
# run tests as user "cirrus" instead of root
|
||||||
|
- pw useradd cirrus -m
|
||||||
|
- chown -R cirrus:cirrus .
|
||||||
|
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
|
||||||
|
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*.go]
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = 4
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.{yml,yaml}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
|
@ -0,0 +1 @@
|
||||||
|
go.sum linguist-generated
|
|
@ -0,0 +1,7 @@
|
||||||
|
# go test -c output
|
||||||
|
*.test
|
||||||
|
*.test.exe
|
||||||
|
|
||||||
|
# Output of go build ./cmd/fsnotify
|
||||||
|
/fsnotify
|
||||||
|
/fsnotify.exe
|
|
@ -0,0 +1,2 @@
|
||||||
|
Chris Howey <howeyc@gmail.com> <chris@howey.me>
|
||||||
|
Nathan Youngman <git@nathany.com> <4566+nathany@users.noreply.github.com>
|
|
@ -0,0 +1,541 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
Unreleased
|
||||||
|
----------
|
||||||
|
Nothing yet.
|
||||||
|
|
||||||
|
1.7.0 - 2023-10-22
|
||||||
|
------------------
|
||||||
|
This version of fsnotify needs Go 1.17.
|
||||||
|
|
||||||
|
### Additions
|
||||||
|
|
||||||
|
- illumos: add FEN backend to support illumos and Solaris. ([#371])
|
||||||
|
|
||||||
|
- all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful
|
||||||
|
in cases where you can't control the kernel buffer and receive a large number
|
||||||
|
of events in bursts. ([#550], [#572])
|
||||||
|
|
||||||
|
- all: add `AddWith()`, which is identical to `Add()` but allows passing
|
||||||
|
options. ([#521])
|
||||||
|
|
||||||
|
- windows: allow setting the ReadDirectoryChangesW() buffer size with
|
||||||
|
`fsnotify.WithBufferSize()`; the default of 64K is the highest value that
|
||||||
|
works on all platforms and is enough for most purposes, but in some cases a
|
||||||
|
highest buffer is needed. ([#521])
|
||||||
|
|
||||||
|
### Changes and fixes
|
||||||
|
|
||||||
|
- inotify: remove watcher if a watched path is renamed ([#518])
|
||||||
|
|
||||||
|
After a rename the reported name wasn't updated, or even an empty string.
|
||||||
|
Inotify doesn't provide any good facilities to update it, so just remove the
|
||||||
|
watcher. This is already how it worked on kqueue and FEN.
|
||||||
|
|
||||||
|
On Windows this does work, and remains working.
|
||||||
|
|
||||||
|
- windows: don't listen for file attribute changes ([#520])
|
||||||
|
|
||||||
|
File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API,
|
||||||
|
with no way to see if they're a file write or attribute change, so would show
|
||||||
|
up as a fsnotify.Write event. This is never useful, and could result in many
|
||||||
|
spurious Write events.
|
||||||
|
|
||||||
|
- windows: return `ErrEventOverflow` if the buffer is full ([#525])
|
||||||
|
|
||||||
|
Before it would merely return "short read", making it hard to detect this
|
||||||
|
error.
|
||||||
|
|
||||||
|
- kqueue: make sure events for all files are delivered properly when removing a
|
||||||
|
watched directory ([#526])
|
||||||
|
|
||||||
|
Previously they would get sent with `""` (empty string) or `"."` as the path
|
||||||
|
name.
|
||||||
|
|
||||||
|
- kqueue: don't emit spurious Create events for symbolic links ([#524])
|
||||||
|
|
||||||
|
The link would get resolved but kqueue would "forget" it already saw the link
|
||||||
|
itself, resulting on a Create for every Write event for the directory.
|
||||||
|
|
||||||
|
- all: return `ErrClosed` on `Add()` when the watcher is closed ([#516])
|
||||||
|
|
||||||
|
- other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in
|
||||||
|
`backend_other.go`, making it easier to use on unsupported platforms such as
|
||||||
|
WASM, AIX, etc. ([#528])
|
||||||
|
|
||||||
|
- other: use the `backend_other.go` no-op if the `appengine` build tag is set;
|
||||||
|
Google AppEngine forbids usage of the unsafe package so the inotify backend
|
||||||
|
won't compile there.
|
||||||
|
|
||||||
|
[#371]: https://github.com/fsnotify/fsnotify/pull/371
|
||||||
|
[#516]: https://github.com/fsnotify/fsnotify/pull/516
|
||||||
|
[#518]: https://github.com/fsnotify/fsnotify/pull/518
|
||||||
|
[#520]: https://github.com/fsnotify/fsnotify/pull/520
|
||||||
|
[#521]: https://github.com/fsnotify/fsnotify/pull/521
|
||||||
|
[#524]: https://github.com/fsnotify/fsnotify/pull/524
|
||||||
|
[#525]: https://github.com/fsnotify/fsnotify/pull/525
|
||||||
|
[#526]: https://github.com/fsnotify/fsnotify/pull/526
|
||||||
|
[#528]: https://github.com/fsnotify/fsnotify/pull/528
|
||||||
|
[#537]: https://github.com/fsnotify/fsnotify/pull/537
|
||||||
|
[#550]: https://github.com/fsnotify/fsnotify/pull/550
|
||||||
|
[#572]: https://github.com/fsnotify/fsnotify/pull/572
|
||||||
|
|
||||||
|
1.6.0 - 2022-10-13
|
||||||
|
------------------
|
||||||
|
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
|
||||||
|
but not documented). It also increases the minimum Linux version to 2.6.32.
|
||||||
|
|
||||||
|
### Additions
|
||||||
|
|
||||||
|
- all: add `Event.Has()` and `Op.Has()` ([#477])
|
||||||
|
|
||||||
|
This makes checking events a lot easier; for example:
|
||||||
|
|
||||||
|
if event.Op&Write == Write && !(event.Op&Remove == Remove) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Becomes:
|
||||||
|
|
||||||
|
if event.Has(Write) && !event.Has(Remove) {
|
||||||
|
}
|
||||||
|
|
||||||
|
- all: add cmd/fsnotify ([#463])
|
||||||
|
|
||||||
|
A command-line utility for testing and some examples.
|
||||||
|
|
||||||
|
### Changes and fixes
|
||||||
|
|
||||||
|
- inotify: don't ignore events for files that don't exist ([#260], [#470])
|
||||||
|
|
||||||
|
Previously the inotify watcher would call `os.Lstat()` to check if a file
|
||||||
|
still exists before emitting events.
|
||||||
|
|
||||||
|
This was inconsistent with other platforms and resulted in inconsistent event
|
||||||
|
reporting (e.g. when a file is quickly removed and re-created), and generally
|
||||||
|
a source of confusion. It was added in 2013 to fix a memory leak that no
|
||||||
|
longer exists.
|
||||||
|
|
||||||
|
- all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's
|
||||||
|
not watched ([#460])
|
||||||
|
|
||||||
|
- inotify: replace epoll() with non-blocking inotify ([#434])
|
||||||
|
|
||||||
|
Non-blocking inotify was not generally available at the time this library was
|
||||||
|
written in 2014, but now it is. As a result, the minimum Linux version is
|
||||||
|
bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster.
|
||||||
|
|
||||||
|
- kqueue: don't check for events every 100ms ([#480])
|
||||||
|
|
||||||
|
The watcher would wake up every 100ms, even when there was nothing to do. Now
|
||||||
|
it waits until there is something to do.
|
||||||
|
|
||||||
|
- macos: retry opening files on EINTR ([#475])
|
||||||
|
|
||||||
|
- kqueue: skip unreadable files ([#479])
|
||||||
|
|
||||||
|
kqueue requires a file descriptor for every file in a directory; this would
|
||||||
|
fail if a file was unreadable by the current user. Now these files are simply
|
||||||
|
skipped.
|
||||||
|
|
||||||
|
- windows: fix renaming a watched directory if the parent is also watched ([#370])
|
||||||
|
|
||||||
|
- windows: increase buffer size from 4K to 64K ([#485])
|
||||||
|
|
||||||
|
- windows: close file handle on Remove() ([#288])
|
||||||
|
|
||||||
|
- kqueue: put pathname in the error if watching a file fails ([#471])
|
||||||
|
|
||||||
|
- inotify, windows: calling Close() more than once could race ([#465])
|
||||||
|
|
||||||
|
- kqueue: improve Close() performance ([#233])
|
||||||
|
|
||||||
|
- all: various documentation additions and clarifications.
|
||||||
|
|
||||||
|
[#233]: https://github.com/fsnotify/fsnotify/pull/233
|
||||||
|
[#260]: https://github.com/fsnotify/fsnotify/pull/260
|
||||||
|
[#288]: https://github.com/fsnotify/fsnotify/pull/288
|
||||||
|
[#370]: https://github.com/fsnotify/fsnotify/pull/370
|
||||||
|
[#434]: https://github.com/fsnotify/fsnotify/pull/434
|
||||||
|
[#460]: https://github.com/fsnotify/fsnotify/pull/460
|
||||||
|
[#463]: https://github.com/fsnotify/fsnotify/pull/463
|
||||||
|
[#465]: https://github.com/fsnotify/fsnotify/pull/465
|
||||||
|
[#470]: https://github.com/fsnotify/fsnotify/pull/470
|
||||||
|
[#471]: https://github.com/fsnotify/fsnotify/pull/471
|
||||||
|
[#475]: https://github.com/fsnotify/fsnotify/pull/475
|
||||||
|
[#477]: https://github.com/fsnotify/fsnotify/pull/477
|
||||||
|
[#479]: https://github.com/fsnotify/fsnotify/pull/479
|
||||||
|
[#480]: https://github.com/fsnotify/fsnotify/pull/480
|
||||||
|
[#485]: https://github.com/fsnotify/fsnotify/pull/485
|
||||||
|
|
||||||
|
## [1.5.4] - 2022-04-25
|
||||||
|
|
||||||
|
* Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447)
|
||||||
|
* go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444)
|
||||||
|
* Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443)
|
||||||
|
|
||||||
|
## [1.5.3] - 2022-04-22
|
||||||
|
|
||||||
|
* This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445)
|
||||||
|
|
||||||
|
## [1.5.2] - 2022-04-21
|
||||||
|
|
||||||
|
* Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374)
|
||||||
|
* Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361)
|
||||||
|
* Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424)
|
||||||
|
* Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406)
|
||||||
|
* fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416)
|
||||||
|
|
||||||
|
## [1.5.1] - 2021-08-24
|
||||||
|
|
||||||
|
* Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394)
|
||||||
|
|
||||||
|
## [1.5.0] - 2021-08-20
|
||||||
|
|
||||||
|
* Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381)
|
||||||
|
* Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298)
|
||||||
|
* Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289)
|
||||||
|
* CI: Use GitHub Actions for CI and cover go 1.12-1.17
|
||||||
|
[#378](https://github.com/fsnotify/fsnotify/pull/378)
|
||||||
|
[#381](https://github.com/fsnotify/fsnotify/pull/381)
|
||||||
|
[#385](https://github.com/fsnotify/fsnotify/pull/385)
|
||||||
|
* Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325)
|
||||||
|
|
||||||
|
## [1.4.9] - 2020-03-11
|
||||||
|
|
||||||
|
* Move example usage to the readme #329. This may resolve #328.
|
||||||
|
|
||||||
|
## [1.4.8] - 2020-03-10
|
||||||
|
|
||||||
|
* CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216)
|
||||||
|
* Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265)
|
||||||
|
* Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266)
|
||||||
|
* CI: Less verbosity (@nathany #267)
|
||||||
|
* Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267)
|
||||||
|
* Tests: Check if channels are closed in the example (@alexeykazakov #244)
|
||||||
|
* CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284)
|
||||||
|
* CI: Add windows to travis matrix (@cpuguy83 #284)
|
||||||
|
* Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93)
|
||||||
|
* Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219)
|
||||||
|
* Linux: open files with close-on-exec (@linxiulei #273)
|
||||||
|
* Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 )
|
||||||
|
* Project: Add go.mod (@nathany #309)
|
||||||
|
* Project: Revise editor config (@nathany #309)
|
||||||
|
* Project: Update copyright for 2019 (@nathany #309)
|
||||||
|
* CI: Drop go1.8 from CI matrix (@nathany #309)
|
||||||
|
* Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e )
|
||||||
|
|
||||||
|
## [1.4.7] - 2018-01-09
|
||||||
|
|
||||||
|
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
|
||||||
|
* Tests: Fix missing verb on format string (thanks @rchiossi)
|
||||||
|
* Linux: Fix deadlock in Remove (thanks @aarondl)
|
||||||
|
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
|
||||||
|
* Docs: Moved FAQ into the README (thanks @vahe)
|
||||||
|
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
|
||||||
|
* Docs: replace references to OS X with macOS
|
||||||
|
|
||||||
|
## [1.4.2] - 2016-10-10
|
||||||
|
|
||||||
|
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
|
||||||
|
|
||||||
|
## [1.4.1] - 2016-10-04
|
||||||
|
|
||||||
|
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack)
|
||||||
|
|
||||||
|
## [1.4.0] - 2016-10-01
|
||||||
|
|
||||||
|
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie)
|
||||||
|
|
||||||
|
## [1.3.1] - 2016-06-28
|
||||||
|
|
||||||
|
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc)
|
||||||
|
|
||||||
|
## [1.3.0] - 2016-04-19
|
||||||
|
|
||||||
|
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135)
|
||||||
|
|
||||||
|
## [1.2.10] - 2016-03-02
|
||||||
|
|
||||||
|
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj)
|
||||||
|
|
||||||
|
## [1.2.9] - 2016-01-13
|
||||||
|
|
||||||
|
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep)
|
||||||
|
|
||||||
|
## [1.2.8] - 2015-12-17
|
||||||
|
|
||||||
|
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test)
|
||||||
|
* inotify: fix race in test
|
||||||
|
* enable race detection for continuous integration (Linux, Mac, Windows)
|
||||||
|
|
||||||
|
## [1.2.5] - 2015-10-17
|
||||||
|
|
||||||
|
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki)
|
||||||
|
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken)
|
||||||
|
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie)
|
||||||
|
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion)
|
||||||
|
|
||||||
|
## [1.2.1] - 2015-10-14
|
||||||
|
|
||||||
|
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx)
|
||||||
|
|
||||||
|
## [1.2.0] - 2015-02-08
|
||||||
|
|
||||||
|
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD)
|
||||||
|
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD)
|
||||||
|
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59)
|
||||||
|
|
||||||
|
## [1.1.1] - 2015-02-05
|
||||||
|
|
||||||
|
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD)
|
||||||
|
|
||||||
|
## [1.1.0] - 2014-12-12
|
||||||
|
|
||||||
|
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43)
|
||||||
|
* add low-level functions
|
||||||
|
* only need to store flags on directories
|
||||||
|
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13)
|
||||||
|
* done can be an unbuffered channel
|
||||||
|
* remove calls to os.NewSyscallError
|
||||||
|
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher)
|
||||||
|
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48)
|
||||||
|
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
|
||||||
|
|
||||||
|
## [1.0.4] - 2014-09-07
|
||||||
|
|
||||||
|
* kqueue: add dragonfly to the build tags.
|
||||||
|
* Rename source code files, rearrange code so exported APIs are at the top.
|
||||||
|
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang)
|
||||||
|
|
||||||
|
## [1.0.3] - 2014-08-19
|
||||||
|
|
||||||
|
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36)
|
||||||
|
|
||||||
|
## [1.0.2] - 2014-08-17
|
||||||
|
|
||||||
|
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
|
||||||
|
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
|
||||||
|
|
||||||
|
## [1.0.0] - 2014-08-15
|
||||||
|
|
||||||
|
* [API] Remove AddWatch on Windows, use Add.
|
||||||
|
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30)
|
||||||
|
* Minor updates based on feedback from golint.
|
||||||
|
|
||||||
|
## dev / 2014-07-09
|
||||||
|
|
||||||
|
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify).
|
||||||
|
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno)
|
||||||
|
|
||||||
|
## dev / 2014-07-04
|
||||||
|
|
||||||
|
* kqueue: fix incorrect mutex used in Close()
|
||||||
|
* Update example to demonstrate usage of Op.
|
||||||
|
|
||||||
|
## dev / 2014-06-28
|
||||||
|
|
||||||
|
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4)
|
||||||
|
* Fix for String() method on Event (thanks Alex Brainman)
|
||||||
|
* Don't build on Plan 9 or Solaris (thanks @4ad)
|
||||||
|
|
||||||
|
## dev / 2014-06-21
|
||||||
|
|
||||||
|
* Events channel of type Event rather than *Event.
|
||||||
|
* [internal] use syscall constants directly for inotify and kqueue.
|
||||||
|
* [internal] kqueue: rename events to kevents and fileEvent to event.
|
||||||
|
|
||||||
|
## dev / 2014-06-19
|
||||||
|
|
||||||
|
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally).
|
||||||
|
* [internal] remove cookie from Event struct (unused).
|
||||||
|
* [internal] Event struct has the same definition across every OS.
|
||||||
|
* [internal] remove internal watch and removeWatch methods.
|
||||||
|
|
||||||
|
## dev / 2014-06-12
|
||||||
|
|
||||||
|
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove().
|
||||||
|
* [API] Pluralized channel names: Events and Errors.
|
||||||
|
* [API] Renamed FileEvent struct to Event.
|
||||||
|
* [API] Op constants replace methods like IsCreate().
|
||||||
|
|
||||||
|
## dev / 2014-06-12
|
||||||
|
|
||||||
|
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
|
||||||
|
|
||||||
|
## dev / 2014-05-23
|
||||||
|
|
||||||
|
* [API] Remove current implementation of WatchFlags.
|
||||||
|
* current implementation doesn't take advantage of OS for efficiency
|
||||||
|
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes
|
||||||
|
* no tests for the current implementation
|
||||||
|
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
|
||||||
|
|
||||||
|
## [0.9.3] - 2014-12-31
|
||||||
|
|
||||||
|
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
|
||||||
|
|
||||||
|
## [0.9.2] - 2014-08-17
|
||||||
|
|
||||||
|
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
|
||||||
|
|
||||||
|
## [0.9.1] - 2014-06-12
|
||||||
|
|
||||||
|
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
|
||||||
|
|
||||||
|
## [0.9.0] - 2014-01-17
|
||||||
|
|
||||||
|
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
|
||||||
|
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
|
||||||
|
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
|
||||||
|
|
||||||
|
## [0.8.12] - 2013-11-13
|
||||||
|
|
||||||
|
* [API] Remove FD_SET and friends from Linux adapter
|
||||||
|
|
||||||
|
## [0.8.11] - 2013-11-02
|
||||||
|
|
||||||
|
* [Doc] Add Changelog [#72][] (thanks @nathany)
|
||||||
|
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
|
||||||
|
|
||||||
|
## [0.8.10] - 2013-10-19
|
||||||
|
|
||||||
|
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
|
||||||
|
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
|
||||||
|
* [Doc] specify OS-specific limits in README (thanks @debrando)
|
||||||
|
|
||||||
|
## [0.8.9] - 2013-09-08
|
||||||
|
|
||||||
|
* [Doc] Contributing (thanks @nathany)
|
||||||
|
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
|
||||||
|
* [Doc] GoCI badge in README (Linux only) [#60][]
|
||||||
|
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
|
||||||
|
|
||||||
|
## [0.8.8] - 2013-06-17
|
||||||
|
|
||||||
|
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
|
||||||
|
|
||||||
|
## [0.8.7] - 2013-06-03
|
||||||
|
|
||||||
|
* [API] Make syscall flags internal
|
||||||
|
* [Fix] inotify: ignore event changes
|
||||||
|
* [Fix] race in symlink test [#45][] (reported by @srid)
|
||||||
|
* [Fix] tests on Windows
|
||||||
|
* lower case error messages
|
||||||
|
|
||||||
|
## [0.8.6] - 2013-05-23
|
||||||
|
|
||||||
|
* kqueue: Use EVT_ONLY flag on Darwin
|
||||||
|
* [Doc] Update README with full example
|
||||||
|
|
||||||
|
## [0.8.5] - 2013-05-09
|
||||||
|
|
||||||
|
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
|
||||||
|
|
||||||
|
## [0.8.4] - 2013-04-07
|
||||||
|
|
||||||
|
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
|
||||||
|
|
||||||
|
## [0.8.3] - 2013-03-13
|
||||||
|
|
||||||
|
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
|
||||||
|
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
|
||||||
|
|
||||||
|
## [0.8.2] - 2013-02-07
|
||||||
|
|
||||||
|
* [Doc] add Authors
|
||||||
|
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
|
||||||
|
|
||||||
|
## [0.8.1] - 2013-01-09
|
||||||
|
|
||||||
|
* [Fix] Windows path separators
|
||||||
|
* [Doc] BSD License
|
||||||
|
|
||||||
|
## [0.8.0] - 2012-11-09
|
||||||
|
|
||||||
|
* kqueue: directory watching improvements (thanks @vmirage)
|
||||||
|
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
|
||||||
|
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
|
||||||
|
|
||||||
|
## [0.7.4] - 2012-10-09
|
||||||
|
|
||||||
|
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
|
||||||
|
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
|
||||||
|
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
|
||||||
|
* [Fix] kqueue: modify after recreation of file
|
||||||
|
|
||||||
|
## [0.7.3] - 2012-09-27
|
||||||
|
|
||||||
|
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
|
||||||
|
* [Fix] kqueue: no longer get duplicate CREATE events
|
||||||
|
|
||||||
|
## [0.7.2] - 2012-09-01
|
||||||
|
|
||||||
|
* kqueue: events for created directories
|
||||||
|
|
||||||
|
## [0.7.1] - 2012-07-14
|
||||||
|
|
||||||
|
* [Fix] for renaming files
|
||||||
|
|
||||||
|
## [0.7.0] - 2012-07-02
|
||||||
|
|
||||||
|
* [Feature] FSNotify flags
|
||||||
|
* [Fix] inotify: Added file name back to event path
|
||||||
|
|
||||||
|
## [0.6.0] - 2012-06-06
|
||||||
|
|
||||||
|
* kqueue: watch files after directory created (thanks @tmc)
|
||||||
|
|
||||||
|
## [0.5.1] - 2012-05-22
|
||||||
|
|
||||||
|
* [Fix] inotify: remove all watches before Close()
|
||||||
|
|
||||||
|
## [0.5.0] - 2012-05-03
|
||||||
|
|
||||||
|
* [API] kqueue: return errors during watch instead of sending over channel
|
||||||
|
* kqueue: match symlink behavior on Linux
|
||||||
|
* inotify: add `DELETE_SELF` (requested by @taralx)
|
||||||
|
* [Fix] kqueue: handle EINTR (reported by @robfig)
|
||||||
|
* [Doc] Godoc example [#1][] (thanks @davecheney)
|
||||||
|
|
||||||
|
## [0.4.0] - 2012-03-30
|
||||||
|
|
||||||
|
* Go 1 released: build with go tool
|
||||||
|
* [Feature] Windows support using winfsnotify
|
||||||
|
* Windows does not have attribute change notifications
|
||||||
|
* Roll attribute notifications into IsModify
|
||||||
|
|
||||||
|
## [0.3.0] - 2012-02-19
|
||||||
|
|
||||||
|
* kqueue: add files when watch directory
|
||||||
|
|
||||||
|
## [0.2.0] - 2011-12-30
|
||||||
|
|
||||||
|
* update to latest Go weekly code
|
||||||
|
|
||||||
|
## [0.1.0] - 2011-10-19
|
||||||
|
|
||||||
|
* kqueue: add watch on file creation to match inotify
|
||||||
|
* kqueue: create file event
|
||||||
|
* inotify: ignore `IN_IGNORED` events
|
||||||
|
* event String()
|
||||||
|
* linux: common FileEvent functions
|
||||||
|
* initial commit
|
||||||
|
|
||||||
|
[#79]: https://github.com/howeyc/fsnotify/pull/79
|
||||||
|
[#77]: https://github.com/howeyc/fsnotify/pull/77
|
||||||
|
[#72]: https://github.com/howeyc/fsnotify/issues/72
|
||||||
|
[#71]: https://github.com/howeyc/fsnotify/issues/71
|
||||||
|
[#70]: https://github.com/howeyc/fsnotify/issues/70
|
||||||
|
[#63]: https://github.com/howeyc/fsnotify/issues/63
|
||||||
|
[#62]: https://github.com/howeyc/fsnotify/issues/62
|
||||||
|
[#60]: https://github.com/howeyc/fsnotify/issues/60
|
||||||
|
[#59]: https://github.com/howeyc/fsnotify/issues/59
|
||||||
|
[#49]: https://github.com/howeyc/fsnotify/issues/49
|
||||||
|
[#45]: https://github.com/howeyc/fsnotify/issues/45
|
||||||
|
[#40]: https://github.com/howeyc/fsnotify/issues/40
|
||||||
|
[#36]: https://github.com/howeyc/fsnotify/issues/36
|
||||||
|
[#33]: https://github.com/howeyc/fsnotify/issues/33
|
||||||
|
[#29]: https://github.com/howeyc/fsnotify/issues/29
|
||||||
|
[#25]: https://github.com/howeyc/fsnotify/issues/25
|
||||||
|
[#24]: https://github.com/howeyc/fsnotify/issues/24
|
||||||
|
[#21]: https://github.com/howeyc/fsnotify/issues/21
|
|
@ -0,0 +1,26 @@
|
||||||
|
Thank you for your interest in contributing to fsnotify! We try to review and
|
||||||
|
merge PRs in a reasonable timeframe, but please be aware that:
|
||||||
|
|
||||||
|
- To avoid "wasted" work, please discus changes on the issue tracker first. You
|
||||||
|
can just send PRs, but they may end up being rejected for one reason or the
|
||||||
|
other.
|
||||||
|
|
||||||
|
- fsnotify is a cross-platform library, and changes must work reasonably well on
|
||||||
|
all supported platforms.
|
||||||
|
|
||||||
|
- Changes will need to be compatible; old code should still compile, and the
|
||||||
|
runtime behaviour can't change in ways that are likely to lead to problems for
|
||||||
|
users.
|
||||||
|
|
||||||
|
Testing
|
||||||
|
-------
|
||||||
|
Just `go test ./...` runs all the tests; the CI runs this on all supported
|
||||||
|
platforms. Testing different platforms locally can be done with something like
|
||||||
|
[goon] or [Vagrant], but this isn't super-easy to set up at the moment.
|
||||||
|
|
||||||
|
Use the `-short` flag to make the "stress test" run faster.
|
||||||
|
|
||||||
|
|
||||||
|
[goon]: https://github.com/arp242/goon
|
||||||
|
[Vagrant]: https://www.vagrantup.com/
|
||||||
|
[integration_test.go]: /integration_test.go
|
|
@ -0,0 +1,25 @@
|
||||||
|
Copyright © 2012 The Go Authors. All rights reserved.
|
||||||
|
Copyright © fsnotify Authors. 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.
|
|
@ -0,0 +1,184 @@
|
||||||
|
fsnotify is a Go library to provide cross-platform filesystem notifications on
|
||||||
|
Windows, Linux, macOS, BSD, and illumos.
|
||||||
|
|
||||||
|
Go 1.17 or newer is required; the full documentation is at
|
||||||
|
https://pkg.go.dev/github.com/fsnotify/fsnotify
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Platform support:
|
||||||
|
|
||||||
|
| Backend | OS | Status |
|
||||||
|
| :-------------------- | :--------- | :------------------------------------------------------------------------ |
|
||||||
|
| inotify | Linux | Supported |
|
||||||
|
| kqueue | BSD, macOS | Supported |
|
||||||
|
| ReadDirectoryChangesW | Windows | Supported |
|
||||||
|
| FEN | illumos | Supported |
|
||||||
|
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
|
||||||
|
| AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
|
||||||
|
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
|
||||||
|
| USN Journals | Windows | [Needs support in x/sys/windows][usn] |
|
||||||
|
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
|
||||||
|
|
||||||
|
Linux and illumos should include Android and Solaris, but these are currently
|
||||||
|
untested.
|
||||||
|
|
||||||
|
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
|
||||||
|
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
|
||||||
|
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
A basic example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create new watcher.
|
||||||
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer watcher.Close()
|
||||||
|
|
||||||
|
// Start listening for events.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case event, ok := <-watcher.Events:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("event:", event)
|
||||||
|
if event.Has(fsnotify.Write) {
|
||||||
|
log.Println("modified file:", event.Name)
|
||||||
|
}
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println("error:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Add a path.
|
||||||
|
err = watcher.Add("/tmp")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block main goroutine forever.
|
||||||
|
<-make(chan struct{})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be
|
||||||
|
run with:
|
||||||
|
|
||||||
|
% go run ./cmd/fsnotify
|
||||||
|
|
||||||
|
Further detailed documentation can be found in godoc:
|
||||||
|
https://pkg.go.dev/github.com/fsnotify/fsnotify
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
---
|
||||||
|
### Will a file still be watched when it's moved to another directory?
|
||||||
|
No, not unless you are watching the location it was moved to.
|
||||||
|
|
||||||
|
### Are subdirectories watched?
|
||||||
|
No, you must add watches for any directory you want to watch (a recursive
|
||||||
|
watcher is on the roadmap: [#18]).
|
||||||
|
|
||||||
|
[#18]: https://github.com/fsnotify/fsnotify/issues/18
|
||||||
|
|
||||||
|
### Do I have to watch the Error and Event channels in a goroutine?
|
||||||
|
Yes. You can read both channels in the same goroutine using `select` (you don't
|
||||||
|
need a separate goroutine for both channels; see the example).
|
||||||
|
|
||||||
|
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
|
||||||
|
fsnotify requires support from underlying OS to work. The current NFS and SMB
|
||||||
|
protocols does not provide network level support for file notifications, and
|
||||||
|
neither do the /proc and /sys virtual filesystems.
|
||||||
|
|
||||||
|
This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
|
||||||
|
|
||||||
|
[#9]: https://github.com/fsnotify/fsnotify/issues/9
|
||||||
|
|
||||||
|
### Why do I get many Chmod events?
|
||||||
|
Some programs may generate a lot of attribute changes; for example Spotlight on
|
||||||
|
macOS, anti-virus programs, backup applications, and some others are known to do
|
||||||
|
this. As a rule, it's typically best to ignore Chmod events. They're often not
|
||||||
|
useful, and tend to cause problems.
|
||||||
|
|
||||||
|
Spotlight indexing on macOS can result in multiple events (see [#15]). A
|
||||||
|
temporary workaround is to add your folder(s) to the *Spotlight Privacy
|
||||||
|
settings* until we have a native FSEvents implementation (see [#11]).
|
||||||
|
|
||||||
|
[#11]: https://github.com/fsnotify/fsnotify/issues/11
|
||||||
|
[#15]: https://github.com/fsnotify/fsnotify/issues/15
|
||||||
|
|
||||||
|
### Watching a file doesn't work well
|
||||||
|
Watching individual files (rather than directories) is generally not recommended
|
||||||
|
as many programs (especially editors) update files atomically: it will write to
|
||||||
|
a temporary file which is then moved to to destination, overwriting the original
|
||||||
|
(or some variant thereof). The watcher on the original file is now lost, as that
|
||||||
|
no longer exists.
|
||||||
|
|
||||||
|
The upshot of this is that a power failure or crash won't leave a half-written
|
||||||
|
file.
|
||||||
|
|
||||||
|
Watch the parent directory and use `Event.Name` to filter out files you're not
|
||||||
|
interested in. There is an example of this in `cmd/fsnotify/file.go`.
|
||||||
|
|
||||||
|
Platform-specific notes
|
||||||
|
-----------------------
|
||||||
|
### Linux
|
||||||
|
When a file is removed a REMOVE event won't be emitted until all file
|
||||||
|
descriptors are closed; it will emit a CHMOD instead:
|
||||||
|
|
||||||
|
fp := os.Open("file")
|
||||||
|
os.Remove("file") // CHMOD
|
||||||
|
fp.Close() // REMOVE
|
||||||
|
|
||||||
|
This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
|
||||||
|
The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for
|
||||||
|
the number of watches per user, and `fs.inotify.max_user_instances` specifies
|
||||||
|
the maximum number of inotify instances per user. Every Watcher you create is an
|
||||||
|
"instance", and every path you add is a "watch".
|
||||||
|
|
||||||
|
These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and
|
||||||
|
`/proc/sys/fs/inotify/max_user_instances`
|
||||||
|
|
||||||
|
To increase them you can use `sysctl` or write the value to proc file:
|
||||||
|
|
||||||
|
# The default values on Linux 5.18
|
||||||
|
sysctl fs.inotify.max_user_watches=124983
|
||||||
|
sysctl fs.inotify.max_user_instances=128
|
||||||
|
|
||||||
|
To make the changes persist on reboot edit `/etc/sysctl.conf` or
|
||||||
|
`/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your
|
||||||
|
distro's documentation):
|
||||||
|
|
||||||
|
fs.inotify.max_user_watches=124983
|
||||||
|
fs.inotify.max_user_instances=128
|
||||||
|
|
||||||
|
Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
files" error.
|
||||||
|
|
||||||
|
### kqueue (macOS, all BSD systems)
|
||||||
|
kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
so if you're watching a directory with five files then that's six file
|
||||||
|
descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
these platforms.
|
||||||
|
|
||||||
|
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
|
||||||
|
control the maximum number of open files.
|
|
@ -0,0 +1,640 @@
|
||||||
|
//go:build solaris
|
||||||
|
// +build solaris
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
port *unix.EventPort
|
||||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||||
|
dirs map[string]struct{} // Explicitly watched directories
|
||||||
|
watches map[string]struct{} // Explicitly watched non-directories
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
w := &Watcher{
|
||||||
|
Events: make(chan Event, sz),
|
||||||
|
Errors: make(chan error),
|
||||||
|
dirs: make(map[string]struct{}),
|
||||||
|
watches: make(map[string]struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
w.port, err = unix.NewEventPort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.readEvents()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendEvent attempts to send an event to the user, returning true if the event
|
||||||
|
// was put in the channel successfully and false if the watcher has been closed.
|
||||||
|
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
|
||||||
|
select {
|
||||||
|
case w.Events <- Event{Name: name, Op: op}:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendError attempts to send an error to the user, returning true if the error
|
||||||
|
// was put in the channel successfully and false if the watcher has been closed.
|
||||||
|
func (w *Watcher) sendError(err error) (sent bool) {
|
||||||
|
select {
|
||||||
|
case w.Errors <- err:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error {
|
||||||
|
// Take the lock used by associateFile to prevent lingering events from
|
||||||
|
// being processed after the close
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
close(w.done)
|
||||||
|
return w.port.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if w.port.PathIsWatched(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
|
// Currently we resolve symlinks that were explicitly requested to be
|
||||||
|
// watched. Otherwise we would use LStat here.
|
||||||
|
stat, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate all files in the directory.
|
||||||
|
if stat.IsDir() {
|
||||||
|
err := w.handleDirectory(name, stat, true, w.associateFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.dirs[name] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.associateFile(name, stat, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.watches[name] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !w.port.PathIsWatched(name) {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user has expressed an intent. Immediately remove this name from
|
||||||
|
// whichever watch list it might be in. If it's not in there the delete
|
||||||
|
// doesn't cause harm.
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.watches, name)
|
||||||
|
delete(w.dirs, name)
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
stat, err := os.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove associations for every file in the directory.
|
||||||
|
if stat.IsDir() {
|
||||||
|
err := w.handleDirectory(name, stat, false, w.dissociateFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.port.DissociatePath(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readEvents contains the main loop that runs in a goroutine watching for events.
|
||||||
|
func (w *Watcher) readEvents() {
|
||||||
|
// If this function returns, the watcher has been closed and we can close
|
||||||
|
// these channels
|
||||||
|
defer func() {
|
||||||
|
close(w.Errors)
|
||||||
|
close(w.Events)
|
||||||
|
}()
|
||||||
|
|
||||||
|
pevents := make([]unix.PortEvent, 8)
|
||||||
|
for {
|
||||||
|
count, err := w.port.Get(pevents, 1, nil)
|
||||||
|
if err != nil && err != unix.ETIME {
|
||||||
|
// Interrupted system call (count should be 0) ignore and continue
|
||||||
|
if errors.Is(err, unix.EINTR) && count == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Get failed because we called w.Close()
|
||||||
|
if errors.Is(err, unix.EBADF) && w.isClosed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// There was an error not caused by calling w.Close()
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := pevents[:count]
|
||||||
|
for _, pevent := range p {
|
||||||
|
if pevent.Source != unix.PORT_SOURCE_FILE {
|
||||||
|
// Event from unexpected source received; should never happen.
|
||||||
|
if !w.sendError(errors.New("Event from unexpected source received")) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.handleEvent(&pevent)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle all children of the directory.
|
||||||
|
for _, entry := range files {
|
||||||
|
finfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally handle the directory itself.
|
||||||
|
return handler(path, stat, follow)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvent might need to emit more than one fsnotify event if the events
|
||||||
|
// bitmap matches more than one event type (e.g. the file was both modified and
|
||||||
|
// had the attributes changed between when the association was created and the
|
||||||
|
// when event was returned)
|
||||||
|
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
|
||||||
|
var (
|
||||||
|
events = event.Events
|
||||||
|
path = event.Path
|
||||||
|
fmode = event.Cookie.(os.FileMode)
|
||||||
|
reRegister = true
|
||||||
|
)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
_, watchedDir := w.dirs[path]
|
||||||
|
_, watchedPath := w.watches[path]
|
||||||
|
w.mu.Unlock()
|
||||||
|
isWatched := watchedDir || watchedPath
|
||||||
|
|
||||||
|
if events&unix.FILE_DELETE != 0 {
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
if events&unix.FILE_RENAME_FROM != 0 {
|
||||||
|
if !w.sendEvent(path, Rename) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't keep watching the new file name
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
if events&unix.FILE_RENAME_TO != 0 {
|
||||||
|
// We don't report a Rename event for this case, because Rename events
|
||||||
|
// are interpreted as referring to the _old_ name of the file, and in
|
||||||
|
// this case the event would refer to the new name of the file. This
|
||||||
|
// type of rename event is not supported by fsnotify.
|
||||||
|
|
||||||
|
// inotify reports a Remove event in this case, so we simulate this
|
||||||
|
// here.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't keep watching the file that was removed
|
||||||
|
reRegister = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// The file is gone, nothing left to do.
|
||||||
|
if !reRegister {
|
||||||
|
if watchedDir {
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.dirs, path)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
if watchedPath {
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.watches, path)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we didn't get a deletion the file still exists and we're going to have
|
||||||
|
// to watch it again. Let's Stat it now so that we can compare permissions
|
||||||
|
// and have what we need to continue watching the file
|
||||||
|
|
||||||
|
stat, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
// This is unexpected, but we should still emit an event. This happens
|
||||||
|
// most often on "rm -r" of a subdirectory inside a watched directory We
|
||||||
|
// get a modify event of something happening inside, but by the time we
|
||||||
|
// get here, the sudirectory is already gone. Clearly we were watching
|
||||||
|
// this path but now it is gone. Let's tell the user that it was
|
||||||
|
// removed.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Suppress extra write events on removed directories; they are not
|
||||||
|
// informative and can be confusing.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolve symlinks that were explicitly watched as we would have at Add()
|
||||||
|
// time. this helps suppress spurious Chmod events on watched symlinks
|
||||||
|
if isWatched {
|
||||||
|
stat, err = os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
// The symlink still exists, but the target is gone. Report the
|
||||||
|
// Remove similar to above.
|
||||||
|
if !w.sendEvent(path, Remove) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Don't return the error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if events&unix.FILE_MODIFIED != 0 {
|
||||||
|
if fmode.IsDir() {
|
||||||
|
if watchedDir {
|
||||||
|
if err := w.updateDirectory(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !w.sendEvent(path, Write) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !w.sendEvent(path, Write) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if events&unix.FILE_ATTRIB != 0 && stat != nil {
|
||||||
|
// Only send Chmod if perms changed
|
||||||
|
if stat.Mode().Perm() != fmode.Perm() {
|
||||||
|
if !w.sendEvent(path, Chmod) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if stat != nil {
|
||||||
|
// If we get here, it means we've hit an event above that requires us to
|
||||||
|
// continue watching the file or directory
|
||||||
|
return w.associateFile(path, stat, isWatched)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) updateDirectory(path string) error {
|
||||||
|
// The directory was modified, so we must find unwatched entities and watch
|
||||||
|
// them. If something was removed from the directory, nothing will happen,
|
||||||
|
// as everything else should still be watched.
|
||||||
|
files, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range files {
|
||||||
|
path := filepath.Join(path, entry.Name())
|
||||||
|
if w.port.PathIsWatched(path) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
finfo, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = w.associateFile(path, finfo, false)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !w.sendEvent(path, Create) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
// This is primarily protecting the call to AssociatePath but it is
|
||||||
|
// important and intentional that the call to PathIsWatched is also
|
||||||
|
// protected by this mutex. Without this mutex, AssociatePath has been seen
|
||||||
|
// to error out that the path is already associated.
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
if w.port.PathIsWatched(path) {
|
||||||
|
// Remove the old association in favor of this one If we get ENOENT,
|
||||||
|
// then while the x/sys/unix wrapper still thought that this path was
|
||||||
|
// associated, the underlying event port did not. This call will have
|
||||||
|
// cleared up that discrepancy. The most likely cause is that the event
|
||||||
|
// has fired but we haven't processed it yet.
|
||||||
|
err := w.port.DissociatePath(path)
|
||||||
|
if err != nil && err != unix.ENOENT {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
|
||||||
|
// targets.
|
||||||
|
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
|
||||||
|
if follow {
|
||||||
|
// We *DO* follow symlinks for explicitly watched entries.
|
||||||
|
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
|
||||||
|
}
|
||||||
|
return w.port.AssociatePath(path, stat,
|
||||||
|
events,
|
||||||
|
stat.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
|
||||||
|
if !w.port.PathIsWatched(path) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.port.DissociatePath(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.watches)+len(w.dirs))
|
||||||
|
for pathname := range w.dirs {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
for pathname := range w.watches {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
|
@ -0,0 +1,594 @@
|
||||||
|
//go:build linux && !appengine
|
||||||
|
// +build linux,!appengine
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
|
||||||
|
// Store fd here as os.File.Read() will no longer return on close after
|
||||||
|
// calling Fd(). See: https://github.com/golang/go/issues/26439
|
||||||
|
fd int
|
||||||
|
inotifyFile *os.File
|
||||||
|
watches *watches
|
||||||
|
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
|
||||||
|
closeMu sync.Mutex
|
||||||
|
doneResp chan struct{} // Channel to respond to Close
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
watches struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
wd map[uint32]*watch // wd → watch
|
||||||
|
path map[string]uint32 // pathname → wd
|
||||||
|
}
|
||||||
|
watch struct {
|
||||||
|
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
|
||||||
|
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
|
||||||
|
path string // Watch path.
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newWatches() *watches {
|
||||||
|
return &watches{
|
||||||
|
wd: make(map[uint32]*watch),
|
||||||
|
path: make(map[string]uint32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) len() int {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return len(w.wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) add(ww *watch) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
w.wd[ww.wd] = ww
|
||||||
|
w.path[ww.path] = ww.wd
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) remove(wd uint32) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
delete(w.path, w.wd[wd].path)
|
||||||
|
delete(w.wd, wd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) removePath(path string) (uint32, bool) {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
wd, ok := w.path[path]
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(w.path, path)
|
||||||
|
delete(w.wd, wd)
|
||||||
|
|
||||||
|
return wd, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) byPath(path string) *watch {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return w.wd[w.path[path]]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) byWd(wd uint32) *watch {
|
||||||
|
w.mu.RLock()
|
||||||
|
defer w.mu.RUnlock()
|
||||||
|
return w.wd[wd]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
var existing *watch
|
||||||
|
wd, ok := w.path[path]
|
||||||
|
if ok {
|
||||||
|
existing = w.wd[wd]
|
||||||
|
}
|
||||||
|
|
||||||
|
upd, err := f(existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if upd != nil {
|
||||||
|
w.wd[upd.wd] = upd
|
||||||
|
w.path[upd.path] = upd.wd
|
||||||
|
|
||||||
|
if upd.wd != wd {
|
||||||
|
delete(w.wd, wd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
|
||||||
|
// I/O operations won't terminate on close.
|
||||||
|
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
|
||||||
|
if fd == -1 {
|
||||||
|
return nil, errno
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &Watcher{
|
||||||
|
fd: fd,
|
||||||
|
inotifyFile: os.NewFile(uintptr(fd), ""),
|
||||||
|
watches: newWatches(),
|
||||||
|
Events: make(chan Event, sz),
|
||||||
|
Errors: make(chan error),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
doneResp: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.readEvents()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the event was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendEvent(e Event) bool {
|
||||||
|
select {
|
||||||
|
case w.Events <- e:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool {
|
||||||
|
select {
|
||||||
|
case w.Errors <- err:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool {
|
||||||
|
select {
|
||||||
|
case <-w.done:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error {
|
||||||
|
w.closeMu.Lock()
|
||||||
|
if w.isClosed() {
|
||||||
|
w.closeMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
close(w.done)
|
||||||
|
w.closeMu.Unlock()
|
||||||
|
|
||||||
|
// Causes any blocking reads to return with an error, provided the file
|
||||||
|
// still supports deadline operations.
|
||||||
|
err := w.inotifyFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for goroutine to close
|
||||||
|
<-w.doneResp
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
name = filepath.Clean(name)
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
|
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
|
||||||
|
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
|
||||||
|
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
|
||||||
|
|
||||||
|
return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
|
||||||
|
if existing != nil {
|
||||||
|
flags |= existing.flags | unix.IN_MASK_ADD
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := unix.InotifyAddWatch(w.fd, name, flags)
|
||||||
|
if wd == -1 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing == nil {
|
||||||
|
return &watch{
|
||||||
|
wd: uint32(wd),
|
||||||
|
path: name,
|
||||||
|
flags: flags,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
existing.wd = uint32(wd)
|
||||||
|
existing.flags = flags
|
||||||
|
return existing, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return w.remove(filepath.Clean(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) remove(name string) error {
|
||||||
|
wd, ok := w.watches.removePath(name)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
success, errno := unix.InotifyRmWatch(w.fd, wd)
|
||||||
|
if success == -1 {
|
||||||
|
// TODO: Perhaps it's not helpful to return an error here in every case;
|
||||||
|
// The only two possible errors are:
|
||||||
|
//
|
||||||
|
// - EBADF, which happens when w.fd is not a valid file descriptor
|
||||||
|
// of any kind.
|
||||||
|
// - EINVAL, which is when fd is not an inotify descriptor or wd
|
||||||
|
// is not a valid watch descriptor. Watch descriptors are
|
||||||
|
// invalidated when they are removed explicitly or implicitly;
|
||||||
|
// explicitly by inotify_rm_watch, implicitly when the file they
|
||||||
|
// are watching is deleted.
|
||||||
|
return errno
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]string, 0, w.watches.len())
|
||||||
|
w.watches.mu.RLock()
|
||||||
|
for pathname := range w.watches.path {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
w.watches.mu.RUnlock()
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// readEvents reads from the inotify file descriptor, converts the
|
||||||
|
// received events into Event objects and sends them via the Events channel
|
||||||
|
func (w *Watcher) readEvents() {
|
||||||
|
defer func() {
|
||||||
|
close(w.doneResp)
|
||||||
|
close(w.Errors)
|
||||||
|
close(w.Events)
|
||||||
|
}()
|
||||||
|
|
||||||
|
var (
|
||||||
|
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
|
||||||
|
errno error // Syscall errno
|
||||||
|
)
|
||||||
|
for {
|
||||||
|
// See if we have been closed.
|
||||||
|
if w.isClosed() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := w.inotifyFile.Read(buf[:])
|
||||||
|
switch {
|
||||||
|
case errors.Unwrap(err) == os.ErrClosed:
|
||||||
|
return
|
||||||
|
case err != nil:
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if n < unix.SizeofInotifyEvent {
|
||||||
|
var err error
|
||||||
|
if n == 0 {
|
||||||
|
err = io.EOF // If EOF is received. This should really never happen.
|
||||||
|
} else if n < 0 {
|
||||||
|
err = errno // If an error occurred while reading.
|
||||||
|
} else {
|
||||||
|
err = errors.New("notify: short read in readEvents()") // Read was too short.
|
||||||
|
}
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset uint32
|
||||||
|
// We don't know how many events we just read into the buffer
|
||||||
|
// While the offset points to at least one whole event...
|
||||||
|
for offset <= uint32(n-unix.SizeofInotifyEvent) {
|
||||||
|
var (
|
||||||
|
// Point "raw" to the event in the buffer
|
||||||
|
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
|
||||||
|
mask = uint32(raw.Mask)
|
||||||
|
nameLen = uint32(raw.Len)
|
||||||
|
)
|
||||||
|
|
||||||
|
if mask&unix.IN_Q_OVERFLOW != 0 {
|
||||||
|
if !w.sendError(ErrEventOverflow) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the event happened to the watched directory or the watched file, the kernel
|
||||||
|
// doesn't append the filename to the event, but we would like to always fill the
|
||||||
|
// the "Name" field with a valid filename. We retrieve the path of the watch from
|
||||||
|
// the "paths" map.
|
||||||
|
watch := w.watches.byWd(uint32(raw.Wd))
|
||||||
|
|
||||||
|
// inotify will automatically remove the watch on deletes; just need
|
||||||
|
// to clean our state here.
|
||||||
|
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
|
||||||
|
w.watches.remove(watch.wd)
|
||||||
|
}
|
||||||
|
// We can't really update the state when a watched path is moved;
|
||||||
|
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
|
||||||
|
// the watch.
|
||||||
|
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
|
||||||
|
err := w.remove(watch.path)
|
||||||
|
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var name string
|
||||||
|
if watch != nil {
|
||||||
|
name = watch.path
|
||||||
|
}
|
||||||
|
if nameLen > 0 {
|
||||||
|
// Point "bytes" at the first byte of the filename
|
||||||
|
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
|
||||||
|
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
|
||||||
|
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
|
||||||
|
}
|
||||||
|
|
||||||
|
event := w.newEvent(name, mask)
|
||||||
|
|
||||||
|
// Send the events that are not ignored on the events channel
|
||||||
|
if mask&unix.IN_IGNORED == 0 {
|
||||||
|
if !w.sendEvent(event) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next event in the buffer
|
||||||
|
offset += unix.SizeofInotifyEvent + nameLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newEvent returns an platform-independent Event based on an inotify mask.
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event {
|
||||||
|
e := Event{Name: name}
|
||||||
|
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
|
||||||
|
e.Op |= Create
|
||||||
|
}
|
||||||
|
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
|
||||||
|
e.Op |= Remove
|
||||||
|
}
|
||||||
|
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
|
||||||
|
e.Op |= Write
|
||||||
|
}
|
||||||
|
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
|
||||||
|
e.Op |= Rename
|
||||||
|
}
|
||||||
|
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
|
||||||
|
e.Op |= Chmod
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
|
@ -0,0 +1,782 @@
|
||||||
|
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
|
||||||
|
// +build freebsd openbsd netbsd dragonfly darwin
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
|
||||||
|
done chan struct{}
|
||||||
|
kq int // File descriptor (as returned by the kqueue() syscall).
|
||||||
|
closepipe [2]int // Pipe used for closing.
|
||||||
|
mu sync.Mutex // Protects access to watcher data
|
||||||
|
watches map[string]int // Watched file descriptors (key: path).
|
||||||
|
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
|
||||||
|
userWatches map[string]struct{} // Watches added with Watcher.Add()
|
||||||
|
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
|
||||||
|
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
|
||||||
|
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
|
||||||
|
isClosed bool // Set to true when Close() is first called
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathInfo struct {
|
||||||
|
name string
|
||||||
|
isDir bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
kq, closepipe, err := newKqueue()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
w := &Watcher{
|
||||||
|
kq: kq,
|
||||||
|
closepipe: closepipe,
|
||||||
|
watches: make(map[string]int),
|
||||||
|
watchesByDir: make(map[string]map[int]struct{}),
|
||||||
|
dirFlags: make(map[string]uint32),
|
||||||
|
paths: make(map[int]pathInfo),
|
||||||
|
fileExists: make(map[string]struct{}),
|
||||||
|
userWatches: make(map[string]struct{}),
|
||||||
|
Events: make(chan Event, sz),
|
||||||
|
Errors: make(chan error),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.readEvents()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newKqueue creates a new kernel event queue and returns a descriptor.
|
||||||
|
//
|
||||||
|
// This registers a new event on closepipe, which will trigger an event when
|
||||||
|
// it's closed. This way we can use kevent() without timeout/polling; without
|
||||||
|
// the closepipe, it would block forever and we wouldn't be able to stop it at
|
||||||
|
// all.
|
||||||
|
func newKqueue() (kq int, closepipe [2]int, err error) {
|
||||||
|
kq, err = unix.Kqueue()
|
||||||
|
if kq == -1 {
|
||||||
|
return kq, closepipe, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the close pipe.
|
||||||
|
err = unix.Pipe(closepipe[:])
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(kq)
|
||||||
|
return kq, closepipe, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register changes to listen on the closepipe.
|
||||||
|
changes := make([]unix.Kevent_t, 1)
|
||||||
|
// SetKevent converts int to the platform-specific types.
|
||||||
|
unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ,
|
||||||
|
unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT)
|
||||||
|
|
||||||
|
ok, err := unix.Kevent(kq, changes, nil, nil)
|
||||||
|
if ok == -1 {
|
||||||
|
unix.Close(kq)
|
||||||
|
unix.Close(closepipe[0])
|
||||||
|
unix.Close(closepipe[1])
|
||||||
|
return kq, closepipe, err
|
||||||
|
}
|
||||||
|
return kq, closepipe, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the event was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendEvent(e Event) bool {
|
||||||
|
select {
|
||||||
|
case w.Events <- e:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool {
|
||||||
|
select {
|
||||||
|
case w.Errors <- err:
|
||||||
|
return true
|
||||||
|
case <-w.done:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error {
|
||||||
|
w.mu.Lock()
|
||||||
|
if w.isClosed {
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.isClosed = true
|
||||||
|
|
||||||
|
// copy paths to remove while locked
|
||||||
|
pathsToRemove := make([]string, 0, len(w.watches))
|
||||||
|
for name := range w.watches {
|
||||||
|
pathsToRemove = append(pathsToRemove, name)
|
||||||
|
}
|
||||||
|
w.mu.Unlock() // Unlock before calling Remove, which also locks
|
||||||
|
for _, name := range pathsToRemove {
|
||||||
|
w.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send "quit" message to the reader goroutine.
|
||||||
|
unix.Close(w.closepipe[1])
|
||||||
|
close(w.done)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
_ = getOptions(opts...)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.userWatches[name] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
_, err := w.addWatch(name, noteAllEvents)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
return w.remove(name, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) remove(name string, unwatchFiles bool) error {
|
||||||
|
name = filepath.Clean(name)
|
||||||
|
w.mu.Lock()
|
||||||
|
if w.isClosed {
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
watchfd, ok := w.watches[name]
|
||||||
|
w.mu.Unlock()
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.register([]int{watchfd}, unix.EV_DELETE, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
unix.Close(watchfd)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
isDir := w.paths[watchfd].isDir
|
||||||
|
delete(w.watches, name)
|
||||||
|
delete(w.userWatches, name)
|
||||||
|
|
||||||
|
parentName := filepath.Dir(name)
|
||||||
|
delete(w.watchesByDir[parentName], watchfd)
|
||||||
|
|
||||||
|
if len(w.watchesByDir[parentName]) == 0 {
|
||||||
|
delete(w.watchesByDir, parentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(w.paths, watchfd)
|
||||||
|
delete(w.dirFlags, name)
|
||||||
|
delete(w.fileExists, name)
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
// Find all watched paths that are in this directory that are not external.
|
||||||
|
if unwatchFiles && isDir {
|
||||||
|
var pathsToRemove []string
|
||||||
|
w.mu.Lock()
|
||||||
|
for fd := range w.watchesByDir[name] {
|
||||||
|
path := w.paths[fd]
|
||||||
|
if _, ok := w.userWatches[path.name]; !ok {
|
||||||
|
pathsToRemove = append(pathsToRemove, path.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.mu.Unlock()
|
||||||
|
for _, name := range pathsToRemove {
|
||||||
|
// Since these are internal, not much sense in propagating error to
|
||||||
|
// the user, as that will just confuse them with an error about a
|
||||||
|
// path they did not explicitly watch themselves.
|
||||||
|
w.Remove(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
if w.isClosed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.userWatches))
|
||||||
|
for pathname := range w.userWatches {
|
||||||
|
entries = append(entries, pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
|
||||||
|
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
|
||||||
|
|
||||||
|
// addWatch adds name to the watched file set; the flags are interpreted as
|
||||||
|
// described in kevent(2).
|
||||||
|
//
|
||||||
|
// Returns the real path to the file which was added, with symlinks resolved.
|
||||||
|
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
|
||||||
|
var isDir bool
|
||||||
|
name = filepath.Clean(name)
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
if w.isClosed {
|
||||||
|
w.mu.Unlock()
|
||||||
|
return "", ErrClosed
|
||||||
|
}
|
||||||
|
watchfd, alreadyWatching := w.watches[name]
|
||||||
|
// We already have a watch, but we can still override flags.
|
||||||
|
if alreadyWatching {
|
||||||
|
isDir = w.paths[watchfd].isDir
|
||||||
|
}
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if !alreadyWatching {
|
||||||
|
fi, err := os.Lstat(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't watch sockets or named pipes
|
||||||
|
if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow Symlinks.
|
||||||
|
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||||
|
link, err := os.Readlink(name)
|
||||||
|
if err != nil {
|
||||||
|
// Return nil because Linux can add unresolvable symlinks to the
|
||||||
|
// watch list without problems, so maintain consistency with
|
||||||
|
// that. There will be no file events for broken symlinks.
|
||||||
|
// TODO: more specific check; returns os.PathError; ENOENT?
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
_, alreadyWatching = w.watches[link]
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if alreadyWatching {
|
||||||
|
// Add to watches so we don't get spurious Create events later
|
||||||
|
// on when we diff the directories.
|
||||||
|
w.watches[name] = 0
|
||||||
|
w.fileExists[name] = struct{}{}
|
||||||
|
return link, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name = link
|
||||||
|
fi, err = os.Lstat(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry on EINTR; open() can return EINTR in practice on macOS.
|
||||||
|
// See #354, and Go issues 11180 and 39237.
|
||||||
|
for {
|
||||||
|
watchfd, err = unix.Open(name, openMode, 0)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if errors.Is(err, unix.EINTR) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
isDir = fi.IsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags)
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(watchfd)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !alreadyWatching {
|
||||||
|
w.mu.Lock()
|
||||||
|
parentName := filepath.Dir(name)
|
||||||
|
w.watches[name] = watchfd
|
||||||
|
|
||||||
|
watchesByDir, ok := w.watchesByDir[parentName]
|
||||||
|
if !ok {
|
||||||
|
watchesByDir = make(map[int]struct{}, 1)
|
||||||
|
w.watchesByDir[parentName] = watchesByDir
|
||||||
|
}
|
||||||
|
watchesByDir[watchfd] = struct{}{}
|
||||||
|
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDir {
|
||||||
|
// Watch the directory if it has not been watched before, or if it was
|
||||||
|
// watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
|
||||||
|
w.mu.Lock()
|
||||||
|
|
||||||
|
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
|
||||||
|
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
|
||||||
|
// Store flags so this watch can be updated later
|
||||||
|
w.dirFlags[name] = flags
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if watchDir {
|
||||||
|
if err := w.watchDirectoryFiles(name); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readEvents reads from kqueue and converts the received kevents into
|
||||||
|
// Event values that it sends down the Events channel.
|
||||||
|
func (w *Watcher) readEvents() {
|
||||||
|
defer func() {
|
||||||
|
close(w.Events)
|
||||||
|
close(w.Errors)
|
||||||
|
_ = unix.Close(w.kq)
|
||||||
|
unix.Close(w.closepipe[0])
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventBuffer := make([]unix.Kevent_t, 10)
|
||||||
|
for closed := false; !closed; {
|
||||||
|
kevents, err := w.read(eventBuffer)
|
||||||
|
// EINTR is okay, the syscall was interrupted before timeout expired.
|
||||||
|
if err != nil && err != unix.EINTR {
|
||||||
|
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush the events we received to the Events channel
|
||||||
|
for _, kevent := range kevents {
|
||||||
|
var (
|
||||||
|
watchfd = int(kevent.Ident)
|
||||||
|
mask = uint32(kevent.Fflags)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Shut down the loop when the pipe is closed, but only after all
|
||||||
|
// other events have been processed.
|
||||||
|
if watchfd == w.closepipe[0] {
|
||||||
|
closed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
path := w.paths[watchfd]
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
event := w.newEvent(path.name, mask)
|
||||||
|
|
||||||
|
if event.Has(Rename) || event.Has(Remove) {
|
||||||
|
w.remove(event.Name, false)
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.fileExists, event.Name)
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.isDir && event.Has(Write) && !event.Has(Remove) {
|
||||||
|
w.sendDirectoryChangeEvents(event.Name)
|
||||||
|
} else {
|
||||||
|
if !w.sendEvent(event) {
|
||||||
|
closed = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if event.Has(Remove) {
|
||||||
|
// Look for a file that may have overwritten this; for example,
|
||||||
|
// mv f1 f2 will delete f2, then create f2.
|
||||||
|
if path.isDir {
|
||||||
|
fileDir := filepath.Clean(event.Name)
|
||||||
|
w.mu.Lock()
|
||||||
|
_, found := w.watches[fileDir]
|
||||||
|
w.mu.Unlock()
|
||||||
|
if found {
|
||||||
|
err := w.sendDirectoryChangeEvents(fileDir)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filePath := filepath.Clean(event.Name)
|
||||||
|
if fi, err := os.Lstat(filePath); err == nil {
|
||||||
|
err := w.sendFileCreatedEventIfNew(filePath, fi)
|
||||||
|
if err != nil {
|
||||||
|
if !w.sendError(err) {
|
||||||
|
closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newEvent returns an platform-independent Event based on kqueue Fflags.
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event {
|
||||||
|
e := Event{Name: name}
|
||||||
|
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
|
||||||
|
e.Op |= Remove
|
||||||
|
}
|
||||||
|
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE {
|
||||||
|
e.Op |= Write
|
||||||
|
}
|
||||||
|
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME {
|
||||||
|
e.Op |= Rename
|
||||||
|
}
|
||||||
|
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
|
||||||
|
e.Op |= Chmod
|
||||||
|
}
|
||||||
|
// No point sending a write and delete event at the same time: if it's gone,
|
||||||
|
// then it's gone.
|
||||||
|
if e.Op.Has(Write) && e.Op.Has(Remove) {
|
||||||
|
e.Op &^= Write
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
|
||||||
|
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
|
||||||
|
// Get all files
|
||||||
|
files, err := os.ReadDir(dirPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
path := filepath.Join(dirPath, f.Name())
|
||||||
|
|
||||||
|
fi, err := f.Info()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath, err := w.internalWatch(path, fi)
|
||||||
|
if err != nil {
|
||||||
|
// No permission to read the file; that's not a problem: just skip.
|
||||||
|
// But do add it to w.fileExists to prevent it from being picked up
|
||||||
|
// as a "new" file later (it still shows up in the directory
|
||||||
|
// listing).
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
|
||||||
|
cleanPath = filepath.Clean(path)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%q: %w", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.fileExists[cleanPath] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search the directory for new files and send an event for them.
|
||||||
|
//
|
||||||
|
// This functionality is to have the BSD watcher match the inotify, which sends
|
||||||
|
// a create event for files created in a watched directory.
|
||||||
|
func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
// Directory no longer exists: we can ignore this safely. kqueue will
|
||||||
|
// still give us the correct events.
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
fi, err := f.Info()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
|
||||||
|
if err != nil {
|
||||||
|
// Don't need to send an error if this file isn't readable.
|
||||||
|
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
|
||||||
|
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) {
|
||||||
|
w.mu.Lock()
|
||||||
|
_, doesExist := w.fileExists[filePath]
|
||||||
|
w.mu.Unlock()
|
||||||
|
if !doesExist {
|
||||||
|
if !w.sendEvent(Event{Name: filePath, Op: Create}) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// like watchDirectoryFiles (but without doing another ReadDir)
|
||||||
|
filePath, err = w.internalWatch(filePath, fi)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.fileExists[filePath] = struct{}{}
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
|
||||||
|
if fi.IsDir() {
|
||||||
|
// mimic Linux providing delete events for subdirectories, but preserve
|
||||||
|
// the flags used if currently watching subdirectory
|
||||||
|
w.mu.Lock()
|
||||||
|
flags := w.dirFlags[name]
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
|
||||||
|
return w.addWatch(name, flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
// watch file to mimic Linux inotify
|
||||||
|
return w.addWatch(name, noteAllEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register events with the queue.
|
||||||
|
func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
|
||||||
|
changes := make([]unix.Kevent_t, len(fds))
|
||||||
|
for i, fd := range fds {
|
||||||
|
// SetKevent converts int to the platform-specific types.
|
||||||
|
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags)
|
||||||
|
changes[i].Fflags = fflags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the events.
|
||||||
|
success, err := unix.Kevent(w.kq, changes, nil, nil)
|
||||||
|
if success == -1 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// read retrieves pending events, or waits until an event occurs.
|
||||||
|
func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
|
||||||
|
n, err := unix.Kevent(w.kq, nil, events, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return events[0:n], nil
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
|
||||||
|
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
|
||||||
|
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return nil, errors.New("fsnotify not supported on the current platform")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error { return nil }
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string { return nil }
|
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return nil }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
|
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) Remove(name string) error { return nil }
|
|
@ -0,0 +1,827 @@
|
||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
// Windows backend based on ReadDirectoryChangesW()
|
||||||
|
//
|
||||||
|
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
|
||||||
|
//
|
||||||
|
// Note: the documentation on the Watcher type and methods is generated from
|
||||||
|
// mkdoc.zsh
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\path\to\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
type Watcher struct {
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
Events chan Event
|
||||||
|
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
Errors chan error
|
||||||
|
|
||||||
|
port windows.Handle // Handle to completion port
|
||||||
|
input chan *input // Inputs to the reader are sent on this channel
|
||||||
|
quit chan chan<- error
|
||||||
|
|
||||||
|
mu sync.Mutex // Protects access to watches, closed
|
||||||
|
watches watchMap // Map of watches (key: i-number)
|
||||||
|
closed bool // Set to true when Close() is first called
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
func NewWatcher() (*Watcher, error) {
|
||||||
|
return NewBufferedWatcher(50)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
func NewBufferedWatcher(sz uint) (*Watcher, error) {
|
||||||
|
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
|
||||||
|
}
|
||||||
|
w := &Watcher{
|
||||||
|
port: port,
|
||||||
|
watches: make(watchMap),
|
||||||
|
input: make(chan *input, 1),
|
||||||
|
Events: make(chan Event, sz),
|
||||||
|
Errors: make(chan error),
|
||||||
|
quit: make(chan chan<- error, 1),
|
||||||
|
}
|
||||||
|
go w.readEvents()
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) isClosed() bool {
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
return w.closed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) sendEvent(name string, mask uint64) bool {
|
||||||
|
if mask == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
event := w.newEvent(name, uint32(mask))
|
||||||
|
select {
|
||||||
|
case ch := <-w.quit:
|
||||||
|
w.quit <- ch
|
||||||
|
case w.Events <- event:
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the error was sent, or false if watcher is closed.
|
||||||
|
func (w *Watcher) sendError(err error) bool {
|
||||||
|
select {
|
||||||
|
case w.Errors <- err:
|
||||||
|
return true
|
||||||
|
case <-w.quit:
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
func (w *Watcher) Close() error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
w.closed = true
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
// Send "quit" message to the reader goroutine
|
||||||
|
ch := make(chan error)
|
||||||
|
w.quit <- ch
|
||||||
|
if err := w.wakeupReader(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return <-ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
|
||||||
|
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
|
||||||
|
with := getOptions(opts...)
|
||||||
|
if with.bufsize < 4096 {
|
||||||
|
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
|
||||||
|
}
|
||||||
|
|
||||||
|
in := &input{
|
||||||
|
op: opAddWatch,
|
||||||
|
path: filepath.Clean(name),
|
||||||
|
flags: sysFSALLEVENTS,
|
||||||
|
reply: make(chan error),
|
||||||
|
bufsize: with.bufsize,
|
||||||
|
}
|
||||||
|
w.input <- in
|
||||||
|
if err := w.wakeupReader(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return <-in.reply
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) Remove(name string) error {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
in := &input{
|
||||||
|
op: opRemoveWatch,
|
||||||
|
path: filepath.Clean(name),
|
||||||
|
reply: make(chan error),
|
||||||
|
}
|
||||||
|
w.input <- in
|
||||||
|
if err := w.wakeupReader(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return <-in.reply
|
||||||
|
}
|
||||||
|
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
func (w *Watcher) WatchList() []string {
|
||||||
|
if w.isClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
defer w.mu.Unlock()
|
||||||
|
|
||||||
|
entries := make([]string, 0, len(w.watches))
|
||||||
|
for _, entry := range w.watches {
|
||||||
|
for _, watchEntry := range entry {
|
||||||
|
entries = append(entries, watchEntry.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
|
// These options are from the old golang.org/x/exp/winfsnotify, where you could
|
||||||
|
// add various options to the watch. This has long since been removed.
|
||||||
|
//
|
||||||
|
// The "sys" in the name is misleading as they're not part of any "system".
|
||||||
|
//
|
||||||
|
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
|
||||||
|
const (
|
||||||
|
sysFSALLEVENTS = 0xfff
|
||||||
|
sysFSCREATE = 0x100
|
||||||
|
sysFSDELETE = 0x200
|
||||||
|
sysFSDELETESELF = 0x400
|
||||||
|
sysFSMODIFY = 0x2
|
||||||
|
sysFSMOVE = 0xc0
|
||||||
|
sysFSMOVEDFROM = 0x40
|
||||||
|
sysFSMOVEDTO = 0x80
|
||||||
|
sysFSMOVESELF = 0x800
|
||||||
|
sysFSIGNORED = 0x8000
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w *Watcher) newEvent(name string, mask uint32) Event {
|
||||||
|
e := Event{Name: name}
|
||||||
|
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
|
||||||
|
e.Op |= Create
|
||||||
|
}
|
||||||
|
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
|
||||||
|
e.Op |= Remove
|
||||||
|
}
|
||||||
|
if mask&sysFSMODIFY == sysFSMODIFY {
|
||||||
|
e.Op |= Write
|
||||||
|
}
|
||||||
|
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
|
||||||
|
e.Op |= Rename
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
opAddWatch = iota
|
||||||
|
opRemoveWatch
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
provisional uint64 = 1 << (32 + iota)
|
||||||
|
)
|
||||||
|
|
||||||
|
type input struct {
|
||||||
|
op int
|
||||||
|
path string
|
||||||
|
flags uint32
|
||||||
|
bufsize int
|
||||||
|
reply chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
type inode struct {
|
||||||
|
handle windows.Handle
|
||||||
|
volume uint32
|
||||||
|
index uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type watch struct {
|
||||||
|
ov windows.Overlapped
|
||||||
|
ino *inode // i-number
|
||||||
|
recurse bool // Recursive watch?
|
||||||
|
path string // Directory path
|
||||||
|
mask uint64 // Directory itself is being watched with these notify flags
|
||||||
|
names map[string]uint64 // Map of names being watched and their notify flags
|
||||||
|
rename string // Remembers the old name while renaming a file
|
||||||
|
buf []byte // buffer, allocated later
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
indexMap map[uint64]*watch
|
||||||
|
watchMap map[uint32]indexMap
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w *Watcher) wakeupReader() error {
|
||||||
|
err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
|
||||||
|
if err != nil {
|
||||||
|
return os.NewSyscallError("PostQueuedCompletionStatus", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) getDir(pathname string) (dir string, err error) {
|
||||||
|
attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
|
||||||
|
if err != nil {
|
||||||
|
return "", os.NewSyscallError("GetFileAttributes", err)
|
||||||
|
}
|
||||||
|
if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||||
|
dir = pathname
|
||||||
|
} else {
|
||||||
|
dir, _ = filepath.Split(pathname)
|
||||||
|
dir = filepath.Clean(dir)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) getIno(path string) (ino *inode, err error) {
|
||||||
|
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
|
||||||
|
windows.FILE_LIST_DIRECTORY,
|
||||||
|
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
|
||||||
|
nil, windows.OPEN_EXISTING,
|
||||||
|
windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, os.NewSyscallError("CreateFile", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fi windows.ByHandleFileInformation
|
||||||
|
err = windows.GetFileInformationByHandle(h, &fi)
|
||||||
|
if err != nil {
|
||||||
|
windows.CloseHandle(h)
|
||||||
|
return nil, os.NewSyscallError("GetFileInformationByHandle", err)
|
||||||
|
}
|
||||||
|
ino = &inode{
|
||||||
|
handle: h,
|
||||||
|
volume: fi.VolumeSerialNumber,
|
||||||
|
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
|
||||||
|
}
|
||||||
|
return ino, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (m watchMap) get(ino *inode) *watch {
|
||||||
|
if i := m[ino.volume]; i != nil {
|
||||||
|
return i[ino.index]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (m watchMap) set(ino *inode, watch *watch) {
|
||||||
|
i := m[ino.volume]
|
||||||
|
if i == nil {
|
||||||
|
i = make(indexMap)
|
||||||
|
m[ino.volume] = i
|
||||||
|
}
|
||||||
|
i[ino.index] = watch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
|
||||||
|
//pathname, recurse := recursivePath(pathname)
|
||||||
|
recurse := false
|
||||||
|
|
||||||
|
dir, err := w.getDir(pathname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ino, err := w.getIno(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.mu.Lock()
|
||||||
|
watchEntry := w.watches.get(ino)
|
||||||
|
w.mu.Unlock()
|
||||||
|
if watchEntry == nil {
|
||||||
|
_, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
windows.CloseHandle(ino.handle)
|
||||||
|
return os.NewSyscallError("CreateIoCompletionPort", err)
|
||||||
|
}
|
||||||
|
watchEntry = &watch{
|
||||||
|
ino: ino,
|
||||||
|
path: dir,
|
||||||
|
names: make(map[string]uint64),
|
||||||
|
recurse: recurse,
|
||||||
|
buf: make([]byte, bufsize),
|
||||||
|
}
|
||||||
|
w.mu.Lock()
|
||||||
|
w.watches.set(ino, watchEntry)
|
||||||
|
w.mu.Unlock()
|
||||||
|
flags |= provisional
|
||||||
|
} else {
|
||||||
|
windows.CloseHandle(ino.handle)
|
||||||
|
}
|
||||||
|
if pathname == dir {
|
||||||
|
watchEntry.mask |= flags
|
||||||
|
} else {
|
||||||
|
watchEntry.names[filepath.Base(pathname)] |= flags
|
||||||
|
}
|
||||||
|
|
||||||
|
err = w.startRead(watchEntry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if pathname == dir {
|
||||||
|
watchEntry.mask &= ^provisional
|
||||||
|
} else {
|
||||||
|
watchEntry.names[filepath.Base(pathname)] &= ^provisional
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) remWatch(pathname string) error {
|
||||||
|
pathname, recurse := recursivePath(pathname)
|
||||||
|
|
||||||
|
dir, err := w.getDir(pathname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ino, err := w.getIno(dir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
w.mu.Lock()
|
||||||
|
watch := w.watches.get(ino)
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if recurse && !watch.recurse {
|
||||||
|
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = windows.CloseHandle(ino.handle)
|
||||||
|
if err != nil {
|
||||||
|
w.sendError(os.NewSyscallError("CloseHandle", err))
|
||||||
|
}
|
||||||
|
if watch == nil {
|
||||||
|
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
|
||||||
|
}
|
||||||
|
if pathname == dir {
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
|
||||||
|
watch.mask = 0
|
||||||
|
} else {
|
||||||
|
name := filepath.Base(pathname)
|
||||||
|
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
|
||||||
|
delete(watch.names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.startRead(watch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) deleteWatch(watch *watch) {
|
||||||
|
for name, mask := range watch.names {
|
||||||
|
if mask&provisional == 0 {
|
||||||
|
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
|
||||||
|
}
|
||||||
|
delete(watch.names, name)
|
||||||
|
}
|
||||||
|
if watch.mask != 0 {
|
||||||
|
if watch.mask&provisional == 0 {
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
|
||||||
|
}
|
||||||
|
watch.mask = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must run within the I/O thread.
|
||||||
|
func (w *Watcher) startRead(watch *watch) error {
|
||||||
|
err := windows.CancelIo(watch.ino.handle)
|
||||||
|
if err != nil {
|
||||||
|
w.sendError(os.NewSyscallError("CancelIo", err))
|
||||||
|
w.deleteWatch(watch)
|
||||||
|
}
|
||||||
|
mask := w.toWindowsFlags(watch.mask)
|
||||||
|
for _, m := range watch.names {
|
||||||
|
mask |= w.toWindowsFlags(m)
|
||||||
|
}
|
||||||
|
if mask == 0 {
|
||||||
|
err := windows.CloseHandle(watch.ino.handle)
|
||||||
|
if err != nil {
|
||||||
|
w.sendError(os.NewSyscallError("CloseHandle", err))
|
||||||
|
}
|
||||||
|
w.mu.Lock()
|
||||||
|
delete(w.watches[watch.ino.volume], watch.ino.index)
|
||||||
|
w.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to pass the array, rather than the slice.
|
||||||
|
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
|
||||||
|
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
|
||||||
|
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
|
||||||
|
watch.recurse, mask, nil, &watch.ov, 0)
|
||||||
|
if rdErr != nil {
|
||||||
|
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
|
||||||
|
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
|
||||||
|
// Watched directory was probably removed
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
w.deleteWatch(watch)
|
||||||
|
w.startRead(watch)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readEvents reads from the I/O completion port, converts the
|
||||||
|
// received events into Event objects and sends them via the Events channel.
|
||||||
|
// Entry point to the I/O thread.
|
||||||
|
func (w *Watcher) readEvents() {
|
||||||
|
var (
|
||||||
|
n uint32
|
||||||
|
key uintptr
|
||||||
|
ov *windows.Overlapped
|
||||||
|
)
|
||||||
|
runtime.LockOSThread()
|
||||||
|
|
||||||
|
for {
|
||||||
|
// This error is handled after the watch == nil check below.
|
||||||
|
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
|
||||||
|
|
||||||
|
watch := (*watch)(unsafe.Pointer(ov))
|
||||||
|
if watch == nil {
|
||||||
|
select {
|
||||||
|
case ch := <-w.quit:
|
||||||
|
w.mu.Lock()
|
||||||
|
var indexes []indexMap
|
||||||
|
for _, index := range w.watches {
|
||||||
|
indexes = append(indexes, index)
|
||||||
|
}
|
||||||
|
w.mu.Unlock()
|
||||||
|
for _, index := range indexes {
|
||||||
|
for _, watch := range index {
|
||||||
|
w.deleteWatch(watch)
|
||||||
|
w.startRead(watch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := windows.CloseHandle(w.port)
|
||||||
|
if err != nil {
|
||||||
|
err = os.NewSyscallError("CloseHandle", err)
|
||||||
|
}
|
||||||
|
close(w.Events)
|
||||||
|
close(w.Errors)
|
||||||
|
ch <- err
|
||||||
|
return
|
||||||
|
case in := <-w.input:
|
||||||
|
switch in.op {
|
||||||
|
case opAddWatch:
|
||||||
|
in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
|
||||||
|
case opRemoveWatch:
|
||||||
|
in.reply <- w.remWatch(in.path)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch qErr {
|
||||||
|
case nil:
|
||||||
|
// No error
|
||||||
|
case windows.ERROR_MORE_DATA:
|
||||||
|
if watch == nil {
|
||||||
|
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
|
||||||
|
} else {
|
||||||
|
// The i/o succeeded but the buffer is full.
|
||||||
|
// In theory we should be building up a full packet.
|
||||||
|
// In practice we can get away with just carrying on.
|
||||||
|
n = uint32(unsafe.Sizeof(watch.buf))
|
||||||
|
}
|
||||||
|
case windows.ERROR_ACCESS_DENIED:
|
||||||
|
// Watched directory was probably removed
|
||||||
|
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
|
||||||
|
w.deleteWatch(watch)
|
||||||
|
w.startRead(watch)
|
||||||
|
continue
|
||||||
|
case windows.ERROR_OPERATION_ABORTED:
|
||||||
|
// CancelIo was called on this handle
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset uint32
|
||||||
|
for {
|
||||||
|
if n == 0 {
|
||||||
|
w.sendError(ErrEventOverflow)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point "raw" to the event in the buffer
|
||||||
|
raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
|
||||||
|
|
||||||
|
// Create a buf that is the size of the path name
|
||||||
|
size := int(raw.FileNameLength / 2)
|
||||||
|
var buf []uint16
|
||||||
|
// TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
|
||||||
|
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
|
||||||
|
sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
|
||||||
|
sh.Len = size
|
||||||
|
sh.Cap = size
|
||||||
|
name := windows.UTF16ToString(buf)
|
||||||
|
fullname := filepath.Join(watch.path, name)
|
||||||
|
|
||||||
|
var mask uint64
|
||||||
|
switch raw.Action {
|
||||||
|
case windows.FILE_ACTION_REMOVED:
|
||||||
|
mask = sysFSDELETESELF
|
||||||
|
case windows.FILE_ACTION_MODIFIED:
|
||||||
|
mask = sysFSMODIFY
|
||||||
|
case windows.FILE_ACTION_RENAMED_OLD_NAME:
|
||||||
|
watch.rename = name
|
||||||
|
case windows.FILE_ACTION_RENAMED_NEW_NAME:
|
||||||
|
// Update saved path of all sub-watches.
|
||||||
|
old := filepath.Join(watch.path, watch.rename)
|
||||||
|
w.mu.Lock()
|
||||||
|
for _, watchMap := range w.watches {
|
||||||
|
for _, ww := range watchMap {
|
||||||
|
if strings.HasPrefix(ww.path, old) {
|
||||||
|
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.mu.Unlock()
|
||||||
|
|
||||||
|
if watch.names[watch.rename] != 0 {
|
||||||
|
watch.names[name] |= watch.names[watch.rename]
|
||||||
|
delete(watch.names, watch.rename)
|
||||||
|
mask = sysFSMOVESELF
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sendNameEvent := func() {
|
||||||
|
w.sendEvent(fullname, watch.names[name]&mask)
|
||||||
|
}
|
||||||
|
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
|
||||||
|
sendNameEvent()
|
||||||
|
}
|
||||||
|
if raw.Action == windows.FILE_ACTION_REMOVED {
|
||||||
|
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
|
||||||
|
delete(watch.names, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action))
|
||||||
|
if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
|
||||||
|
fullname = filepath.Join(watch.path, watch.rename)
|
||||||
|
sendNameEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the next event in the buffer
|
||||||
|
if raw.NextEntryOffset == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
offset += raw.NextEntryOffset
|
||||||
|
|
||||||
|
// Error!
|
||||||
|
if offset >= n {
|
||||||
|
//lint:ignore ST1005 Windows should be capitalized
|
||||||
|
w.sendError(errors.New(
|
||||||
|
"Windows system assumed buffer larger than it is, events have likely been missed"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := w.startRead(watch); err != nil {
|
||||||
|
w.sendError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
|
||||||
|
var m uint32
|
||||||
|
if mask&sysFSMODIFY != 0 {
|
||||||
|
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
|
||||||
|
}
|
||||||
|
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
|
||||||
|
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
|
||||||
|
switch action {
|
||||||
|
case windows.FILE_ACTION_ADDED:
|
||||||
|
return sysFSCREATE
|
||||||
|
case windows.FILE_ACTION_REMOVED:
|
||||||
|
return sysFSDELETE
|
||||||
|
case windows.FILE_ACTION_MODIFIED:
|
||||||
|
return sysFSMODIFY
|
||||||
|
case windows.FILE_ACTION_RENAMED_OLD_NAME:
|
||||||
|
return sysFSMOVEDFROM
|
||||||
|
case windows.FILE_ACTION_RENAMED_NEW_NAME:
|
||||||
|
return sysFSMOVEDTO
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
// Package fsnotify provides a cross-platform interface for file system
|
||||||
|
// notifications.
|
||||||
|
//
|
||||||
|
// Currently supported systems:
|
||||||
|
//
|
||||||
|
// Linux 2.6.32+ via inotify
|
||||||
|
// BSD, macOS via kqueue
|
||||||
|
// Windows via ReadDirectoryChangesW
|
||||||
|
// illumos via FEN
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Event represents a file system notification.
|
||||||
|
type Event struct {
|
||||||
|
// Path to the file or directory.
|
||||||
|
//
|
||||||
|
// Paths are relative to the input; for example with Add("dir") the Name
|
||||||
|
// will be set to "dir/file" if you create that file, but if you use
|
||||||
|
// Add("/path/to/dir") it will be "/path/to/dir/file".
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// File operation that triggered the event.
|
||||||
|
//
|
||||||
|
// This is a bitmask and some systems may send multiple operations at once.
|
||||||
|
// Use the Event.Has() method instead of comparing with ==.
|
||||||
|
Op Op
|
||||||
|
}
|
||||||
|
|
||||||
|
// Op describes a set of file operations.
|
||||||
|
type Op uint32
|
||||||
|
|
||||||
|
// The operations fsnotify can trigger; see the documentation on [Watcher] for a
|
||||||
|
// full description, and check them with [Event.Has].
|
||||||
|
const (
|
||||||
|
// A new pathname was created.
|
||||||
|
Create Op = 1 << iota
|
||||||
|
|
||||||
|
// The pathname was written to; this does *not* mean the write has finished,
|
||||||
|
// and a write can be followed by more writes.
|
||||||
|
Write
|
||||||
|
|
||||||
|
// The path was removed; any watches on it will be removed. Some "remove"
|
||||||
|
// operations may trigger a Rename if the file is actually moved (for
|
||||||
|
// example "remove to trash" is often a rename).
|
||||||
|
Remove
|
||||||
|
|
||||||
|
// The path was renamed to something else; any watched on it will be
|
||||||
|
// removed.
|
||||||
|
Rename
|
||||||
|
|
||||||
|
// File attributes were changed.
|
||||||
|
//
|
||||||
|
// It's generally not recommended to take action on this event, as it may
|
||||||
|
// get triggered very frequently by some software. For example, Spotlight
|
||||||
|
// indexing on macOS, anti-virus software, backup software, etc.
|
||||||
|
Chmod
|
||||||
|
)
|
||||||
|
|
||||||
|
// Common errors that can be reported.
|
||||||
|
var (
|
||||||
|
ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
|
||||||
|
ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
|
||||||
|
ErrClosed = errors.New("fsnotify: watcher already closed")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (o Op) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
if o.Has(Create) {
|
||||||
|
b.WriteString("|CREATE")
|
||||||
|
}
|
||||||
|
if o.Has(Remove) {
|
||||||
|
b.WriteString("|REMOVE")
|
||||||
|
}
|
||||||
|
if o.Has(Write) {
|
||||||
|
b.WriteString("|WRITE")
|
||||||
|
}
|
||||||
|
if o.Has(Rename) {
|
||||||
|
b.WriteString("|RENAME")
|
||||||
|
}
|
||||||
|
if o.Has(Chmod) {
|
||||||
|
b.WriteString("|CHMOD")
|
||||||
|
}
|
||||||
|
if b.Len() == 0 {
|
||||||
|
return "[no events]"
|
||||||
|
}
|
||||||
|
return b.String()[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has reports if this operation has the given operation.
|
||||||
|
func (o Op) Has(h Op) bool { return o&h != 0 }
|
||||||
|
|
||||||
|
// Has reports if this event has the given operation.
|
||||||
|
func (e Event) Has(op Op) bool { return e.Op.Has(op) }
|
||||||
|
|
||||||
|
// String returns a string representation of the event with their path.
|
||||||
|
func (e Event) String() string {
|
||||||
|
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type (
|
||||||
|
addOpt func(opt *withOpts)
|
||||||
|
withOpts struct {
|
||||||
|
bufsize int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultOpts = withOpts{
|
||||||
|
bufsize: 65536, // 64K
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOptions(opts ...addOpt) withOpts {
|
||||||
|
with := defaultOpts
|
||||||
|
for _, o := range opts {
|
||||||
|
o(&with)
|
||||||
|
}
|
||||||
|
return with
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithBufferSize sets the [ReadDirectoryChangesW] buffer size.
|
||||||
|
//
|
||||||
|
// This only has effect on Windows systems, and is a no-op for other backends.
|
||||||
|
//
|
||||||
|
// The default value is 64K (65536 bytes) which is the highest value that works
|
||||||
|
// on all filesystems and should be enough for most applications, but if you
|
||||||
|
// have a large burst of events it may not be enough. You can increase it if
|
||||||
|
// you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]).
|
||||||
|
//
|
||||||
|
// [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
|
||||||
|
func WithBufferSize(bytes int) addOpt {
|
||||||
|
return func(opt *withOpts) { opt.bufsize = bytes }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this path is recursive (ends with "/..." or "\..."), and return the
|
||||||
|
// path with the /... stripped.
|
||||||
|
func recursivePath(path string) (string, bool) {
|
||||||
|
if filepath.Base(path) == "..." {
|
||||||
|
return filepath.Dir(path), true
|
||||||
|
}
|
||||||
|
return path, false
|
||||||
|
}
|
|
@ -0,0 +1,259 @@
|
||||||
|
#!/usr/bin/env zsh
|
||||||
|
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
|
||||||
|
setopt err_exit no_unset pipefail extended_glob
|
||||||
|
|
||||||
|
# Simple script to update the godoc comments on all watchers so you don't need
|
||||||
|
# to update the same comment 5 times.
|
||||||
|
|
||||||
|
watcher=$(<<EOF
|
||||||
|
// Watcher watches a set of paths, delivering events on a channel.
|
||||||
|
//
|
||||||
|
// A watcher should not be copied (e.g. pass it by pointer, rather than by
|
||||||
|
// value).
|
||||||
|
//
|
||||||
|
// # Linux notes
|
||||||
|
//
|
||||||
|
// When a file is removed a Remove event won't be emitted until all file
|
||||||
|
// descriptors are closed, and deletes will always emit a Chmod. For example:
|
||||||
|
//
|
||||||
|
// fp := os.Open("file")
|
||||||
|
// os.Remove("file") // Triggers Chmod
|
||||||
|
// fp.Close() // Triggers Remove
|
||||||
|
//
|
||||||
|
// This is the event that inotify sends, so not much can be changed about this.
|
||||||
|
//
|
||||||
|
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
|
||||||
|
// for the number of watches per user, and fs.inotify.max_user_instances
|
||||||
|
// specifies the maximum number of inotify instances per user. Every Watcher you
|
||||||
|
// create is an "instance", and every path you add is a "watch".
|
||||||
|
//
|
||||||
|
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
|
||||||
|
// /proc/sys/fs/inotify/max_user_instances
|
||||||
|
//
|
||||||
|
// To increase them you can use sysctl or write the value to the /proc file:
|
||||||
|
//
|
||||||
|
// # Default values on Linux 5.18
|
||||||
|
// sysctl fs.inotify.max_user_watches=124983
|
||||||
|
// sysctl fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// To make the changes persist on reboot edit /etc/sysctl.conf or
|
||||||
|
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
|
||||||
|
// your distro's documentation):
|
||||||
|
//
|
||||||
|
// fs.inotify.max_user_watches=124983
|
||||||
|
// fs.inotify.max_user_instances=128
|
||||||
|
//
|
||||||
|
// Reaching the limit will result in a "no space left on device" or "too many open
|
||||||
|
// files" error.
|
||||||
|
//
|
||||||
|
// # kqueue notes (macOS, BSD)
|
||||||
|
//
|
||||||
|
// kqueue requires opening a file descriptor for every file that's being watched;
|
||||||
|
// so if you're watching a directory with five files then that's six file
|
||||||
|
// descriptors. You will run in to your system's "max open files" limit faster on
|
||||||
|
// these platforms.
|
||||||
|
//
|
||||||
|
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
|
||||||
|
// control the maximum number of open files, as well as /etc/login.conf on BSD
|
||||||
|
// systems.
|
||||||
|
//
|
||||||
|
// # Windows notes
|
||||||
|
//
|
||||||
|
// Paths can be added as "C:\\path\\to\\dir", but forward slashes
|
||||||
|
// ("C:/path/to/dir") will also work.
|
||||||
|
//
|
||||||
|
// When a watched directory is removed it will always send an event for the
|
||||||
|
// directory itself, but may not send events for all files in that directory.
|
||||||
|
// Sometimes it will send events for all times, sometimes it will send no
|
||||||
|
// events, and often only for some files.
|
||||||
|
//
|
||||||
|
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
|
||||||
|
// value that is guaranteed to work with SMB filesystems. If you have many
|
||||||
|
// events in quick succession this may not be enough, and you will have to use
|
||||||
|
// [WithBufferSize] to increase the value.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
new=$(<<EOF
|
||||||
|
// NewWatcher creates a new Watcher.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
newbuffered=$(<<EOF
|
||||||
|
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
|
||||||
|
// channel.
|
||||||
|
//
|
||||||
|
// The main use case for this is situations with a very large number of events
|
||||||
|
// where the kernel buffer size can't be increased (e.g. due to lack of
|
||||||
|
// permissions). An unbuffered Watcher will perform better for almost all use
|
||||||
|
// cases, and whenever possible you will be better off increasing the kernel
|
||||||
|
// buffers instead of adding a large userspace buffer.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
add=$(<<EOF
|
||||||
|
// Add starts monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// A path can only be watched once; watching it more than once is a no-op and will
|
||||||
|
// not return an error. Paths that do not yet exist on the filesystem cannot be
|
||||||
|
// watched.
|
||||||
|
//
|
||||||
|
// A watch will be automatically removed if the watched path is deleted or
|
||||||
|
// renamed. The exception is the Windows backend, which doesn't remove the
|
||||||
|
// watcher on renames.
|
||||||
|
//
|
||||||
|
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
|
||||||
|
// filesystems (/proc, /sys, etc.) generally don't work.
|
||||||
|
//
|
||||||
|
// Returns [ErrClosed] if [Watcher.Close] was called.
|
||||||
|
//
|
||||||
|
// See [Watcher.AddWith] for a version that allows adding options.
|
||||||
|
//
|
||||||
|
// # Watching directories
|
||||||
|
//
|
||||||
|
// All files in a directory are monitored, including new files that are created
|
||||||
|
// after the watcher is started. Subdirectories are not watched (i.e. it's
|
||||||
|
// non-recursive).
|
||||||
|
//
|
||||||
|
// # Watching files
|
||||||
|
//
|
||||||
|
// Watching individual files (rather than directories) is generally not
|
||||||
|
// recommended as many programs (especially editors) update files atomically: it
|
||||||
|
// will write to a temporary file which is then moved to to destination,
|
||||||
|
// overwriting the original (or some variant thereof). The watcher on the
|
||||||
|
// original file is now lost, as that no longer exists.
|
||||||
|
//
|
||||||
|
// The upshot of this is that a power failure or crash won't leave a
|
||||||
|
// half-written file.
|
||||||
|
//
|
||||||
|
// Watch the parent directory and use Event.Name to filter out files you're not
|
||||||
|
// interested in. There is an example of this in cmd/fsnotify/file.go.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
addwith=$(<<EOF
|
||||||
|
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
|
||||||
|
// the defaults described below are used.
|
||||||
|
//
|
||||||
|
// Possible options are:
|
||||||
|
//
|
||||||
|
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
|
||||||
|
// other platforms. The default is 64K (65536 bytes).
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
remove=$(<<EOF
|
||||||
|
// Remove stops monitoring the path for changes.
|
||||||
|
//
|
||||||
|
// Directories are always removed non-recursively. For example, if you added
|
||||||
|
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
|
||||||
|
//
|
||||||
|
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
close=$(<<EOF
|
||||||
|
// Close removes all watches and closes the Events channel.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
watchlist=$(<<EOF
|
||||||
|
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
|
||||||
|
// yet removed).
|
||||||
|
//
|
||||||
|
// Returns nil if [Watcher.Close] was called.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
events=$(<<EOF
|
||||||
|
// Events sends the filesystem change events.
|
||||||
|
//
|
||||||
|
// fsnotify can send the following events; a "path" here can refer to a
|
||||||
|
// file, directory, symbolic link, or special file like a FIFO.
|
||||||
|
//
|
||||||
|
// fsnotify.Create A new path was created; this may be followed by one
|
||||||
|
// or more Write events if data also gets written to a
|
||||||
|
// file.
|
||||||
|
//
|
||||||
|
// fsnotify.Remove A path was removed.
|
||||||
|
//
|
||||||
|
// fsnotify.Rename A path was renamed. A rename is always sent with the
|
||||||
|
// old path as Event.Name, and a Create event will be
|
||||||
|
// sent with the new name. Renames are only sent for
|
||||||
|
// paths that are currently watched; e.g. moving an
|
||||||
|
// unmonitored file into a monitored directory will
|
||||||
|
// show up as just a Create. Similarly, renaming a file
|
||||||
|
// to outside a monitored directory will show up as
|
||||||
|
// only a Rename.
|
||||||
|
//
|
||||||
|
// fsnotify.Write A file or named pipe was written to. A Truncate will
|
||||||
|
// also trigger a Write. A single "write action"
|
||||||
|
// initiated by the user may show up as one or multiple
|
||||||
|
// writes, depending on when the system syncs things to
|
||||||
|
// disk. For example when compiling a large Go program
|
||||||
|
// you may get hundreds of Write events, and you may
|
||||||
|
// want to wait until you've stopped receiving them
|
||||||
|
// (see the dedup example in cmd/fsnotify).
|
||||||
|
//
|
||||||
|
// Some systems may send Write event for directories
|
||||||
|
// when the directory content changes.
|
||||||
|
//
|
||||||
|
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
|
||||||
|
// when a file is removed (or more accurately, when a
|
||||||
|
// link to an inode is removed). On kqueue it's sent
|
||||||
|
// when a file is truncated. On Windows it's never
|
||||||
|
// sent.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
errors=$(<<EOF
|
||||||
|
// Errors sends any errors.
|
||||||
|
//
|
||||||
|
// ErrEventOverflow is used to indicate there are too many events:
|
||||||
|
//
|
||||||
|
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
|
||||||
|
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
|
||||||
|
// - kqueue, fen: Not used.
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
set-cmt() {
|
||||||
|
local pat=$1
|
||||||
|
local cmt=$2
|
||||||
|
|
||||||
|
IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
|
||||||
|
for f in $files; do
|
||||||
|
IFS=':' local fields=($=f)
|
||||||
|
local file=$fields[1]
|
||||||
|
local end=$(( $fields[2] - 1 ))
|
||||||
|
|
||||||
|
# Find start of comment.
|
||||||
|
local start=0
|
||||||
|
IFS=$'\n' local lines=($(head -n$end $file))
|
||||||
|
for (( i = 1; i <= $#lines; i++ )); do
|
||||||
|
local line=$lines[-$i]
|
||||||
|
if ! grep -q '^[[:space:]]*//' <<<$line; then
|
||||||
|
start=$(( end - (i - 2) ))
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
head -n $(( start - 1 )) $file >/tmp/x
|
||||||
|
print -r -- $cmt >>/tmp/x
|
||||||
|
tail -n+$(( end + 1 )) $file >>/tmp/x
|
||||||
|
mv /tmp/x $file
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
set-cmt '^type Watcher struct ' $watcher
|
||||||
|
set-cmt '^func NewWatcher(' $new
|
||||||
|
set-cmt '^func NewBufferedWatcher(' $newbuffered
|
||||||
|
set-cmt '^func (w \*Watcher) Add(' $add
|
||||||
|
set-cmt '^func (w \*Watcher) AddWith(' $addwith
|
||||||
|
set-cmt '^func (w \*Watcher) Remove(' $remove
|
||||||
|
set-cmt '^func (w \*Watcher) Close(' $close
|
||||||
|
set-cmt '^func (w \*Watcher) WatchList(' $watchlist
|
||||||
|
set-cmt '^[[:space:]]*Events *chan Event$' $events
|
||||||
|
set-cmt '^[[:space:]]*Errors *chan error$' $errors
|
|
@ -0,0 +1,8 @@
|
||||||
|
//go:build freebsd || openbsd || netbsd || dragonfly
|
||||||
|
// +build freebsd openbsd netbsd dragonfly
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC
|
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package fsnotify
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
// note: this constant is not defined on BSD
|
||||||
|
const openMode = unix.O_EVTONLY | unix.O_CLOEXEC
|
|
@ -0,0 +1,11 @@
|
||||||
|
_test
|
||||||
|
*.test
|
||||||
|
_testmain.go
|
||||||
|
_obj
|
||||||
|
*~
|
||||||
|
*.6
|
||||||
|
6.out
|
||||||
|
gorptest.bin
|
||||||
|
tmp
|
||||||
|
.idea
|
||||||
|
coverage.out
|
|
@ -0,0 +1,36 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- "1.15.x"
|
||||||
|
- "1.16.x"
|
||||||
|
- tip
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- go: tip
|
||||||
|
|
||||||
|
services:
|
||||||
|
- mysql
|
||||||
|
- postgresql
|
||||||
|
- sqlite3
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- secure: RriLxF6+2yMl67hdVv8ImXlu0h62mhcpqjaOgYNU+IEbUQ7hx96CKY6gkpYubW3BgApvF5RH6j3+HKvh2kGp0XhDOYOQCODfBSaSipZ5Aa5RKjsEYLtuVIobvJ80awR9hUeql69+WXs0/s72WThG0qTbOUY4pqHWfteeY235hWM=
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get -t -d
|
||||||
|
- go get -t -d -tags integration
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- mysql -e "CREATE DATABASE gorptest;"
|
||||||
|
- mysql -u root -e "GRANT ALL ON gorptest.* TO gorptest@localhost IDENTIFIED BY 'gorptest'"
|
||||||
|
- psql -c "CREATE DATABASE gorptest;" -U postgres
|
||||||
|
- psql -c "CREATE USER "gorptest" WITH SUPERUSER PASSWORD 'gorptest';" -U postgres
|
||||||
|
- go get github.com/lib/pq
|
||||||
|
- go get github.com/mattn/go-sqlite3
|
||||||
|
- go get github.com/ziutek/mymysql/godrv
|
||||||
|
- go get github.com/go-sql-driver/mysql
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
|
||||||
|
script: ./test_all.sh
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Contributions are very welcome!
|
||||||
|
|
||||||
|
## First: Create an Issue
|
||||||
|
|
||||||
|
Even if your fix is simple, we'd like to have an issue to relate to
|
||||||
|
the PR. Discussion about the architecture and value can go on the
|
||||||
|
issue, leaving PR comments exclusively for coding style.
|
||||||
|
|
||||||
|
## Second: Make Your PR
|
||||||
|
|
||||||
|
- Fork the `master` branch
|
||||||
|
- Make your change
|
||||||
|
- Make a PR against the `master` branch
|
||||||
|
|
||||||
|
You don't need to wait for comments on the issue before making your
|
||||||
|
PR. If you do wait for comments, you'll have a better chance of
|
||||||
|
getting your PR accepted the first time around, but it's not
|
||||||
|
necessary.
|
||||||
|
|
||||||
|
## Third: Be Patient
|
||||||
|
|
||||||
|
- If your change breaks backward compatibility, this becomes
|
||||||
|
especially true.
|
||||||
|
|
||||||
|
We all have lives and jobs, and many of us are no longer on projects
|
||||||
|
that make use of `gorp`. We will get back to you, but it might take a
|
||||||
|
while.
|
||||||
|
|
||||||
|
## Fourth: Consider Becoming a Maintainer
|
||||||
|
|
||||||
|
We really do need help. We will likely ask you for help after a good
|
||||||
|
PR, but if we don't, please create an issue requesting maintainership.
|
||||||
|
Considering how few of us are currently active, we are unlikely to
|
||||||
|
refuse good help.
|
|
@ -0,0 +1,22 @@
|
||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (c) 2012 James Cooper <james@bitmechanic.com>
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,809 @@
|
||||||
|
# Go Relational Persistence
|
||||||
|
|
||||||
|
[![build status](https://github.com/go-gorp/gorp/actions/workflows/go.yml/badge.svg)](https://github.com/go-gorp/gorp/actions)
|
||||||
|
[![issues](https://img.shields.io/github/issues/go-gorp/gorp.svg)](https://github.com/go-gorp/gorp/issues)
|
||||||
|
[![Go Reference](https://pkg.go.dev/badge/github.com/go-gorp/gorp/v3.svg)](https://pkg.go.dev/github.com/go-gorp/gorp/v3)
|
||||||
|
|
||||||
|
### Update 2016-11-13: Future versions
|
||||||
|
|
||||||
|
As many of the maintainers have become busy with other projects,
|
||||||
|
progress toward the ever-elusive v2 has slowed to the point that we're
|
||||||
|
only occasionally making progress outside of merging pull requests.
|
||||||
|
In the interest of continuing to release, I'd like to lean toward a
|
||||||
|
more maintainable path forward.
|
||||||
|
|
||||||
|
For the moment, I am releasing a v2 tag with the current feature set
|
||||||
|
from master, as some of those features have been actively used and
|
||||||
|
relied on by more than one project. Our next goal is to continue
|
||||||
|
cleaning up the code base with non-breaking changes as much as
|
||||||
|
possible, but if/when a breaking change is needed, we'll just release
|
||||||
|
new versions. This allows us to continue development at whatever pace
|
||||||
|
we're capable of, without delaying the release of features or refusing
|
||||||
|
PRs.
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
I hesitate to call gorp an ORM. Go doesn't really have objects, at
|
||||||
|
least not in the classic Smalltalk/Java sense. There goes the "O".
|
||||||
|
gorp doesn't know anything about the relationships between your
|
||||||
|
structs (at least not yet). So the "R" is questionable too (but I use
|
||||||
|
it in the name because, well, it seemed more clever).
|
||||||
|
|
||||||
|
The "M" is alive and well. Given some Go structs and a database, gorp
|
||||||
|
should remove a fair amount of boilerplate busy-work from your code.
|
||||||
|
|
||||||
|
I hope that gorp saves you time, minimizes the drudgery of getting
|
||||||
|
data in and out of your database, and helps your code focus on
|
||||||
|
algorithms, not infrastructure.
|
||||||
|
|
||||||
|
* Bind struct fields to table columns via API or tag
|
||||||
|
* Support for embedded structs
|
||||||
|
* Support for transactions
|
||||||
|
* Forward engineer db schema from structs (great for unit tests)
|
||||||
|
* Pre/post insert/update/delete hooks
|
||||||
|
* Automatically generate insert/update/delete statements for a struct
|
||||||
|
* Automatic binding of auto increment PKs back to struct after insert
|
||||||
|
* Delete by primary key(s)
|
||||||
|
* Select by primary key(s)
|
||||||
|
* Optional trace sql logging
|
||||||
|
* Bind arbitrary SQL queries to a struct
|
||||||
|
* Bind slice to SELECT query results without type assertions
|
||||||
|
* Use positional or named bind parameters in custom SELECT queries
|
||||||
|
* Optional optimistic locking using a version column (for
|
||||||
|
update/deletes)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Use `go get` or your favorite vendoring tool, using whichever import
|
||||||
|
path you'd like.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
We use semantic version tags. Feel free to import through `gopkg.in`
|
||||||
|
(e.g. `gopkg.in/gorp.v2`) to get the latest tag for a major version,
|
||||||
|
or check out the tag using your favorite vendoring tool.
|
||||||
|
|
||||||
|
Development is not very active right now, but we have plans to
|
||||||
|
restructure `gorp` as we continue to move toward a more extensible
|
||||||
|
system. Whenever a breaking change is needed, the major version will
|
||||||
|
be bumped.
|
||||||
|
|
||||||
|
The `master` branch is where all development is done, and breaking
|
||||||
|
changes may happen from time to time. That said, if you want to live
|
||||||
|
on the bleeding edge and are comfortable updating your code when we
|
||||||
|
make a breaking change, you may use `github.com/go-gorp/gorp` as your
|
||||||
|
import path.
|
||||||
|
|
||||||
|
Check the version tags to see what's available. We'll make a good
|
||||||
|
faith effort to add badges for new versions, but we make no
|
||||||
|
guarantees.
|
||||||
|
|
||||||
|
## Supported Go versions
|
||||||
|
|
||||||
|
This package is guaranteed to be compatible with the latest 2 major
|
||||||
|
versions of Go.
|
||||||
|
|
||||||
|
Any earlier versions are only supported on a best effort basis and can
|
||||||
|
be dropped any time. Go has a great compatibility promise. Upgrading
|
||||||
|
your program to a newer version of Go should never really be a
|
||||||
|
problem.
|
||||||
|
|
||||||
|
## Migration guide
|
||||||
|
|
||||||
|
#### Pre-v2 to v2
|
||||||
|
Automatic mapping of the version column used in optimistic locking has
|
||||||
|
been removed as it could cause problems if the type was not int. The
|
||||||
|
version column must now explicitly be set with
|
||||||
|
`tablemap.SetVersionCol()`.
|
||||||
|
|
||||||
|
## Help/Support
|
||||||
|
|
||||||
|
Use our [`gitter` channel](https://gitter.im/go-gorp/gorp). We used
|
||||||
|
to use IRC, but with most of us being pulled in many directions, we
|
||||||
|
often need the email notifications from `gitter` to yell at us to sign
|
||||||
|
in.
|
||||||
|
|
||||||
|
## Quickstart
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"gopkg.in/gorp.v1"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// initialize the DbMap
|
||||||
|
dbmap := initDb()
|
||||||
|
defer dbmap.Db.Close()
|
||||||
|
|
||||||
|
// delete any existing rows
|
||||||
|
err := dbmap.TruncateTables()
|
||||||
|
checkErr(err, "TruncateTables failed")
|
||||||
|
|
||||||
|
// create two posts
|
||||||
|
p1 := newPost("Go 1.1 released!", "Lorem ipsum lorem ipsum")
|
||||||
|
p2 := newPost("Go 1.2 released!", "Lorem ipsum lorem ipsum")
|
||||||
|
|
||||||
|
// insert rows - auto increment PKs will be set properly after the insert
|
||||||
|
err = dbmap.Insert(&p1, &p2)
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
// use convenience SelectInt
|
||||||
|
count, err := dbmap.SelectInt("select count(*) from posts")
|
||||||
|
checkErr(err, "select count(*) failed")
|
||||||
|
log.Println("Rows after inserting:", count)
|
||||||
|
|
||||||
|
// update a row
|
||||||
|
p2.Title = "Go 1.2 is better than ever"
|
||||||
|
count, err = dbmap.Update(&p2)
|
||||||
|
checkErr(err, "Update failed")
|
||||||
|
log.Println("Rows updated:", count)
|
||||||
|
|
||||||
|
// fetch one row - note use of "post_id" instead of "Id" since column is aliased
|
||||||
|
//
|
||||||
|
// Postgres users should use $1 instead of ? placeholders
|
||||||
|
// See 'Known Issues' below
|
||||||
|
//
|
||||||
|
err = dbmap.SelectOne(&p2, "select * from posts where post_id=?", p2.Id)
|
||||||
|
checkErr(err, "SelectOne failed")
|
||||||
|
log.Println("p2 row:", p2)
|
||||||
|
|
||||||
|
// fetch all rows
|
||||||
|
var posts []Post
|
||||||
|
_, err = dbmap.Select(&posts, "select * from posts order by post_id")
|
||||||
|
checkErr(err, "Select failed")
|
||||||
|
log.Println("All rows:")
|
||||||
|
for x, p := range posts {
|
||||||
|
log.Printf(" %d: %v\n", x, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete row by PK
|
||||||
|
count, err = dbmap.Delete(&p1)
|
||||||
|
checkErr(err, "Delete failed")
|
||||||
|
log.Println("Rows deleted:", count)
|
||||||
|
|
||||||
|
// delete row manually via Exec
|
||||||
|
_, err = dbmap.Exec("delete from posts where post_id=?", p2.Id)
|
||||||
|
checkErr(err, "Exec failed")
|
||||||
|
|
||||||
|
// confirm count is zero
|
||||||
|
count, err = dbmap.SelectInt("select count(*) from posts")
|
||||||
|
checkErr(err, "select count(*) failed")
|
||||||
|
log.Println("Row count - should be zero:", count)
|
||||||
|
|
||||||
|
log.Println("Done!")
|
||||||
|
}
|
||||||
|
|
||||||
|
type Post struct {
|
||||||
|
// db tag lets you specify the column name if it differs from the struct field
|
||||||
|
Id int64 `db:"post_id"`
|
||||||
|
Created int64
|
||||||
|
Title string `db:",size:50"` // Column size set to 50
|
||||||
|
Body string `db:"article_body,size:1024"` // Set both column name and size
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPost(title, body string) Post {
|
||||||
|
return Post{
|
||||||
|
Created: time.Now().UnixNano(),
|
||||||
|
Title: title,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDb() *gorp.DbMap {
|
||||||
|
// connect to db using standard Go database/sql API
|
||||||
|
// use whatever database/sql driver you wish
|
||||||
|
db, err := sql.Open("sqlite3", "/tmp/post_db.bin")
|
||||||
|
checkErr(err, "sql.Open failed")
|
||||||
|
|
||||||
|
// construct a gorp DbMap
|
||||||
|
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.SqliteDialect{}}
|
||||||
|
|
||||||
|
// add a table, setting the table name to 'posts' and
|
||||||
|
// specifying that the Id property is an auto incrementing PK
|
||||||
|
dbmap.AddTableWithName(Post{}, "posts").SetKeys(true, "Id")
|
||||||
|
|
||||||
|
// create the table. in a production system you'd generally
|
||||||
|
// use a migration tool, or create the tables via scripts
|
||||||
|
err = dbmap.CreateTablesIfNotExists()
|
||||||
|
checkErr(err, "Create tables failed")
|
||||||
|
|
||||||
|
return dbmap
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkErr(err error, msg string) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(msg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Mapping structs to tables
|
||||||
|
|
||||||
|
First define some types:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Invoice struct {
|
||||||
|
Id int64
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
Memo string
|
||||||
|
PersonId int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Id int64
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
FName string
|
||||||
|
LName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example of using tags to alias fields to column names
|
||||||
|
// The 'db' value is the column name
|
||||||
|
//
|
||||||
|
// A hyphen will cause gorp to skip this field, similar to the
|
||||||
|
// Go json package.
|
||||||
|
//
|
||||||
|
// This is equivalent to using the ColMap methods:
|
||||||
|
//
|
||||||
|
// table := dbmap.AddTableWithName(Product{}, "product")
|
||||||
|
// table.ColMap("Id").Rename("product_id")
|
||||||
|
// table.ColMap("Price").Rename("unit_price")
|
||||||
|
// table.ColMap("IgnoreMe").SetTransient(true)
|
||||||
|
//
|
||||||
|
// You can optionally declare the field to be a primary key and/or autoincrement
|
||||||
|
//
|
||||||
|
type Product struct {
|
||||||
|
Id int64 `db:"product_id, primarykey, autoincrement"`
|
||||||
|
Price int64 `db:"unit_price"`
|
||||||
|
IgnoreMe string `db:"-"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create a mapper, typically you'd do this one time at app startup:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// connect to db using standard Go database/sql API
|
||||||
|
// use whatever database/sql driver you wish
|
||||||
|
db, err := sql.Open("mymysql", "tcp:localhost:3306*mydb/myuser/mypassword")
|
||||||
|
|
||||||
|
// construct a gorp DbMap
|
||||||
|
dbmap := &gorp.DbMap{Db: db, Dialect: gorp.MySQLDialect{"InnoDB", "UTF8"}}
|
||||||
|
|
||||||
|
// register the structs you wish to use with gorp
|
||||||
|
// you can also use the shorter dbmap.AddTable() if you
|
||||||
|
// don't want to override the table name
|
||||||
|
//
|
||||||
|
// SetKeys(true) means we have a auto increment primary key, which
|
||||||
|
// will get automatically bound to your struct post-insert
|
||||||
|
//
|
||||||
|
t1 := dbmap.AddTableWithName(Invoice{}, "invoice_test").SetKeys(true, "Id")
|
||||||
|
t2 := dbmap.AddTableWithName(Person{}, "person_test").SetKeys(true, "Id")
|
||||||
|
t3 := dbmap.AddTableWithName(Product{}, "product_test").SetKeys(true, "Id")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Struct Embedding
|
||||||
|
|
||||||
|
gorp supports embedding structs. For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Names struct {
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WithEmbeddedStruct struct {
|
||||||
|
Id int64
|
||||||
|
Names
|
||||||
|
}
|
||||||
|
|
||||||
|
es := &WithEmbeddedStruct{-1, Names{FirstName: "Alice", LastName: "Smith"}}
|
||||||
|
err := dbmap.Insert(es)
|
||||||
|
```
|
||||||
|
|
||||||
|
See the `TestWithEmbeddedStruct` function in `gorp_test.go` for a full example.
|
||||||
|
|
||||||
|
### Create/Drop Tables ###
|
||||||
|
|
||||||
|
Automatically create / drop registered tables. This is useful for unit tests
|
||||||
|
but is entirely optional. You can of course use gorp with tables created manually,
|
||||||
|
or with a separate migration tool (like [sql-migrate](https://github.com/rubenv/sql-migrate), [goose](https://bitbucket.org/liamstask/goose) or [migrate](https://github.com/mattes/migrate)).
|
||||||
|
|
||||||
|
```go
|
||||||
|
// create all registered tables
|
||||||
|
dbmap.CreateTables()
|
||||||
|
|
||||||
|
// same as above, but uses "if not exists" clause to skip tables that are
|
||||||
|
// already defined
|
||||||
|
dbmap.CreateTablesIfNotExists()
|
||||||
|
|
||||||
|
// drop
|
||||||
|
dbmap.DropTables()
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL Logging
|
||||||
|
|
||||||
|
Optionally you can pass in a logger to trace all SQL statements.
|
||||||
|
I recommend enabling this initially while you're getting the feel for what
|
||||||
|
gorp is doing on your behalf.
|
||||||
|
|
||||||
|
Gorp defines a `GorpLogger` interface that Go's built in `log.Logger` satisfies.
|
||||||
|
However, you can write your own `GorpLogger` implementation, or use a package such
|
||||||
|
as `glog` if you want more control over how statements are logged.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Will log all SQL statements + args as they are run
|
||||||
|
// The first arg is a string prefix to prepend to all log messages
|
||||||
|
dbmap.TraceOn("[gorp]", log.New(os.Stdout, "myapp:", log.Lmicroseconds))
|
||||||
|
|
||||||
|
// Turn off tracing
|
||||||
|
dbmap.TraceOff()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Must declare as pointers so optional callback hooks
|
||||||
|
// can operate on your data, not copies
|
||||||
|
inv1 := &Invoice{0, 100, 200, "first order", 0}
|
||||||
|
inv2 := &Invoice{0, 100, 200, "second order", 0}
|
||||||
|
|
||||||
|
// Insert your rows
|
||||||
|
err := dbmap.Insert(inv1, inv2)
|
||||||
|
|
||||||
|
// Because we called SetKeys(true) on Invoice, the Id field
|
||||||
|
// will be populated after the Insert() automatically
|
||||||
|
fmt.Printf("inv1.Id=%d inv2.Id=%d\n", inv1.Id, inv2.Id)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update
|
||||||
|
|
||||||
|
Continuing the above example, use the `Update` method to modify an Invoice:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// count is the # of rows updated, which should be 1 in this example
|
||||||
|
count, err := dbmap.Update(inv1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete
|
||||||
|
|
||||||
|
If you have primary key(s) defined for a struct, you can use the `Delete`
|
||||||
|
method to remove rows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
count, err := dbmap.Delete(inv1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Select by Key
|
||||||
|
|
||||||
|
Use the `Get` method to fetch a single row by primary key. It returns
|
||||||
|
nil if no row is found.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// fetch Invoice with Id=99
|
||||||
|
obj, err := dbmap.Get(Invoice{}, 99)
|
||||||
|
inv := obj.(*Invoice)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ad Hoc SQL
|
||||||
|
|
||||||
|
#### SELECT
|
||||||
|
|
||||||
|
`Select()` and `SelectOne()` provide a simple way to bind arbitrary queries to a slice
|
||||||
|
or a single struct.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Select a slice - first return value is not needed when a slice pointer is passed to Select()
|
||||||
|
var posts []Post
|
||||||
|
_, err := dbmap.Select(&posts, "select * from post order by id")
|
||||||
|
|
||||||
|
// You can also use primitive types
|
||||||
|
var ids []string
|
||||||
|
_, err := dbmap.Select(&ids, "select id from post")
|
||||||
|
|
||||||
|
// Select a single row.
|
||||||
|
// Returns an error if no row found, or if more than one row is found
|
||||||
|
var post Post
|
||||||
|
err := dbmap.SelectOne(&post, "select * from post where id=?", id)
|
||||||
|
```
|
||||||
|
|
||||||
|
Want to do joins? Just write the SQL and the struct. gorp will bind them:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define a type for your join
|
||||||
|
// It *must* contain all the columns in your SELECT statement
|
||||||
|
//
|
||||||
|
// The names here should match the aliased column names you specify
|
||||||
|
// in your SQL - no additional binding work required. simple.
|
||||||
|
//
|
||||||
|
type InvoicePersonView struct {
|
||||||
|
InvoiceId int64
|
||||||
|
PersonId int64
|
||||||
|
Memo string
|
||||||
|
FName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create some rows
|
||||||
|
p1 := &Person{0, 0, 0, "bob", "smith"}
|
||||||
|
err = dbmap.Insert(p1)
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
// notice how we can wire up p1.Id to the invoice easily
|
||||||
|
inv1 := &Invoice{0, 0, 0, "xmas order", p1.Id}
|
||||||
|
err = dbmap.Insert(inv1)
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
// Run your query
|
||||||
|
query := "select i.Id InvoiceId, p.Id PersonId, i.Memo, p.FName " +
|
||||||
|
"from invoice_test i, person_test p " +
|
||||||
|
"where i.PersonId = p.Id"
|
||||||
|
|
||||||
|
// pass a slice to Select()
|
||||||
|
var list []InvoicePersonView
|
||||||
|
_, err := dbmap.Select(&list, query)
|
||||||
|
|
||||||
|
// this should test true
|
||||||
|
expected := InvoicePersonView{inv1.Id, p1.Id, inv1.Memo, p1.FName}
|
||||||
|
if reflect.DeepEqual(list[0], expected) {
|
||||||
|
fmt.Println("Woot! My join worked!")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SELECT string or int64
|
||||||
|
|
||||||
|
gorp provides a few convenience methods for selecting a single string or int64.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// select single int64 from db (use $1 instead of ? for postgresql)
|
||||||
|
i64, err := dbmap.SelectInt("select count(*) from foo where blah=?", blahVal)
|
||||||
|
|
||||||
|
// select single string from db:
|
||||||
|
s, err := dbmap.SelectStr("select name from foo where blah=?", blahVal)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Named bind parameters
|
||||||
|
|
||||||
|
You may use a map or struct to bind parameters by name. This is currently
|
||||||
|
only supported in SELECT queries.
|
||||||
|
|
||||||
|
```go
|
||||||
|
_, err := dbm.Select(&dest, "select * from Foo where name = :name and age = :age", map[string]interface{}{
|
||||||
|
"name": "Rob",
|
||||||
|
"age": 31,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UPDATE / DELETE
|
||||||
|
|
||||||
|
You can execute raw SQL if you wish. Particularly good for batch operations.
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := dbmap.Exec("delete from invoice_test where PersonId=?", 10)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transactions
|
||||||
|
|
||||||
|
You can batch operations into a transaction:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func InsertInv(dbmap *DbMap, inv *Invoice, per *Person) error {
|
||||||
|
// Start a new transaction
|
||||||
|
trans, err := dbmap.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = trans.Insert(per)
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
inv.PersonId = per.Id
|
||||||
|
err = trans.Insert(inv)
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
// if the commit is successful, a nil error is returned
|
||||||
|
return trans.Commit()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
Use hooks to update data before/after saving to the db. Good for timestamps:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// implement the PreInsert and PreUpdate hooks
|
||||||
|
func (i *Invoice) PreInsert(s gorp.SqlExecutor) error {
|
||||||
|
i.Created = time.Now().UnixNano()
|
||||||
|
i.Updated = i.Created
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Invoice) PreUpdate(s gorp.SqlExecutor) error {
|
||||||
|
i.Updated = time.Now().UnixNano()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can use the SqlExecutor to cascade additional SQL
|
||||||
|
// Take care to avoid cycles. gorp won't prevent them.
|
||||||
|
//
|
||||||
|
// Here's an example of a cascading delete
|
||||||
|
//
|
||||||
|
func (p *Person) PreDelete(s gorp.SqlExecutor) error {
|
||||||
|
query := "delete from invoice_test where PersonId=?"
|
||||||
|
|
||||||
|
_, err := s.Exec(query, p.Id)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Full list of hooks that you can implement:
|
||||||
|
|
||||||
|
PostGet
|
||||||
|
PreInsert
|
||||||
|
PostInsert
|
||||||
|
PreUpdate
|
||||||
|
PostUpdate
|
||||||
|
PreDelete
|
||||||
|
PostDelete
|
||||||
|
|
||||||
|
All have the same signature. for example:
|
||||||
|
|
||||||
|
func (p *MyStruct) PostUpdate(s gorp.SqlExecutor) error
|
||||||
|
|
||||||
|
### Optimistic Locking
|
||||||
|
|
||||||
|
#### Note that this behaviour has changed in v2. See [Migration Guide](#migration-guide).
|
||||||
|
|
||||||
|
gorp provides a simple optimistic locking feature, similar to Java's
|
||||||
|
JPA, that will raise an error if you try to update/delete a row whose
|
||||||
|
`version` column has a value different than the one in memory. This
|
||||||
|
provides a safe way to do "select then update" style operations
|
||||||
|
without explicit read and write locks.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Version is an auto-incremented number, managed by gorp
|
||||||
|
// If this property is present on your struct, update
|
||||||
|
// operations will be constrained
|
||||||
|
//
|
||||||
|
// For example, say we defined Person as:
|
||||||
|
|
||||||
|
type Person struct {
|
||||||
|
Id int64
|
||||||
|
Created int64
|
||||||
|
Updated int64
|
||||||
|
FName string
|
||||||
|
LName string
|
||||||
|
|
||||||
|
// automatically used as the Version col
|
||||||
|
// use table.SetVersionCol("columnName") to map a different
|
||||||
|
// struct field as the version field
|
||||||
|
Version int64
|
||||||
|
}
|
||||||
|
|
||||||
|
p1 := &Person{0, 0, 0, "Bob", "Smith", 0}
|
||||||
|
err = dbmap.Insert(p1) // Version is now 1
|
||||||
|
checkErr(err, "Insert failed")
|
||||||
|
|
||||||
|
obj, err := dbmap.Get(Person{}, p1.Id)
|
||||||
|
p2 := obj.(*Person)
|
||||||
|
p2.LName = "Edwards"
|
||||||
|
_,err = dbmap.Update(p2) // Version is now 2
|
||||||
|
checkErr(err, "Update failed")
|
||||||
|
|
||||||
|
p1.LName = "Howard"
|
||||||
|
|
||||||
|
// Raises error because p1.Version == 1, which is out of date
|
||||||
|
count, err := dbmap.Update(p1)
|
||||||
|
_, ok := err.(gorp.OptimisticLockError)
|
||||||
|
if ok {
|
||||||
|
// should reach this statement
|
||||||
|
|
||||||
|
// in a real app you might reload the row and retry, or
|
||||||
|
// you might propegate this to the user, depending on the desired
|
||||||
|
// semantics
|
||||||
|
fmt.Printf("Tried to update row with stale data: %v\n", err)
|
||||||
|
} else {
|
||||||
|
// some other db error occurred - log or return up the stack
|
||||||
|
fmt.Printf("Unknown db err: %v\n", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Adding INDEX(es) on column(s) beyond the primary key ###
|
||||||
|
|
||||||
|
Indexes are frequently critical for performance. Here is how to add
|
||||||
|
them to your tables.
|
||||||
|
|
||||||
|
NB: SqlServer and Oracle need testing and possible adjustment to the
|
||||||
|
CreateIndexSuffix() and DropIndexSuffix() methods to make AddIndex()
|
||||||
|
work for them.
|
||||||
|
|
||||||
|
In the example below we put an index both on the Id field, and on the
|
||||||
|
AcctId field.
|
||||||
|
|
||||||
|
```
|
||||||
|
type Account struct {
|
||||||
|
Id int64
|
||||||
|
AcctId string // e.g. this might be a long uuid for portability
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexType (the 2nd param to AddIndex call) is "Btree" or "Hash" for MySQL.
|
||||||
|
// demonstrate adding a second index on AcctId, and constrain that field to have unique values.
|
||||||
|
dbm.AddTable(iptab.Account{}).SetKeys(true, "Id").AddIndex("AcctIdIndex", "Btree", []string{"AcctId"}).SetUnique(true)
|
||||||
|
|
||||||
|
err = dbm.CreateTablesIfNotExists()
|
||||||
|
checkErr(err, "CreateTablesIfNotExists failed")
|
||||||
|
|
||||||
|
err = dbm.CreateIndex()
|
||||||
|
checkErr(err, "CreateIndex failed")
|
||||||
|
|
||||||
|
```
|
||||||
|
Check the effect of the CreateIndex() call in mysql:
|
||||||
|
```
|
||||||
|
$ mysql
|
||||||
|
|
||||||
|
MariaDB [test]> show create table Account;
|
||||||
|
+---------+--------------------------+
|
||||||
|
| Account | CREATE TABLE `Account` (
|
||||||
|
`Id` bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
`AcctId` varchar(255) DEFAULT NULL,
|
||||||
|
PRIMARY KEY (`Id`),
|
||||||
|
UNIQUE KEY `AcctIdIndex` (`AcctId`) USING BTREE <<<--- yes! index added.
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
||||||
|
+---------+--------------------------+
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Database Drivers
|
||||||
|
|
||||||
|
gorp uses the Go 1 `database/sql` package. A full list of compliant
|
||||||
|
drivers is available here:
|
||||||
|
|
||||||
|
http://code.google.com/p/go-wiki/wiki/SQLDrivers
|
||||||
|
|
||||||
|
Sadly, SQL databases differ on various issues. gorp provides a Dialect
|
||||||
|
interface that should be implemented per database vendor. Dialects
|
||||||
|
are provided for:
|
||||||
|
|
||||||
|
* MySQL
|
||||||
|
* PostgreSQL
|
||||||
|
* sqlite3
|
||||||
|
|
||||||
|
Each of these three databases pass the test suite. See `gorp_test.go`
|
||||||
|
for example DSNs for these three databases.
|
||||||
|
|
||||||
|
Support is also provided for:
|
||||||
|
|
||||||
|
* Oracle (contributed by @klaidliadon)
|
||||||
|
* SQL Server (contributed by @qrawl) - use driver:
|
||||||
|
github.com/denisenkom/go-mssqldb
|
||||||
|
|
||||||
|
Note that these databases are not covered by CI and I (@coopernurse)
|
||||||
|
have no good way to test them locally. So please try them and send
|
||||||
|
patches as needed, but expect a bit more unpredicability.
|
||||||
|
|
||||||
|
## Sqlite3 Extensions
|
||||||
|
|
||||||
|
In order to use sqlite3 extensions you need to first register a custom driver:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
// use whatever database/sql driver you wish
|
||||||
|
sqlite "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func customDriver() (*sql.DB, error) {
|
||||||
|
|
||||||
|
// create custom driver with extensions defined
|
||||||
|
sql.Register("sqlite3-custom", &sqlite.SQLiteDriver{
|
||||||
|
Extensions: []string{
|
||||||
|
"mod_spatialite",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// now you can then connect using the 'sqlite3-custom' driver instead of 'sqlite3'
|
||||||
|
return sql.Open("sqlite3-custom", "/tmp/post_db.bin")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
### SQL placeholder portability
|
||||||
|
|
||||||
|
Different databases use different strings to indicate variable
|
||||||
|
placeholders in prepared SQL statements. Unlike some database
|
||||||
|
abstraction layers (such as JDBC), Go's `database/sql` does not
|
||||||
|
standardize this.
|
||||||
|
|
||||||
|
SQL generated by gorp in the `Insert`, `Update`, `Delete`, and `Get`
|
||||||
|
methods delegates to a Dialect implementation for each database, and
|
||||||
|
will generate portable SQL.
|
||||||
|
|
||||||
|
Raw SQL strings passed to `Exec`, `Select`, `SelectOne`, `SelectInt`,
|
||||||
|
etc will not be parsed. Consequently you may have portability issues
|
||||||
|
if you write a query like this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// works on MySQL and Sqlite3, but not with Postgresql err :=
|
||||||
|
dbmap.SelectOne(&val, "select * from foo where id = ?", 30)
|
||||||
|
```
|
||||||
|
|
||||||
|
In `Select` and `SelectOne` you can use named parameters to work
|
||||||
|
around this. The following is portable:
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := dbmap.SelectOne(&val, "select * from foo where id = :id",
|
||||||
|
map[string]interface{} { "id": 30})
|
||||||
|
```
|
||||||
|
|
||||||
|
Additionally, when using Postgres as your database, you should utilize
|
||||||
|
`$1` instead of `?` placeholders as utilizing `?` placeholders when
|
||||||
|
querying Postgres will result in `pq: operator does not exist`
|
||||||
|
errors. Alternatively, use `dbMap.Dialect.BindVar(varIdx)` to get the
|
||||||
|
proper variable binding for your dialect.
|
||||||
|
|
||||||
|
### time.Time and time zones
|
||||||
|
|
||||||
|
gorp will pass `time.Time` fields through to the `database/sql`
|
||||||
|
driver, but note that the behavior of this type varies across database
|
||||||
|
drivers.
|
||||||
|
|
||||||
|
MySQL users should be especially cautious. See:
|
||||||
|
https://github.com/ziutek/mymysql/pull/77
|
||||||
|
|
||||||
|
To avoid any potential issues with timezone/DST, consider:
|
||||||
|
|
||||||
|
- Using an integer field for time data and storing UNIX time.
|
||||||
|
- Using a custom time type that implements some SQL types:
|
||||||
|
- [`"database/sql".Scanner`](https://golang.org/pkg/database/sql/#Scanner)
|
||||||
|
- [`"database/sql/driver".Valuer`](https://golang.org/pkg/database/sql/driver/#Valuer)
|
||||||
|
|
||||||
|
## Running the tests
|
||||||
|
|
||||||
|
The included tests may be run against MySQL, Postgresql, or sqlite3.
|
||||||
|
You must set two environment variables so the test code knows which
|
||||||
|
driver to use, and how to connect to your database.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# MySQL example:
|
||||||
|
export GORP_TEST_DSN=gomysql_test/gomysql_test/abc123
|
||||||
|
export GORP_TEST_DIALECT=mysql
|
||||||
|
|
||||||
|
# run the tests
|
||||||
|
go test
|
||||||
|
|
||||||
|
# run the tests and benchmarks
|
||||||
|
go test -bench="Bench" -benchtime 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid `GORP_TEST_DIALECT` values are: "mysql"(for mymysql),
|
||||||
|
"gomysql"(for go-sql-driver), "postgres", "sqlite" See the
|
||||||
|
`test_all.sh` script for examples of all 3 databases. This is the
|
||||||
|
script I run locally to test the library.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
gorp uses reflection to construct SQL queries and bind parameters.
|
||||||
|
See the BenchmarkNativeCrud vs BenchmarkGorpCrud in gorp_test.go for a
|
||||||
|
simple perf test. On my MacBook Pro gorp is about 2-3% slower than
|
||||||
|
hand written SQL.
|
||||||
|
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
|
||||||
|
* matthias-margush - column aliasing via tags
|
||||||
|
* Rob Figueiredo - @robfig
|
||||||
|
* Quinn Slack - @sqs
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import "reflect"
|
||||||
|
|
||||||
|
// ColumnMap represents a mapping between a Go struct field and a single
|
||||||
|
// column in a table.
|
||||||
|
// Unique and MaxSize only inform the
|
||||||
|
// CreateTables() function and are not used by Insert/Update/Delete/Get.
|
||||||
|
type ColumnMap struct {
|
||||||
|
// Column name in db table
|
||||||
|
ColumnName string
|
||||||
|
|
||||||
|
// If true, this column is skipped in generated SQL statements
|
||||||
|
Transient bool
|
||||||
|
|
||||||
|
// If true, " unique" is added to create table statements.
|
||||||
|
// Not used elsewhere
|
||||||
|
Unique bool
|
||||||
|
|
||||||
|
// Query used for getting generated id after insert
|
||||||
|
GeneratedIdQuery string
|
||||||
|
|
||||||
|
// Passed to Dialect.ToSqlType() to assist in informing the
|
||||||
|
// correct column type to map to in CreateTables()
|
||||||
|
MaxSize int
|
||||||
|
|
||||||
|
DefaultValue string
|
||||||
|
|
||||||
|
fieldName string
|
||||||
|
gotype reflect.Type
|
||||||
|
isPK bool
|
||||||
|
isAutoIncr bool
|
||||||
|
isNotNull bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename allows you to specify the column name in the table
|
||||||
|
//
|
||||||
|
// Example: table.ColMap("Updated").Rename("date_updated")
|
||||||
|
//
|
||||||
|
func (c *ColumnMap) Rename(colname string) *ColumnMap {
|
||||||
|
c.ColumnName = colname
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransient allows you to mark the column as transient. If true
|
||||||
|
// this column will be skipped when SQL statements are generated
|
||||||
|
func (c *ColumnMap) SetTransient(b bool) *ColumnMap {
|
||||||
|
c.Transient = b
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUnique adds "unique" to the create table statements for this
|
||||||
|
// column, if b is true.
|
||||||
|
func (c *ColumnMap) SetUnique(b bool) *ColumnMap {
|
||||||
|
c.Unique = b
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetNotNull adds "not null" to the create table statements for this
|
||||||
|
// column, if nn is true.
|
||||||
|
func (c *ColumnMap) SetNotNull(nn bool) *ColumnMap {
|
||||||
|
c.isNotNull = nn
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMaxSize specifies the max length of values of this column. This is
|
||||||
|
// passed to the dialect.ToSqlType() function, which can use the value
|
||||||
|
// to alter the generated type for "create table" statements
|
||||||
|
func (c *ColumnMap) SetMaxSize(size int) *ColumnMap {
|
||||||
|
c.MaxSize = size
|
||||||
|
return c
|
||||||
|
}
|
|
@ -0,0 +1,985 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DbMap is the root gorp mapping object. Create one of these for each
|
||||||
|
// database schema you wish to map. Each DbMap contains a list of
|
||||||
|
// mapped tables.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// dialect := gorp.MySQLDialect{"InnoDB", "UTF8"}
|
||||||
|
// dbmap := &gorp.DbMap{Db: db, Dialect: dialect}
|
||||||
|
//
|
||||||
|
type DbMap struct {
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
// Db handle to use with this map
|
||||||
|
Db *sql.DB
|
||||||
|
|
||||||
|
// Dialect implementation to use with this map
|
||||||
|
Dialect Dialect
|
||||||
|
|
||||||
|
TypeConverter TypeConverter
|
||||||
|
|
||||||
|
// ExpandSlices when enabled will convert slice arguments in mappers into flat
|
||||||
|
// values. It will modify the query, adding more placeholders, and the mapper,
|
||||||
|
// adding each item of the slice as a new unique entry in the mapper. For
|
||||||
|
// example, given the scenario bellow:
|
||||||
|
//
|
||||||
|
// dbmap.Select(&output, "SELECT 1 FROM example WHERE id IN (:IDs)", map[string]interface{}{
|
||||||
|
// "IDs": []int64{1, 2, 3},
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// The executed query would be:
|
||||||
|
//
|
||||||
|
// SELECT 1 FROM example WHERE id IN (:IDs0,:IDs1,:IDs2)
|
||||||
|
//
|
||||||
|
// With the mapper:
|
||||||
|
//
|
||||||
|
// map[string]interface{}{
|
||||||
|
// "IDs": []int64{1, 2, 3},
|
||||||
|
// "IDs0": int64(1),
|
||||||
|
// "IDs1": int64(2),
|
||||||
|
// "IDs2": int64(3),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// It is also flexible for custom slice types. The value just need to
|
||||||
|
// implement stringer or numberer interfaces.
|
||||||
|
//
|
||||||
|
// type CustomValue string
|
||||||
|
//
|
||||||
|
// const (
|
||||||
|
// CustomValueHey CustomValue = "hey"
|
||||||
|
// CustomValueOh CustomValue = "oh"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// type CustomValues []CustomValue
|
||||||
|
//
|
||||||
|
// func (c CustomValues) ToStringSlice() []string {
|
||||||
|
// values := make([]string, len(c))
|
||||||
|
// for i := range c {
|
||||||
|
// values[i] = string(c[i])
|
||||||
|
// }
|
||||||
|
// return values
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func query() {
|
||||||
|
// // ...
|
||||||
|
// result, err := dbmap.Select(&output, "SELECT 1 FROM example WHERE value IN (:Values)", map[string]interface{}{
|
||||||
|
// "Values": CustomValues([]CustomValue{CustomValueHey}),
|
||||||
|
// })
|
||||||
|
// // ...
|
||||||
|
// }
|
||||||
|
ExpandSliceArgs bool
|
||||||
|
|
||||||
|
tables []*TableMap
|
||||||
|
tablesDynamic map[string]*TableMap // tables that use same go-struct and different db table names
|
||||||
|
logger GorpLogger
|
||||||
|
logPrefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) dynamicTableAdd(tableName string, tbl *TableMap) {
|
||||||
|
if m.tablesDynamic == nil {
|
||||||
|
m.tablesDynamic = make(map[string]*TableMap)
|
||||||
|
}
|
||||||
|
m.tablesDynamic[tableName] = tbl
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) dynamicTableFind(tableName string) (*TableMap, bool) {
|
||||||
|
if m.tablesDynamic == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
tbl, found := m.tablesDynamic[tableName]
|
||||||
|
return tbl, found
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) dynamicTableMap() map[string]*TableMap {
|
||||||
|
if m.tablesDynamic == nil {
|
||||||
|
m.tablesDynamic = make(map[string]*TableMap)
|
||||||
|
}
|
||||||
|
return m.tablesDynamic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) WithContext(ctx context.Context) SqlExecutor {
|
||||||
|
copy := &DbMap{}
|
||||||
|
*copy = *m
|
||||||
|
copy.ctx = ctx
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) CreateIndex() error {
|
||||||
|
var err error
|
||||||
|
dialect := reflect.TypeOf(m.Dialect)
|
||||||
|
for _, table := range m.tables {
|
||||||
|
for _, index := range table.indexes {
|
||||||
|
err = m.createIndexImpl(dialect, table, index)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range m.dynamicTableMap() {
|
||||||
|
for _, index := range table.indexes {
|
||||||
|
err = m.createIndexImpl(dialect, table, index)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) createIndexImpl(dialect reflect.Type,
|
||||||
|
table *TableMap,
|
||||||
|
index *IndexMap) error {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s.WriteString("create")
|
||||||
|
if index.Unique {
|
||||||
|
s.WriteString(" unique")
|
||||||
|
}
|
||||||
|
s.WriteString(" index")
|
||||||
|
s.WriteString(fmt.Sprintf(" %s on %s", index.IndexName, table.TableName))
|
||||||
|
if dname := dialect.Name(); dname == "PostgresDialect" && index.IndexType != "" {
|
||||||
|
s.WriteString(fmt.Sprintf(" %s %s", m.Dialect.CreateIndexSuffix(), index.IndexType))
|
||||||
|
}
|
||||||
|
s.WriteString(" (")
|
||||||
|
for x, col := range index.columns {
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
s.WriteString(m.Dialect.QuoteField(col))
|
||||||
|
}
|
||||||
|
s.WriteString(")")
|
||||||
|
|
||||||
|
if dname := dialect.Name(); dname == "MySQLDialect" && index.IndexType != "" {
|
||||||
|
s.WriteString(fmt.Sprintf(" %s %s", m.Dialect.CreateIndexSuffix(), index.IndexType))
|
||||||
|
}
|
||||||
|
s.WriteString(";")
|
||||||
|
_, err := m.Exec(s.String())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableMap) DropIndex(name string) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
dialect := reflect.TypeOf(t.dbmap.Dialect)
|
||||||
|
for _, idx := range t.indexes {
|
||||||
|
if idx.IndexName == name {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s.WriteString(fmt.Sprintf("DROP INDEX %s", idx.IndexName))
|
||||||
|
|
||||||
|
if dname := dialect.Name(); dname == "MySQLDialect" {
|
||||||
|
s.WriteString(fmt.Sprintf(" %s %s", t.dbmap.Dialect.DropIndexSuffix(), t.TableName))
|
||||||
|
}
|
||||||
|
s.WriteString(";")
|
||||||
|
_, e := t.dbmap.Exec(s.String())
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.ResetSql()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTable registers the given interface type with gorp. The table name
|
||||||
|
// will be given the name of the TypeOf(i). You must call this function,
|
||||||
|
// or AddTableWithName, for any struct type you wish to persist with
|
||||||
|
// the given DbMap.
|
||||||
|
//
|
||||||
|
// This operation is idempotent. If i's type is already mapped, the
|
||||||
|
// existing *TableMap is returned
|
||||||
|
func (m *DbMap) AddTable(i interface{}) *TableMap {
|
||||||
|
return m.AddTableWithName(i, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTableWithName has the same behavior as AddTable, but sets
|
||||||
|
// table.TableName to name.
|
||||||
|
func (m *DbMap) AddTableWithName(i interface{}, name string) *TableMap {
|
||||||
|
return m.AddTableWithNameAndSchema(i, "", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTableWithNameAndSchema has the same behavior as AddTable, but sets
|
||||||
|
// table.TableName to name.
|
||||||
|
func (m *DbMap) AddTableWithNameAndSchema(i interface{}, schema string, name string) *TableMap {
|
||||||
|
t := reflect.TypeOf(i)
|
||||||
|
if name == "" {
|
||||||
|
name = t.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if we have a table for this type already
|
||||||
|
// if so, update the name and return the existing pointer
|
||||||
|
for i := range m.tables {
|
||||||
|
table := m.tables[i]
|
||||||
|
if table.gotype == t {
|
||||||
|
table.TableName = name
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmap := &TableMap{gotype: t, TableName: name, SchemaName: schema, dbmap: m}
|
||||||
|
var primaryKey []*ColumnMap
|
||||||
|
tmap.Columns, primaryKey = m.readStructColumns(t)
|
||||||
|
m.tables = append(m.tables, tmap)
|
||||||
|
if len(primaryKey) > 0 {
|
||||||
|
tmap.keys = append(tmap.keys, primaryKey...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTableDynamic registers the given interface type with gorp.
|
||||||
|
// The table name will be dynamically determined at runtime by
|
||||||
|
// using the GetTableName method on DynamicTable interface
|
||||||
|
func (m *DbMap) AddTableDynamic(inp DynamicTable, schema string) *TableMap {
|
||||||
|
|
||||||
|
val := reflect.ValueOf(inp)
|
||||||
|
elm := val.Elem()
|
||||||
|
t := elm.Type()
|
||||||
|
name := inp.TableName()
|
||||||
|
if name == "" {
|
||||||
|
panic("Missing table name in DynamicTable instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there is another dynamic table with the same name
|
||||||
|
if _, found := m.dynamicTableFind(name); found {
|
||||||
|
panic(fmt.Sprintf("A table with the same name %v already exists", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
tmap := &TableMap{gotype: t, TableName: name, SchemaName: schema, dbmap: m}
|
||||||
|
var primaryKey []*ColumnMap
|
||||||
|
tmap.Columns, primaryKey = m.readStructColumns(t)
|
||||||
|
if len(primaryKey) > 0 {
|
||||||
|
tmap.keys = append(tmap.keys, primaryKey...)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dynamicTableAdd(name, tmap)
|
||||||
|
|
||||||
|
return tmap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) readStructColumns(t reflect.Type) (cols []*ColumnMap, primaryKey []*ColumnMap) {
|
||||||
|
primaryKey = make([]*ColumnMap, 0)
|
||||||
|
n := t.NumField()
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
f := t.Field(i)
|
||||||
|
if f.Anonymous && f.Type.Kind() == reflect.Struct {
|
||||||
|
// Recursively add nested fields in embedded structs.
|
||||||
|
subcols, subpk := m.readStructColumns(f.Type)
|
||||||
|
// Don't append nested fields that have the same field
|
||||||
|
// name as an already-mapped field.
|
||||||
|
for _, subcol := range subcols {
|
||||||
|
shouldAppend := true
|
||||||
|
for _, col := range cols {
|
||||||
|
if !subcol.Transient && subcol.fieldName == col.fieldName {
|
||||||
|
shouldAppend = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldAppend {
|
||||||
|
cols = append(cols, subcol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subpk != nil {
|
||||||
|
primaryKey = append(primaryKey, subpk...)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Tag = Name { ',' Option }
|
||||||
|
// Option = OptionKey [ ':' OptionValue ]
|
||||||
|
cArguments := strings.Split(f.Tag.Get("db"), ",")
|
||||||
|
columnName := cArguments[0]
|
||||||
|
var maxSize int
|
||||||
|
var defaultValue string
|
||||||
|
var isAuto bool
|
||||||
|
var isPK bool
|
||||||
|
var isNotNull bool
|
||||||
|
for _, argString := range cArguments[1:] {
|
||||||
|
argString = strings.TrimSpace(argString)
|
||||||
|
arg := strings.SplitN(argString, ":", 2)
|
||||||
|
|
||||||
|
// check mandatory/unexpected option values
|
||||||
|
switch arg[0] {
|
||||||
|
case "size", "default":
|
||||||
|
// options requiring value
|
||||||
|
if len(arg) == 1 {
|
||||||
|
panic(fmt.Sprintf("missing option value for option %v on field %v", arg[0], f.Name))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// options where value is invalid (currently all other options)
|
||||||
|
if len(arg) == 2 {
|
||||||
|
panic(fmt.Sprintf("unexpected option value for option %v on field %v", arg[0], f.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch arg[0] {
|
||||||
|
case "size":
|
||||||
|
maxSize, _ = strconv.Atoi(arg[1])
|
||||||
|
case "default":
|
||||||
|
defaultValue = arg[1]
|
||||||
|
case "primarykey":
|
||||||
|
isPK = true
|
||||||
|
case "autoincrement":
|
||||||
|
isAuto = true
|
||||||
|
case "notnull":
|
||||||
|
isNotNull = true
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unrecognized tag option for field %v: %v", f.Name, arg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if columnName == "" {
|
||||||
|
columnName = f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
gotype := f.Type
|
||||||
|
valueType := gotype
|
||||||
|
if valueType.Kind() == reflect.Ptr {
|
||||||
|
valueType = valueType.Elem()
|
||||||
|
}
|
||||||
|
value := reflect.New(valueType).Interface()
|
||||||
|
if m.TypeConverter != nil {
|
||||||
|
// Make a new pointer to a value of type gotype and
|
||||||
|
// pass it to the TypeConverter's FromDb method to see
|
||||||
|
// if a different type should be used for the column
|
||||||
|
// type during table creation.
|
||||||
|
scanner, useHolder := m.TypeConverter.FromDb(value)
|
||||||
|
if useHolder {
|
||||||
|
value = scanner.Holder
|
||||||
|
gotype = reflect.TypeOf(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if typer, ok := value.(SqlTyper); ok {
|
||||||
|
gotype = reflect.TypeOf(typer.SqlType())
|
||||||
|
} else if typer, ok := value.(legacySqlTyper); ok {
|
||||||
|
log.Printf("Deprecation Warning: update your SqlType methods to return a driver.Value")
|
||||||
|
gotype = reflect.TypeOf(typer.SqlType())
|
||||||
|
} else if valuer, ok := value.(driver.Valuer); ok {
|
||||||
|
// Only check for driver.Valuer if SqlTyper wasn't
|
||||||
|
// found.
|
||||||
|
v, err := valuer.Value()
|
||||||
|
if err == nil && v != nil {
|
||||||
|
gotype = reflect.TypeOf(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cm := &ColumnMap{
|
||||||
|
ColumnName: columnName,
|
||||||
|
DefaultValue: defaultValue,
|
||||||
|
Transient: columnName == "-",
|
||||||
|
fieldName: f.Name,
|
||||||
|
gotype: gotype,
|
||||||
|
isPK: isPK,
|
||||||
|
isAutoIncr: isAuto,
|
||||||
|
isNotNull: isNotNull,
|
||||||
|
MaxSize: maxSize,
|
||||||
|
}
|
||||||
|
if isPK {
|
||||||
|
primaryKey = append(primaryKey, cm)
|
||||||
|
}
|
||||||
|
// Check for nested fields of the same field name and
|
||||||
|
// override them.
|
||||||
|
shouldAppend := true
|
||||||
|
for index, col := range cols {
|
||||||
|
if !col.Transient && col.fieldName == cm.fieldName {
|
||||||
|
cols[index] = cm
|
||||||
|
shouldAppend = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if shouldAppend {
|
||||||
|
cols = append(cols, cm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTables iterates through TableMaps registered to this DbMap and
|
||||||
|
// executes "create table" statements against the database for each.
|
||||||
|
//
|
||||||
|
// This is particularly useful in unit tests where you want to create
|
||||||
|
// and destroy the schema automatically.
|
||||||
|
func (m *DbMap) CreateTables() error {
|
||||||
|
return m.createTables(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTablesIfNotExists is similar to CreateTables, but starts
|
||||||
|
// each statement with "create table if not exists" so that existing
|
||||||
|
// tables do not raise errors
|
||||||
|
func (m *DbMap) CreateTablesIfNotExists() error {
|
||||||
|
return m.createTables(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) createTables(ifNotExists bool) error {
|
||||||
|
var err error
|
||||||
|
for i := range m.tables {
|
||||||
|
table := m.tables[i]
|
||||||
|
sql := table.SqlForCreate(ifNotExists)
|
||||||
|
_, err = m.Exec(sql)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tbl := range m.dynamicTableMap() {
|
||||||
|
sql := tbl.SqlForCreate(ifNotExists)
|
||||||
|
_, err = m.Exec(sql)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTable drops an individual table.
|
||||||
|
// Returns an error when the table does not exist.
|
||||||
|
func (m *DbMap) DropTable(table interface{}) error {
|
||||||
|
t := reflect.TypeOf(table)
|
||||||
|
|
||||||
|
tableName := ""
|
||||||
|
if dyn, ok := table.(DynamicTable); ok {
|
||||||
|
tableName = dyn.TableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.dropTable(t, tableName, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTableIfExists drops an individual table when the table exists.
|
||||||
|
func (m *DbMap) DropTableIfExists(table interface{}) error {
|
||||||
|
t := reflect.TypeOf(table)
|
||||||
|
|
||||||
|
tableName := ""
|
||||||
|
if dyn, ok := table.(DynamicTable); ok {
|
||||||
|
tableName = dyn.TableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.dropTable(t, tableName, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTables iterates through TableMaps registered to this DbMap and
|
||||||
|
// executes "drop table" statements against the database for each.
|
||||||
|
func (m *DbMap) DropTables() error {
|
||||||
|
return m.dropTables(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DropTablesIfExists is the same as DropTables, but uses the "if exists" clause to
|
||||||
|
// avoid errors for tables that do not exist.
|
||||||
|
func (m *DbMap) DropTablesIfExists() error {
|
||||||
|
return m.dropTables(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Goes through all the registered tables, dropping them one by one.
|
||||||
|
// If an error is encountered, then it is returned and the rest of
|
||||||
|
// the tables are not dropped.
|
||||||
|
func (m *DbMap) dropTables(addIfExists bool) (err error) {
|
||||||
|
for _, table := range m.tables {
|
||||||
|
err = m.dropTableImpl(table, addIfExists)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range m.dynamicTableMap() {
|
||||||
|
err = m.dropTableImpl(table, addIfExists)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation of dropping a single table.
|
||||||
|
func (m *DbMap) dropTable(t reflect.Type, name string, addIfExists bool) error {
|
||||||
|
table := tableOrNil(m, t, name)
|
||||||
|
if table == nil {
|
||||||
|
return fmt.Errorf("table %s was not registered", table.TableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.dropTableImpl(table, addIfExists)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) dropTableImpl(table *TableMap, ifExists bool) (err error) {
|
||||||
|
tableDrop := "drop table"
|
||||||
|
if ifExists {
|
||||||
|
tableDrop = m.Dialect.IfTableExists(tableDrop, table.SchemaName, table.TableName)
|
||||||
|
}
|
||||||
|
_, err = m.Exec(fmt.Sprintf("%s %s;", tableDrop, m.Dialect.QuotedTableForQuery(table.SchemaName, table.TableName)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateTables iterates through TableMaps registered to this DbMap and
|
||||||
|
// executes "truncate table" statements against the database for each, or in the case of
|
||||||
|
// sqlite, a "delete from" with no "where" clause, which uses the truncate optimization
|
||||||
|
// (http://www.sqlite.org/lang_delete.html)
|
||||||
|
func (m *DbMap) TruncateTables() error {
|
||||||
|
var err error
|
||||||
|
for i := range m.tables {
|
||||||
|
table := m.tables[i]
|
||||||
|
_, e := m.Exec(fmt.Sprintf("%s %s;", m.Dialect.TruncateClause(), m.Dialect.QuotedTableForQuery(table.SchemaName, table.TableName)))
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, table := range m.dynamicTableMap() {
|
||||||
|
_, e := m.Exec(fmt.Sprintf("%s %s;", m.Dialect.TruncateClause(), m.Dialect.QuotedTableForQuery(table.SchemaName, table.TableName)))
|
||||||
|
if e != nil {
|
||||||
|
err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert runs a SQL INSERT statement for each element in list. List
|
||||||
|
// items must be pointers.
|
||||||
|
//
|
||||||
|
// Any interface whose TableMap has an auto-increment primary key will
|
||||||
|
// have its last insert id bound to the PK field on the struct.
|
||||||
|
//
|
||||||
|
// The hook functions PreInsert() and/or PostInsert() will be executed
|
||||||
|
// before/after the INSERT statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Panics if any interface in the list has not been registered with AddTable
|
||||||
|
func (m *DbMap) Insert(list ...interface{}) error {
|
||||||
|
return insert(m, m, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update runs a SQL UPDATE statement for each element in list. List
|
||||||
|
// items must be pointers.
|
||||||
|
//
|
||||||
|
// The hook functions PreUpdate() and/or PostUpdate() will be executed
|
||||||
|
// before/after the UPDATE statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Returns the number of rows updated.
|
||||||
|
//
|
||||||
|
// Returns an error if SetKeys has not been called on the TableMap
|
||||||
|
// Panics if any interface in the list has not been registered with AddTable
|
||||||
|
func (m *DbMap) Update(list ...interface{}) (int64, error) {
|
||||||
|
return update(m, m, nil, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumns runs a SQL UPDATE statement for each element in list. List
|
||||||
|
// items must be pointers.
|
||||||
|
//
|
||||||
|
// Only the columns accepted by filter are included in the UPDATE.
|
||||||
|
//
|
||||||
|
// The hook functions PreUpdate() and/or PostUpdate() will be executed
|
||||||
|
// before/after the UPDATE statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Returns the number of rows updated.
|
||||||
|
//
|
||||||
|
// Returns an error if SetKeys has not been called on the TableMap
|
||||||
|
// Panics if any interface in the list has not been registered with AddTable
|
||||||
|
func (m *DbMap) UpdateColumns(filter ColumnFilter, list ...interface{}) (int64, error) {
|
||||||
|
return update(m, m, filter, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete runs a SQL DELETE statement for each element in list. List
|
||||||
|
// items must be pointers.
|
||||||
|
//
|
||||||
|
// The hook functions PreDelete() and/or PostDelete() will be executed
|
||||||
|
// before/after the DELETE statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Returns the number of rows deleted.
|
||||||
|
//
|
||||||
|
// Returns an error if SetKeys has not been called on the TableMap
|
||||||
|
// Panics if any interface in the list has not been registered with AddTable
|
||||||
|
func (m *DbMap) Delete(list ...interface{}) (int64, error) {
|
||||||
|
return delete(m, m, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get runs a SQL SELECT to fetch a single row from the table based on the
|
||||||
|
// primary key(s)
|
||||||
|
//
|
||||||
|
// i should be an empty value for the struct to load. keys should be
|
||||||
|
// the primary key value(s) for the row to load. If multiple keys
|
||||||
|
// exist on the table, the order should match the column order
|
||||||
|
// specified in SetKeys() when the table mapping was defined.
|
||||||
|
//
|
||||||
|
// The hook function PostGet() will be executed after the SELECT
|
||||||
|
// statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Returns a pointer to a struct that matches or nil if no row is found.
|
||||||
|
//
|
||||||
|
// Returns an error if SetKeys has not been called on the TableMap
|
||||||
|
// Panics if any interface in the list has not been registered with AddTable
|
||||||
|
func (m *DbMap) Get(i interface{}, keys ...interface{}) (interface{}, error) {
|
||||||
|
return get(m, m, i, keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select runs an arbitrary SQL query, binding the columns in the result
|
||||||
|
// to fields on the struct specified by i. args represent the bind
|
||||||
|
// parameters for the SQL statement.
|
||||||
|
//
|
||||||
|
// Column names on the SELECT statement should be aliased to the field names
|
||||||
|
// on the struct i. Returns an error if one or more columns in the result
|
||||||
|
// do not match. It is OK if fields on i are not part of the SQL
|
||||||
|
// statement.
|
||||||
|
//
|
||||||
|
// The hook function PostGet() will be executed after the SELECT
|
||||||
|
// statement if the interface defines them.
|
||||||
|
//
|
||||||
|
// Values are returned in one of two ways:
|
||||||
|
// 1. If i is a struct or a pointer to a struct, returns a slice of pointers to
|
||||||
|
// matching rows of type i.
|
||||||
|
// 2. If i is a pointer to a slice, the results will be appended to that slice
|
||||||
|
// and nil returned.
|
||||||
|
//
|
||||||
|
// i does NOT need to be registered with AddTable()
|
||||||
|
func (m *DbMap) Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookedselect(m, m, i, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec runs an arbitrary SQL statement. args represent the bind parameters.
|
||||||
|
// This is equivalent to running: Exec() using database/sql
|
||||||
|
func (m *DbMap) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer m.trace(now, query, args...)
|
||||||
|
}
|
||||||
|
return maybeExpandNamedQueryAndExec(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectInt is a convenience wrapper around the gorp.SelectInt function
|
||||||
|
func (m *DbMap) SelectInt(query string, args ...interface{}) (int64, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectInt(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullInt is a convenience wrapper around the gorp.SelectNullInt function
|
||||||
|
func (m *DbMap) SelectNullInt(query string, args ...interface{}) (sql.NullInt64, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullInt(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFloat is a convenience wrapper around the gorp.SelectFloat function
|
||||||
|
func (m *DbMap) SelectFloat(query string, args ...interface{}) (float64, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectFloat(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullFloat is a convenience wrapper around the gorp.SelectNullFloat function
|
||||||
|
func (m *DbMap) SelectNullFloat(query string, args ...interface{}) (sql.NullFloat64, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullFloat(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectStr is a convenience wrapper around the gorp.SelectStr function
|
||||||
|
func (m *DbMap) SelectStr(query string, args ...interface{}) (string, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectStr(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullStr is a convenience wrapper around the gorp.SelectNullStr function
|
||||||
|
func (m *DbMap) SelectNullStr(query string, args ...interface{}) (sql.NullString, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullStr(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectOne is a convenience wrapper around the gorp.SelectOne function
|
||||||
|
func (m *DbMap) SelectOne(holder interface{}, query string, args ...interface{}) error {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectOne(m, m, holder, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin starts a gorp Transaction
|
||||||
|
func (m *DbMap) Begin() (*Transaction, error) {
|
||||||
|
if m.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer m.trace(now, "begin;")
|
||||||
|
}
|
||||||
|
tx, err := begin(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Transaction{
|
||||||
|
dbmap: m,
|
||||||
|
tx: tx,
|
||||||
|
closed: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableFor returns the *TableMap corresponding to the given Go Type
|
||||||
|
// If no table is mapped to that type an error is returned.
|
||||||
|
// If checkPK is true and the mapped table has no registered PKs, an error is returned.
|
||||||
|
func (m *DbMap) TableFor(t reflect.Type, checkPK bool) (*TableMap, error) {
|
||||||
|
table := tableOrNil(m, t, "")
|
||||||
|
if table == nil {
|
||||||
|
return nil, fmt.Errorf("no table found for type: %v", t.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkPK && len(table.keys) < 1 {
|
||||||
|
e := fmt.Sprintf("gorp: no keys defined for table: %s",
|
||||||
|
table.TableName)
|
||||||
|
return nil, errors.New(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicTableFor returns the *TableMap for the dynamic table corresponding
|
||||||
|
// to the input tablename
|
||||||
|
// If no table is mapped to that tablename an error is returned.
|
||||||
|
// If checkPK is true and the mapped table has no registered PKs, an error is returned.
|
||||||
|
func (m *DbMap) DynamicTableFor(tableName string, checkPK bool) (*TableMap, error) {
|
||||||
|
table, found := m.dynamicTableFind(tableName)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("gorp: no table found for name: %v", tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkPK && len(table.keys) < 1 {
|
||||||
|
e := fmt.Sprintf("gorp: no keys defined for table: %s",
|
||||||
|
table.TableName)
|
||||||
|
return nil, errors.New(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare creates a prepared statement for later queries or executions.
|
||||||
|
// Multiple queries or executions may be run concurrently from the returned statement.
|
||||||
|
// This is equivalent to running: Prepare() using database/sql
|
||||||
|
func (m *DbMap) Prepare(query string) (*sql.Stmt, error) {
|
||||||
|
if m.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer m.trace(now, query, nil)
|
||||||
|
}
|
||||||
|
return prepare(m, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableOrNil(m *DbMap, t reflect.Type, name string) *TableMap {
|
||||||
|
if name != "" {
|
||||||
|
// Search by table name (dynamic tables)
|
||||||
|
if table, found := m.dynamicTableFind(name); found {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range m.tables {
|
||||||
|
table := m.tables[i]
|
||||||
|
if table.gotype == t {
|
||||||
|
return table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) tableForPointer(ptr interface{}, checkPK bool) (*TableMap, reflect.Value, error) {
|
||||||
|
ptrv := reflect.ValueOf(ptr)
|
||||||
|
if ptrv.Kind() != reflect.Ptr {
|
||||||
|
e := fmt.Sprintf("gorp: passed non-pointer: %v (kind=%v)", ptr,
|
||||||
|
ptrv.Kind())
|
||||||
|
return nil, reflect.Value{}, errors.New(e)
|
||||||
|
}
|
||||||
|
elem := ptrv.Elem()
|
||||||
|
ifc := elem.Interface()
|
||||||
|
var t *TableMap
|
||||||
|
var err error
|
||||||
|
tableName := ""
|
||||||
|
if dyn, isDyn := ptr.(DynamicTable); isDyn {
|
||||||
|
tableName = dyn.TableName()
|
||||||
|
t, err = m.DynamicTableFor(tableName, checkPK)
|
||||||
|
} else {
|
||||||
|
etype := reflect.TypeOf(ifc)
|
||||||
|
t, err = m.TableFor(etype, checkPK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, reflect.Value{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return t, elem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer m.trace(now, query, args...)
|
||||||
|
}
|
||||||
|
return queryRow(m, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) Query(q string, args ...interface{}) (*sql.Rows, error) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&q, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer m.trace(now, q, args...)
|
||||||
|
}
|
||||||
|
return query(m, q, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DbMap) trace(started time.Time, query string, args ...interface{}) {
|
||||||
|
if m.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.logger != nil {
|
||||||
|
var margs = argsString(args...)
|
||||||
|
m.logger.Printf("%s%s [%s] (%v)", m.logPrefix, query, margs, (time.Now().Sub(started)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringer interface {
|
||||||
|
ToStringSlice() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type numberer interface {
|
||||||
|
ToInt64Slice() []int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func expandSliceArgs(query *string, args ...interface{}) {
|
||||||
|
for _, arg := range args {
|
||||||
|
mapper, ok := arg.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value := range mapper {
|
||||||
|
var replacements []string
|
||||||
|
|
||||||
|
// add flexibility for any custom type to be convert to one of the
|
||||||
|
// acceptable formats.
|
||||||
|
if v, ok := value.(stringer); ok {
|
||||||
|
value = v.ToStringSlice()
|
||||||
|
}
|
||||||
|
if v, ok := value.(numberer); ok {
|
||||||
|
value = v.ToInt64Slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch v := value.(type) {
|
||||||
|
case []string:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []uint:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []uint8:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []uint16:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []uint32:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []uint64:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []int:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []int8:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []int16:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []int32:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []int64:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []float32:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
case []float64:
|
||||||
|
for id, replace := range v {
|
||||||
|
mapper[fmt.Sprintf("%s%d", key, id)] = replace
|
||||||
|
replacements = append(replacements, fmt.Sprintf(":%s%d", key, id))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(replacements) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
*query = strings.Replace(*query, fmt.Sprintf(":%s", key), strings.Join(replacements, ","), -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Dialect interface encapsulates behaviors that differ across
|
||||||
|
// SQL databases. At present the Dialect is only used by CreateTables()
|
||||||
|
// but this could change in the future
|
||||||
|
type Dialect interface {
|
||||||
|
// adds a suffix to any query, usually ";"
|
||||||
|
QuerySuffix() string
|
||||||
|
|
||||||
|
// ToSqlType returns the SQL column type to use when creating a
|
||||||
|
// table of the given Go Type. maxsize can be used to switch based on
|
||||||
|
// size. For example, in MySQL []byte could map to BLOB, MEDIUMBLOB,
|
||||||
|
// or LONGBLOB depending on the maxsize
|
||||||
|
ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string
|
||||||
|
|
||||||
|
// string to append to primary key column definitions
|
||||||
|
AutoIncrStr() string
|
||||||
|
|
||||||
|
// string to bind autoincrement columns to. Empty string will
|
||||||
|
// remove reference to those columns in the INSERT statement.
|
||||||
|
AutoIncrBindValue() string
|
||||||
|
|
||||||
|
AutoIncrInsertSuffix(col *ColumnMap) string
|
||||||
|
|
||||||
|
// string to append to "create table" statement for vendor specific
|
||||||
|
// table attributes
|
||||||
|
CreateTableSuffix() string
|
||||||
|
|
||||||
|
// string to append to "create index" statement
|
||||||
|
CreateIndexSuffix() string
|
||||||
|
|
||||||
|
// string to append to "drop index" statement
|
||||||
|
DropIndexSuffix() string
|
||||||
|
|
||||||
|
// string to truncate tables
|
||||||
|
TruncateClause() string
|
||||||
|
|
||||||
|
// bind variable string to use when forming SQL statements
|
||||||
|
// in many dbs it is "?", but Postgres appears to use $1
|
||||||
|
//
|
||||||
|
// i is a zero based index of the bind variable in this statement
|
||||||
|
//
|
||||||
|
BindVar(i int) string
|
||||||
|
|
||||||
|
// Handles quoting of a field name to ensure that it doesn't raise any
|
||||||
|
// SQL parsing exceptions by using a reserved word as a field name.
|
||||||
|
QuoteField(field string) string
|
||||||
|
|
||||||
|
// Handles building up of a schema.database string that is compatible with
|
||||||
|
// the given dialect
|
||||||
|
//
|
||||||
|
// schema - The schema that <table> lives in
|
||||||
|
// table - The table name
|
||||||
|
QuotedTableForQuery(schema string, table string) string
|
||||||
|
|
||||||
|
// Existence clause for table creation / deletion
|
||||||
|
IfSchemaNotExists(command, schema string) string
|
||||||
|
IfTableExists(command, schema, table string) string
|
||||||
|
IfTableNotExists(command, schema, table string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntegerAutoIncrInserter is implemented by dialects that can perform
|
||||||
|
// inserts with automatically incremented integer primary keys. If
|
||||||
|
// the dialect can handle automatic assignment of more than just
|
||||||
|
// integers, see TargetedAutoIncrInserter.
|
||||||
|
type IntegerAutoIncrInserter interface {
|
||||||
|
InsertAutoIncr(exec SqlExecutor, insertSql string, params ...interface{}) (int64, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetedAutoIncrInserter is implemented by dialects that can
|
||||||
|
// perform automatic assignment of any primary key type (i.e. strings
|
||||||
|
// for uuids, integers for serials, etc).
|
||||||
|
type TargetedAutoIncrInserter interface {
|
||||||
|
// InsertAutoIncrToTarget runs an insert operation and assigns the
|
||||||
|
// automatically generated primary key directly to the passed in
|
||||||
|
// target. The target should be a pointer to the primary key
|
||||||
|
// field of the value being inserted.
|
||||||
|
InsertAutoIncrToTarget(exec SqlExecutor, insertSql string, target interface{}, params ...interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetQueryInserter is implemented by dialects that can perform
|
||||||
|
// assignment of integer primary key type by executing a query
|
||||||
|
// like "select sequence.currval from dual".
|
||||||
|
type TargetQueryInserter interface {
|
||||||
|
// TargetQueryInserter runs an insert operation and assigns the
|
||||||
|
// automatically generated primary key retrived by the query
|
||||||
|
// extracted from the GeneratedIdQuery field of the id column.
|
||||||
|
InsertQueryToTarget(exec SqlExecutor, insertSql, idSql string, target interface{}, params ...interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func standardInsertAutoIncr(exec SqlExecutor, insertSql string, params ...interface{}) (int64, error) {
|
||||||
|
res, err := exec.Exec(insertSql, params...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.LastInsertId()
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implementation of Dialect for MySQL databases.
|
||||||
|
type MySQLDialect struct {
|
||||||
|
|
||||||
|
// Engine is the storage engine to use "InnoDB" vs "MyISAM" for example
|
||||||
|
Engine string
|
||||||
|
|
||||||
|
// Encoding is the character encoding to use for created tables
|
||||||
|
Encoding string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) QuerySuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d MySQLDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Int8:
|
||||||
|
return "tinyint"
|
||||||
|
case reflect.Uint8:
|
||||||
|
return "tinyint unsigned"
|
||||||
|
case reflect.Int16:
|
||||||
|
return "smallint"
|
||||||
|
case reflect.Uint16:
|
||||||
|
return "smallint unsigned"
|
||||||
|
case reflect.Int, reflect.Int32:
|
||||||
|
return "int"
|
||||||
|
case reflect.Uint, reflect.Uint32:
|
||||||
|
return "int unsigned"
|
||||||
|
case reflect.Int64:
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Uint64:
|
||||||
|
return "bigint unsigned"
|
||||||
|
case reflect.Float64, reflect.Float32:
|
||||||
|
return "double"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "mediumblob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "bigint"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "double"
|
||||||
|
case "NullBool":
|
||||||
|
return "tinyint"
|
||||||
|
case "Time":
|
||||||
|
return "datetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize < 1 {
|
||||||
|
maxsize = 255
|
||||||
|
}
|
||||||
|
|
||||||
|
/* == About varchar(N) ==
|
||||||
|
* N is number of characters.
|
||||||
|
* A varchar column can store up to 65535 bytes.
|
||||||
|
* Remember that 1 character is 3 bytes in utf-8 charset.
|
||||||
|
* Also remember that each row can store up to 65535 bytes,
|
||||||
|
* and you have some overheads, so it's not possible for a
|
||||||
|
* varchar column to have 65535/3 characters really.
|
||||||
|
* So it would be better to use 'text' type in stead of
|
||||||
|
* large varchar type.
|
||||||
|
*/
|
||||||
|
if maxsize < 256 {
|
||||||
|
return fmt.Sprintf("varchar(%d)", maxsize)
|
||||||
|
} else {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns auto_increment
|
||||||
|
func (d MySQLDialect) AutoIncrStr() string {
|
||||||
|
return "auto_increment"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) AutoIncrBindValue() string {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns engine=%s charset=%s based on values stored on struct
|
||||||
|
func (d MySQLDialect) CreateTableSuffix() string {
|
||||||
|
if d.Engine == "" || d.Encoding == "" {
|
||||||
|
msg := "gorp - undefined"
|
||||||
|
|
||||||
|
if d.Engine == "" {
|
||||||
|
msg += " MySQLDialect.Engine"
|
||||||
|
}
|
||||||
|
if d.Engine == "" && d.Encoding == "" {
|
||||||
|
msg += ","
|
||||||
|
}
|
||||||
|
if d.Encoding == "" {
|
||||||
|
msg += " MySQLDialect.Encoding"
|
||||||
|
}
|
||||||
|
msg += ". Check that your MySQLDialect was correctly initialized when declared."
|
||||||
|
panic(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(" engine=%s charset=%s", d.Engine, d.Encoding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) CreateIndexSuffix() string {
|
||||||
|
return "using"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) DropIndexSuffix() string {
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) TruncateClause() string {
|
||||||
|
return "truncate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) SleepClause(s time.Duration) string {
|
||||||
|
return fmt.Sprintf("sleep(%f)", s.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "?"
|
||||||
|
func (d MySQLDialect) BindVar(i int) string {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) InsertAutoIncr(exec SqlExecutor, insertSql string, params ...interface{}) (int64, error) {
|
||||||
|
return standardInsertAutoIncr(exec, insertSql, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) QuoteField(f string) string {
|
||||||
|
return "`" + f + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
if strings.TrimSpace(schema) == "" {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema + "." + d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d MySQLDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implementation of Dialect for Oracle databases.
|
||||||
|
type OracleDialect struct{}
|
||||||
|
|
||||||
|
func (d OracleDialect) QuerySuffix() string { return "" }
|
||||||
|
|
||||||
|
func (d OracleDialect) CreateIndexSuffix() string { return "" }
|
||||||
|
|
||||||
|
func (d OracleDialect) DropIndexSuffix() string { return "" }
|
||||||
|
|
||||||
|
func (d OracleDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||||
|
if isAutoIncr {
|
||||||
|
return "serial"
|
||||||
|
}
|
||||||
|
return "integer"
|
||||||
|
case reflect.Int64, reflect.Uint64:
|
||||||
|
if isAutoIncr {
|
||||||
|
return "bigserial"
|
||||||
|
}
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Float64:
|
||||||
|
return "double precision"
|
||||||
|
case reflect.Float32:
|
||||||
|
return "real"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "bytea"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "bigint"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "double precision"
|
||||||
|
case "NullBool":
|
||||||
|
return "boolean"
|
||||||
|
case "NullTime", "Time":
|
||||||
|
return "timestamp with time zone"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize > 0 {
|
||||||
|
return fmt.Sprintf("varchar(%d)", maxsize)
|
||||||
|
} else {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns empty string
|
||||||
|
func (d OracleDialect) AutoIncrStr() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) AutoIncrBindValue() string {
|
||||||
|
return "NULL"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns suffix
|
||||||
|
func (d OracleDialect) CreateTableSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) TruncateClause() string {
|
||||||
|
return "truncate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "$(i+1)"
|
||||||
|
func (d OracleDialect) BindVar(i int) string {
|
||||||
|
return fmt.Sprintf(":%d", i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// After executing the insert uses the ColMap IdQuery to get the generated id
|
||||||
|
func (d OracleDialect) InsertQueryToTarget(exec SqlExecutor, insertSql, idSql string, target interface{}, params ...interface{}) error {
|
||||||
|
_, err := exec.Exec(insertSql, params...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
id, err := exec.SelectInt(idSql)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch target.(type) {
|
||||||
|
case *int64:
|
||||||
|
*(target.(*int64)) = id
|
||||||
|
case *int32:
|
||||||
|
*(target.(*int32)) = int32(id)
|
||||||
|
case int:
|
||||||
|
*(target.(*int)) = int(id)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Id field can be int, int32 or int64")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) QuoteField(f string) string {
|
||||||
|
return `"` + strings.ToUpper(f) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
if strings.TrimSpace(schema) == "" {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema + "." + d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d OracleDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
|
@ -0,0 +1,149 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresDialect struct {
|
||||||
|
suffix string
|
||||||
|
LowercaseFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) QuerySuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d PostgresDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32:
|
||||||
|
if isAutoIncr {
|
||||||
|
return "serial"
|
||||||
|
}
|
||||||
|
return "integer"
|
||||||
|
case reflect.Int64, reflect.Uint64:
|
||||||
|
if isAutoIncr {
|
||||||
|
return "bigserial"
|
||||||
|
}
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Float64:
|
||||||
|
return "double precision"
|
||||||
|
case reflect.Float32:
|
||||||
|
return "real"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "bytea"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "bigint"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "double precision"
|
||||||
|
case "NullBool":
|
||||||
|
return "boolean"
|
||||||
|
case "Time", "NullTime":
|
||||||
|
return "timestamp with time zone"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize > 0 {
|
||||||
|
return fmt.Sprintf("varchar(%d)", maxsize)
|
||||||
|
} else {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns empty string
|
||||||
|
func (d PostgresDialect) AutoIncrStr() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) AutoIncrBindValue() string {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return " returning " + d.QuoteField(col.ColumnName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns suffix
|
||||||
|
func (d PostgresDialect) CreateTableSuffix() string {
|
||||||
|
return d.suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) CreateIndexSuffix() string {
|
||||||
|
return "using"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) DropIndexSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) TruncateClause() string {
|
||||||
|
return "truncate"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) SleepClause(s time.Duration) string {
|
||||||
|
return fmt.Sprintf("pg_sleep(%f)", s.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "$(i+1)"
|
||||||
|
func (d PostgresDialect) BindVar(i int) string {
|
||||||
|
return fmt.Sprintf("$%d", i+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) InsertAutoIncrToTarget(exec SqlExecutor, insertSql string, target interface{}, params ...interface{}) error {
|
||||||
|
rows, err := exec.Query(insertSql, params...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return fmt.Errorf("No serial value returned for insert: %s Encountered error: %s", insertSql, rows.Err())
|
||||||
|
}
|
||||||
|
if err := rows.Scan(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rows.Next() {
|
||||||
|
return fmt.Errorf("more than two serial value returned for insert: %s", insertSql)
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) QuoteField(f string) string {
|
||||||
|
if d.LowercaseFields {
|
||||||
|
return `"` + strings.ToLower(f) + `"`
|
||||||
|
}
|
||||||
|
return `"` + f + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
if strings.TrimSpace(schema) == "" {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema + "." + d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d PostgresDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SnowflakeDialect struct {
|
||||||
|
suffix string
|
||||||
|
LowercaseFields bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) QuerySuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "boolean"
|
||||||
|
case reflect.Int,
|
||||||
|
reflect.Int8,
|
||||||
|
reflect.Int16,
|
||||||
|
reflect.Int32,
|
||||||
|
reflect.Uint,
|
||||||
|
reflect.Uint8,
|
||||||
|
reflect.Uint16,
|
||||||
|
reflect.Uint32:
|
||||||
|
|
||||||
|
if isAutoIncr {
|
||||||
|
return "serial"
|
||||||
|
}
|
||||||
|
return "integer"
|
||||||
|
case reflect.Int64, reflect.Uint64:
|
||||||
|
if isAutoIncr {
|
||||||
|
return "bigserial"
|
||||||
|
}
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Float64:
|
||||||
|
return "double precision"
|
||||||
|
case reflect.Float32:
|
||||||
|
return "real"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "binary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "bigint"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "double precision"
|
||||||
|
case "NullBool":
|
||||||
|
return "boolean"
|
||||||
|
case "Time", "NullTime":
|
||||||
|
return "timestamp with time zone"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize > 0 {
|
||||||
|
return fmt.Sprintf("varchar(%d)", maxsize)
|
||||||
|
} else {
|
||||||
|
return "text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns empty string
|
||||||
|
func (d SnowflakeDialect) AutoIncrStr() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) AutoIncrBindValue() string {
|
||||||
|
return "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns suffix
|
||||||
|
func (d SnowflakeDialect) CreateTableSuffix() string {
|
||||||
|
return d.suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) CreateIndexSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) DropIndexSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) TruncateClause() string {
|
||||||
|
return "truncate"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "$(i+1)"
|
||||||
|
func (d SnowflakeDialect) BindVar(i int) string {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) InsertAutoIncrToTarget(exec SqlExecutor, insertSql string, target interface{}, params ...interface{}) error {
|
||||||
|
rows, err := exec.Query(insertSql, params...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
return fmt.Errorf("No serial value returned for insert: %s Encountered error: %s", insertSql, rows.Err())
|
||||||
|
}
|
||||||
|
if err := rows.Scan(target); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rows.Next() {
|
||||||
|
return fmt.Errorf("more than two serial value returned for insert: %s", insertSql)
|
||||||
|
}
|
||||||
|
return rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) QuoteField(f string) string {
|
||||||
|
if d.LowercaseFields {
|
||||||
|
return `"` + strings.ToLower(f) + `"`
|
||||||
|
}
|
||||||
|
return `"` + f + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
if strings.TrimSpace(schema) == "" {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
return schema + "." + d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SnowflakeDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqliteDialect struct {
|
||||||
|
suffix string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) QuerySuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d SqliteDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "integer"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||||
|
return "integer"
|
||||||
|
case reflect.Float64, reflect.Float32:
|
||||||
|
return "real"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "blob"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "integer"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "real"
|
||||||
|
case "NullBool":
|
||||||
|
return "integer"
|
||||||
|
case "Time":
|
||||||
|
return "datetime"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize < 1 {
|
||||||
|
maxsize = 255
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("varchar(%d)", maxsize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns autoincrement
|
||||||
|
func (d SqliteDialect) AutoIncrStr() string {
|
||||||
|
return "autoincrement"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) AutoIncrBindValue() string {
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns suffix
|
||||||
|
func (d SqliteDialect) CreateTableSuffix() string {
|
||||||
|
return d.suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) CreateIndexSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) DropIndexSuffix() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// With sqlite, there technically isn't a TRUNCATE statement,
|
||||||
|
// but a DELETE FROM uses a truncate optimization:
|
||||||
|
// http://www.sqlite.org/lang_delete.html
|
||||||
|
func (d SqliteDialect) TruncateClause() string {
|
||||||
|
return "delete from"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "?"
|
||||||
|
func (d SqliteDialect) BindVar(i int) string {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) InsertAutoIncr(exec SqlExecutor, insertSql string, params ...interface{}) (int64, error) {
|
||||||
|
return standardInsertAutoIncr(exec, insertSql, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) QuoteField(f string) string {
|
||||||
|
return `"` + f + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// sqlite does not have schemas like PostgreSQL does, so just escape it like normal
|
||||||
|
func (d SqliteDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if exists", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqliteDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
return fmt.Sprintf("%s if not exists", command)
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Implementation of Dialect for Microsoft SQL Server databases.
|
||||||
|
// Use gorp.SqlServerDialect{"2005"} for legacy datatypes.
|
||||||
|
// Tested with driver: github.com/denisenkom/go-mssqldb
|
||||||
|
|
||||||
|
type SqlServerDialect struct {
|
||||||
|
|
||||||
|
// If set to "2005" legacy datatypes will be used
|
||||||
|
Version string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) ToSqlType(val reflect.Type, maxsize int, isAutoIncr bool) string {
|
||||||
|
switch val.Kind() {
|
||||||
|
case reflect.Ptr:
|
||||||
|
return d.ToSqlType(val.Elem(), maxsize, isAutoIncr)
|
||||||
|
case reflect.Bool:
|
||||||
|
return "bit"
|
||||||
|
case reflect.Int8:
|
||||||
|
return "tinyint"
|
||||||
|
case reflect.Uint8:
|
||||||
|
return "smallint"
|
||||||
|
case reflect.Int16:
|
||||||
|
return "smallint"
|
||||||
|
case reflect.Uint16:
|
||||||
|
return "int"
|
||||||
|
case reflect.Int, reflect.Int32:
|
||||||
|
return "int"
|
||||||
|
case reflect.Uint, reflect.Uint32:
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Int64:
|
||||||
|
return "bigint"
|
||||||
|
case reflect.Uint64:
|
||||||
|
return "numeric(20,0)"
|
||||||
|
case reflect.Float32:
|
||||||
|
return "float(24)"
|
||||||
|
case reflect.Float64:
|
||||||
|
return "float(53)"
|
||||||
|
case reflect.Slice:
|
||||||
|
if val.Elem().Kind() == reflect.Uint8 {
|
||||||
|
return "varbinary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch val.Name() {
|
||||||
|
case "NullInt64":
|
||||||
|
return "bigint"
|
||||||
|
case "NullFloat64":
|
||||||
|
return "float(53)"
|
||||||
|
case "NullBool":
|
||||||
|
return "bit"
|
||||||
|
case "NullTime", "Time":
|
||||||
|
if d.Version == "2005" {
|
||||||
|
return "datetime"
|
||||||
|
}
|
||||||
|
return "datetime2"
|
||||||
|
}
|
||||||
|
|
||||||
|
if maxsize < 1 {
|
||||||
|
if d.Version == "2005" {
|
||||||
|
maxsize = 255
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("nvarchar(max)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("nvarchar(%d)", maxsize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns auto_increment
|
||||||
|
func (d SqlServerDialect) AutoIncrStr() string {
|
||||||
|
return "identity(0,1)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string removes autoincrement columns from the INSERT statements.
|
||||||
|
func (d SqlServerDialect) AutoIncrBindValue() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) AutoIncrInsertSuffix(col *ColumnMap) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) CreateTableSuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d SqlServerDialect) TruncateClause() string {
|
||||||
|
return "truncate table"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns "?"
|
||||||
|
func (d SqlServerDialect) BindVar(i int) string {
|
||||||
|
return "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) InsertAutoIncr(exec SqlExecutor, insertSql string, params ...interface{}) (int64, error) {
|
||||||
|
return standardInsertAutoIncr(exec, insertSql, params...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) QuoteField(f string) string {
|
||||||
|
return "[" + strings.Replace(f, "]", "]]", -1) + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) QuotedTableForQuery(schema string, table string) string {
|
||||||
|
if strings.TrimSpace(schema) == "" {
|
||||||
|
return d.QuoteField(table)
|
||||||
|
}
|
||||||
|
return d.QuoteField(schema) + "." + d.QuoteField(table)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) QuerySuffix() string { return ";" }
|
||||||
|
|
||||||
|
func (d SqlServerDialect) IfSchemaNotExists(command, schema string) string {
|
||||||
|
s := fmt.Sprintf("if schema_id(N'%s') is null %s", schema, command)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) IfTableExists(command, schema, table string) string {
|
||||||
|
var schema_clause string
|
||||||
|
if strings.TrimSpace(schema) != "" {
|
||||||
|
schema_clause = fmt.Sprintf("%s.", d.QuoteField(schema))
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("if object_id('%s%s') is not null %s", schema_clause, d.QuoteField(table), command)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) IfTableNotExists(command, schema, table string) string {
|
||||||
|
var schema_clause string
|
||||||
|
if strings.TrimSpace(schema) != "" {
|
||||||
|
schema_clause = fmt.Sprintf("%s.", schema)
|
||||||
|
}
|
||||||
|
s := fmt.Sprintf("if object_id('%s%s') is null %s", schema_clause, table, command)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d SqlServerDialect) CreateIndexSuffix() string { return "" }
|
||||||
|
func (d SqlServerDialect) DropIndexSuffix() string { return "" }
|
|
@ -0,0 +1,11 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package gorp provides a simple way to marshal Go structs to and from
|
||||||
|
// SQL databases. It uses the database/sql package, and should work with any
|
||||||
|
// compliant database/sql driver.
|
||||||
|
//
|
||||||
|
// Source code and project home:
|
||||||
|
// https://github.com/go-gorp/gorp
|
||||||
|
package gorp
|
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A non-fatal error, when a select query returns columns that do not exist
|
||||||
|
// as fields in the struct it is being mapped to
|
||||||
|
// TODO: discuss wether this needs an error. encoding/json silently ignores missing fields
|
||||||
|
type NoFieldInTypeError struct {
|
||||||
|
TypeName string
|
||||||
|
MissingColNames []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *NoFieldInTypeError) Error() string {
|
||||||
|
return fmt.Sprintf("gorp: no fields %+v in type %s", err.MissingColNames, err.TypeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns true if the error is non-fatal (ie, we shouldn't immediately return)
|
||||||
|
func NonFatalError(err error) bool {
|
||||||
|
switch err.(type) {
|
||||||
|
case *NoFieldInTypeError:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,672 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OracleString (empty string is null)
|
||||||
|
// TODO: move to dialect/oracle?, rename to String?
|
||||||
|
type OracleString struct {
|
||||||
|
sql.NullString
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (os *OracleString) Scan(value interface{}) error {
|
||||||
|
if value == nil {
|
||||||
|
os.String, os.Valid = "", false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
os.Valid = true
|
||||||
|
return os.NullString.Scan(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (os OracleString) Value() (driver.Value, error) {
|
||||||
|
if !os.Valid || os.String == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return os.String, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlTyper is a type that returns its database type. Most of the
|
||||||
|
// time, the type can just use "database/sql/driver".Valuer; but when
|
||||||
|
// it returns nil for its empty value, it needs to implement SqlTyper
|
||||||
|
// to have its column type detected properly during table creation.
|
||||||
|
type SqlTyper interface {
|
||||||
|
SqlType() driver.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// legacySqlTyper prevents breaking clients who depended on the previous
|
||||||
|
// SqlTyper interface
|
||||||
|
type legacySqlTyper interface {
|
||||||
|
SqlType() driver.Valuer
|
||||||
|
}
|
||||||
|
|
||||||
|
// for fields that exists in DB table, but not exists in struct
|
||||||
|
type dummyField struct{}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (nt *dummyField) Scan(value interface{}) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroVal reflect.Value
|
||||||
|
var versFieldConst = "[gorp_ver_field]"
|
||||||
|
|
||||||
|
// The TypeConverter interface provides a way to map a value of one
|
||||||
|
// type to another type when persisting to, or loading from, a database.
|
||||||
|
//
|
||||||
|
// Example use cases: Implement type converter to convert bool types to "y"/"n" strings,
|
||||||
|
// or serialize a struct member as a JSON blob.
|
||||||
|
type TypeConverter interface {
|
||||||
|
// ToDb converts val to another type. Called before INSERT/UPDATE operations
|
||||||
|
ToDb(val interface{}) (interface{}, error)
|
||||||
|
|
||||||
|
// FromDb returns a CustomScanner appropriate for this type. This will be used
|
||||||
|
// to hold values returned from SELECT queries.
|
||||||
|
//
|
||||||
|
// In particular the CustomScanner returned should implement a Binder
|
||||||
|
// function appropriate for the Go type you wish to convert the db value to
|
||||||
|
//
|
||||||
|
// If bool==false, then no custom scanner will be used for this field.
|
||||||
|
FromDb(target interface{}) (CustomScanner, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlExecutor exposes gorp operations that can be run from Pre/Post
|
||||||
|
// hooks. This hides whether the current operation that triggered the
|
||||||
|
// hook is in a transaction.
|
||||||
|
//
|
||||||
|
// See the DbMap function docs for each of the functions below for more
|
||||||
|
// information.
|
||||||
|
type SqlExecutor interface {
|
||||||
|
WithContext(ctx context.Context) SqlExecutor
|
||||||
|
Get(i interface{}, keys ...interface{}) (interface{}, error)
|
||||||
|
Insert(list ...interface{}) error
|
||||||
|
Update(list ...interface{}) (int64, error)
|
||||||
|
Delete(list ...interface{}) (int64, error)
|
||||||
|
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||||
|
Select(i interface{}, query string, args ...interface{}) ([]interface{}, error)
|
||||||
|
SelectInt(query string, args ...interface{}) (int64, error)
|
||||||
|
SelectNullInt(query string, args ...interface{}) (sql.NullInt64, error)
|
||||||
|
SelectFloat(query string, args ...interface{}) (float64, error)
|
||||||
|
SelectNullFloat(query string, args ...interface{}) (sql.NullFloat64, error)
|
||||||
|
SelectStr(query string, args ...interface{}) (string, error)
|
||||||
|
SelectNullStr(query string, args ...interface{}) (sql.NullString, error)
|
||||||
|
SelectOne(holder interface{}, query string, args ...interface{}) error
|
||||||
|
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||||
|
QueryRow(query string, args ...interface{}) *sql.Row
|
||||||
|
}
|
||||||
|
|
||||||
|
// DynamicTable allows the users of gorp to dynamically
|
||||||
|
// use different database table names during runtime
|
||||||
|
// while sharing the same golang struct for in-memory data
|
||||||
|
type DynamicTable interface {
|
||||||
|
TableName() string
|
||||||
|
SetTableName(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile-time check that DbMap and Transaction implement the SqlExecutor
|
||||||
|
// interface.
|
||||||
|
var _, _ SqlExecutor = &DbMap{}, &Transaction{}
|
||||||
|
|
||||||
|
func argValue(a interface{}) interface{} {
|
||||||
|
v, ok := a.(driver.Valuer)
|
||||||
|
if !ok {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
vV := reflect.ValueOf(v)
|
||||||
|
if vV.Kind() == reflect.Ptr && vV.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ret, err := v.Value()
|
||||||
|
if err != nil {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func argsString(args ...interface{}) string {
|
||||||
|
var margs string
|
||||||
|
for i, a := range args {
|
||||||
|
v := argValue(a)
|
||||||
|
switch v.(type) {
|
||||||
|
case string:
|
||||||
|
v = fmt.Sprintf("%q", v)
|
||||||
|
default:
|
||||||
|
v = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
margs += fmt.Sprintf("%d:%s", i+1, v)
|
||||||
|
if i+1 < len(args) {
|
||||||
|
margs += " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return margs
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calls the Exec function on the executor, but attempts to expand any eligible named
|
||||||
|
// query arguments first.
|
||||||
|
func maybeExpandNamedQueryAndExec(e SqlExecutor, query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
dbMap := extractDbMap(e)
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
query, args = maybeExpandNamedQuery(dbMap, query, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec(e, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractDbMap(e SqlExecutor) *DbMap {
|
||||||
|
switch m := e.(type) {
|
||||||
|
case *DbMap:
|
||||||
|
return m
|
||||||
|
case *Transaction:
|
||||||
|
return m.dbmap
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// executor exposes the sql.DB and sql.Tx functions so that it can be used
|
||||||
|
// on internal functions that need to be agnostic to the underlying object.
|
||||||
|
type executor interface {
|
||||||
|
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||||
|
Prepare(query string) (*sql.Stmt, error)
|
||||||
|
QueryRow(query string, args ...interface{}) *sql.Row
|
||||||
|
Query(query string, args ...interface{}) (*sql.Rows, error)
|
||||||
|
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
||||||
|
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
|
||||||
|
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
||||||
|
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractExecutorAndContext(e SqlExecutor) (executor, context.Context) {
|
||||||
|
switch m := e.(type) {
|
||||||
|
case *DbMap:
|
||||||
|
return m.Db, m.ctx
|
||||||
|
case *Transaction:
|
||||||
|
return m.tx, m.ctx
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeExpandNamedQuery checks the given arg to see if it's eligible to be used
|
||||||
|
// as input to a named query. If so, it rewrites the query to use
|
||||||
|
// dialect-dependent bindvars and instantiates the corresponding slice of
|
||||||
|
// parameters by extracting data from the map / struct.
|
||||||
|
// If not, returns the input values unchanged.
|
||||||
|
func maybeExpandNamedQuery(m *DbMap, query string, args []interface{}) (string, []interface{}) {
|
||||||
|
var (
|
||||||
|
arg = args[0]
|
||||||
|
argval = reflect.ValueOf(arg)
|
||||||
|
)
|
||||||
|
if argval.Kind() == reflect.Ptr {
|
||||||
|
argval = argval.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if argval.Kind() == reflect.Map && argval.Type().Key().Kind() == reflect.String {
|
||||||
|
return expandNamedQuery(m, query, func(key string) reflect.Value {
|
||||||
|
return argval.MapIndex(reflect.ValueOf(key))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if argval.Kind() != reflect.Struct {
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
if _, ok := arg.(time.Time); ok {
|
||||||
|
// time.Time is driver.Value
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
if _, ok := arg.(driver.Valuer); ok {
|
||||||
|
// driver.Valuer will be converted to driver.Value.
|
||||||
|
return query, args
|
||||||
|
}
|
||||||
|
|
||||||
|
return expandNamedQuery(m, query, argval.FieldByName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyRegexp = regexp.MustCompile(`:[[:word:]]+`)
|
||||||
|
|
||||||
|
// expandNamedQuery accepts a query with placeholders of the form ":key", and a
|
||||||
|
// single arg of Kind Struct or Map[string]. It returns the query with the
|
||||||
|
// dialect's placeholders, and a slice of args ready for positional insertion
|
||||||
|
// into the query.
|
||||||
|
func expandNamedQuery(m *DbMap, query string, keyGetter func(key string) reflect.Value) (string, []interface{}) {
|
||||||
|
var (
|
||||||
|
n int
|
||||||
|
args []interface{}
|
||||||
|
)
|
||||||
|
return keyRegexp.ReplaceAllStringFunc(query, func(key string) string {
|
||||||
|
val := keyGetter(key[1:])
|
||||||
|
if !val.IsValid() {
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
args = append(args, val.Interface())
|
||||||
|
newVar := m.Dialect.BindVar(n)
|
||||||
|
n++
|
||||||
|
return newVar
|
||||||
|
}), args
|
||||||
|
}
|
||||||
|
|
||||||
|
func columnToFieldIndex(m *DbMap, t reflect.Type, name string, cols []string) ([][]int, error) {
|
||||||
|
colToFieldIndex := make([][]int, len(cols))
|
||||||
|
|
||||||
|
// check if type t is a mapped table - if so we'll
|
||||||
|
// check the table for column aliasing below
|
||||||
|
tableMapped := false
|
||||||
|
table := tableOrNil(m, t, name)
|
||||||
|
if table != nil {
|
||||||
|
tableMapped = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over column names and find field in i to bind to
|
||||||
|
// based on column name. all returned columns must match
|
||||||
|
// a field in the i struct
|
||||||
|
missingColNames := []string{}
|
||||||
|
for x := range cols {
|
||||||
|
colName := strings.ToLower(cols[x])
|
||||||
|
field, found := t.FieldByNameFunc(func(fieldName string) bool {
|
||||||
|
field, _ := t.FieldByName(fieldName)
|
||||||
|
cArguments := strings.Split(field.Tag.Get("db"), ",")
|
||||||
|
fieldName = cArguments[0]
|
||||||
|
|
||||||
|
if fieldName == "-" {
|
||||||
|
return false
|
||||||
|
} else if fieldName == "" {
|
||||||
|
fieldName = field.Name
|
||||||
|
}
|
||||||
|
if tableMapped {
|
||||||
|
colMap := colMapOrNil(table, fieldName)
|
||||||
|
if colMap != nil {
|
||||||
|
fieldName = colMap.ColumnName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colName == strings.ToLower(fieldName)
|
||||||
|
})
|
||||||
|
if found {
|
||||||
|
colToFieldIndex[x] = field.Index
|
||||||
|
}
|
||||||
|
if colToFieldIndex[x] == nil {
|
||||||
|
missingColNames = append(missingColNames, colName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(missingColNames) > 0 {
|
||||||
|
return colToFieldIndex, &NoFieldInTypeError{
|
||||||
|
TypeName: t.Name(),
|
||||||
|
MissingColNames: missingColNames,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colToFieldIndex, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fieldByName(val reflect.Value, fieldName string) *reflect.Value {
|
||||||
|
// try to find field by exact match
|
||||||
|
f := val.FieldByName(fieldName)
|
||||||
|
|
||||||
|
if f != zeroVal {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
|
||||||
|
// try to find by case insensitive match - only the Postgres driver
|
||||||
|
// seems to require this - in the case where columns are aliased in the sql
|
||||||
|
fieldNameL := strings.ToLower(fieldName)
|
||||||
|
fieldCount := val.NumField()
|
||||||
|
t := val.Type()
|
||||||
|
for i := 0; i < fieldCount; i++ {
|
||||||
|
sf := t.Field(i)
|
||||||
|
if strings.ToLower(sf.Name) == fieldNameL {
|
||||||
|
f := val.Field(i)
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSliceType returns the element type of the given object, if the object is a
|
||||||
|
// "*[]*Element" or "*[]Element". If not, returns nil.
|
||||||
|
// err is returned if the user was trying to pass a pointer-to-slice but failed.
|
||||||
|
func toSliceType(i interface{}) (reflect.Type, error) {
|
||||||
|
t := reflect.TypeOf(i)
|
||||||
|
if t.Kind() != reflect.Ptr {
|
||||||
|
// If it's a slice, return a more helpful error message
|
||||||
|
if t.Kind() == reflect.Slice {
|
||||||
|
return nil, fmt.Errorf("gorp: cannot SELECT into a non-pointer slice: %v", t)
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if t = t.Elem(); t.Kind() != reflect.Slice {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return t.Elem(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toType(i interface{}) (reflect.Type, error) {
|
||||||
|
t := reflect.TypeOf(i)
|
||||||
|
|
||||||
|
// If a Pointer to a type, follow
|
||||||
|
for t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil, fmt.Errorf("gorp: cannot SELECT into this type: %v", reflect.TypeOf(i))
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type foundTable struct {
|
||||||
|
table *TableMap
|
||||||
|
dynName *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableFor(m *DbMap, t reflect.Type, i interface{}) (*foundTable, error) {
|
||||||
|
if dyn, isDynamic := i.(DynamicTable); isDynamic {
|
||||||
|
tableName := dyn.TableName()
|
||||||
|
table, err := m.DynamicTableFor(tableName, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &foundTable{
|
||||||
|
table: table,
|
||||||
|
dynName: &tableName,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
table, err := m.TableFor(t, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &foundTable{table: table}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func get(m *DbMap, exec SqlExecutor, i interface{},
|
||||||
|
keys ...interface{}) (interface{}, error) {
|
||||||
|
|
||||||
|
t, err := toType(i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
foundTable, err := tableFor(m, t, i)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
table := foundTable.table
|
||||||
|
|
||||||
|
plan := table.bindGet()
|
||||||
|
|
||||||
|
v := reflect.New(t)
|
||||||
|
if foundTable.dynName != nil {
|
||||||
|
retDyn := v.Interface().(DynamicTable)
|
||||||
|
retDyn.SetTableName(*foundTable.dynName)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := make([]interface{}, len(plan.argFields))
|
||||||
|
|
||||||
|
conv := m.TypeConverter
|
||||||
|
custScan := make([]CustomScanner, 0)
|
||||||
|
|
||||||
|
for x, fieldName := range plan.argFields {
|
||||||
|
f := v.Elem().FieldByName(fieldName)
|
||||||
|
target := f.Addr().Interface()
|
||||||
|
if conv != nil {
|
||||||
|
scanner, ok := conv.FromDb(target)
|
||||||
|
if ok {
|
||||||
|
target = scanner.Holder
|
||||||
|
custScan = append(custScan, scanner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dest[x] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
row := exec.QueryRow(plan.query, keys...)
|
||||||
|
err = row.Scan(dest...)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range custScan {
|
||||||
|
err = c.Bind()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := v.Interface().(HasPostGet); ok {
|
||||||
|
err := v.PostGet(exec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Interface(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete(m *DbMap, exec SqlExecutor, list ...interface{}) (int64, error) {
|
||||||
|
count := int64(0)
|
||||||
|
for _, ptr := range list {
|
||||||
|
table, elem, err := m.tableForPointer(ptr, true)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eval := elem.Addr().Interface()
|
||||||
|
if v, ok := eval.(HasPreDelete); ok {
|
||||||
|
err = v.PreDelete(exec)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bi, err := table.bindDelete(elem)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := exec.Exec(bi.query, bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows == 0 && bi.existingVersion > 0 {
|
||||||
|
return lockError(m, exec, table.TableName,
|
||||||
|
bi.existingVersion, elem, bi.keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
count += rows
|
||||||
|
|
||||||
|
if v, ok := eval.(HasPostDelete); ok {
|
||||||
|
err := v.PostDelete(exec)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(m *DbMap, exec SqlExecutor, colFilter ColumnFilter, list ...interface{}) (int64, error) {
|
||||||
|
count := int64(0)
|
||||||
|
for _, ptr := range list {
|
||||||
|
table, elem, err := m.tableForPointer(ptr, true)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eval := elem.Addr().Interface()
|
||||||
|
if v, ok := eval.(HasPreUpdate); ok {
|
||||||
|
err = v.PreUpdate(exec)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bi, err := table.bindUpdate(elem, colFilter)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := exec.Exec(bi.query, bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if rows == 0 && bi.existingVersion > 0 {
|
||||||
|
return lockError(m, exec, table.TableName,
|
||||||
|
bi.existingVersion, elem, bi.keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bi.versField != "" {
|
||||||
|
elem.FieldByName(bi.versField).SetInt(bi.existingVersion + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
count += rows
|
||||||
|
|
||||||
|
if v, ok := eval.(HasPostUpdate); ok {
|
||||||
|
err = v.PostUpdate(exec)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func insert(m *DbMap, exec SqlExecutor, list ...interface{}) error {
|
||||||
|
for _, ptr := range list {
|
||||||
|
table, elem, err := m.tableForPointer(ptr, false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
eval := elem.Addr().Interface()
|
||||||
|
if v, ok := eval.(HasPreInsert); ok {
|
||||||
|
err := v.PreInsert(exec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bi, err := table.bindInsert(elem)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if bi.autoIncrIdx > -1 {
|
||||||
|
f := elem.FieldByName(bi.autoIncrFieldName)
|
||||||
|
switch inserter := m.Dialect.(type) {
|
||||||
|
case IntegerAutoIncrInserter:
|
||||||
|
id, err := inserter.InsertAutoIncr(exec, bi.query, bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
k := f.Kind()
|
||||||
|
if (k == reflect.Int) || (k == reflect.Int16) || (k == reflect.Int32) || (k == reflect.Int64) {
|
||||||
|
f.SetInt(id)
|
||||||
|
} else if (k == reflect.Uint) || (k == reflect.Uint16) || (k == reflect.Uint32) || (k == reflect.Uint64) {
|
||||||
|
f.SetUint(uint64(id))
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("gorp: cannot set autoincrement value on non-Int field. SQL=%s autoIncrIdx=%d autoIncrFieldName=%s", bi.query, bi.autoIncrIdx, bi.autoIncrFieldName)
|
||||||
|
}
|
||||||
|
case TargetedAutoIncrInserter:
|
||||||
|
err := inserter.InsertAutoIncrToTarget(exec, bi.query, f.Addr().Interface(), bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case TargetQueryInserter:
|
||||||
|
var idQuery = table.ColMap(bi.autoIncrFieldName).GeneratedIdQuery
|
||||||
|
if idQuery == "" {
|
||||||
|
return fmt.Errorf("gorp: cannot set %s value if its ColumnMap.GeneratedIdQuery is empty", bi.autoIncrFieldName)
|
||||||
|
}
|
||||||
|
err := inserter.InsertQueryToTarget(exec, bi.query, idQuery, f.Addr().Interface(), bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("gorp: cannot use autoincrement fields on dialects that do not implement an autoincrementing interface")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_, err := exec.Exec(bi.query, bi.args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := eval.(HasPostInsert); ok {
|
||||||
|
err := v.PostInsert(exec)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exec(e SqlExecutor, query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
executor, ctx := extractExecutorAndContext(e)
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
return executor.ExecContext(ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executor.Exec(query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepare(e SqlExecutor, query string) (*sql.Stmt, error) {
|
||||||
|
executor, ctx := extractExecutorAndContext(e)
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
return executor.PrepareContext(ctx, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executor.Prepare(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryRow(e SqlExecutor, query string, args ...interface{}) *sql.Row {
|
||||||
|
executor, ctx := extractExecutorAndContext(e)
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
return executor.QueryRowContext(ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executor.QueryRow(query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func query(e SqlExecutor, query string, args ...interface{}) (*sql.Rows, error) {
|
||||||
|
executor, ctx := extractExecutorAndContext(e)
|
||||||
|
|
||||||
|
if ctx != nil {
|
||||||
|
return executor.QueryContext(ctx, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return executor.Query(query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func begin(m *DbMap) (*sql.Tx, error) {
|
||||||
|
if m.ctx != nil {
|
||||||
|
return m.Db.BeginTx(m.ctx, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Db.Begin()
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
//++ TODO v2-phase3: HasPostGet => PostGetter, HasPostDelete => PostDeleter, etc.
|
||||||
|
|
||||||
|
// HasPostGet provides PostGet() which will be executed after the GET statement.
|
||||||
|
type HasPostGet interface {
|
||||||
|
PostGet(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPostDelete provides PostDelete() which will be executed after the DELETE statement
|
||||||
|
type HasPostDelete interface {
|
||||||
|
PostDelete(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPostUpdate provides PostUpdate() which will be executed after the UPDATE statement
|
||||||
|
type HasPostUpdate interface {
|
||||||
|
PostUpdate(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPostInsert provides PostInsert() which will be executed after the INSERT statement
|
||||||
|
type HasPostInsert interface {
|
||||||
|
PostInsert(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPreDelete provides PreDelete() which will be executed before the DELETE statement.
|
||||||
|
type HasPreDelete interface {
|
||||||
|
PreDelete(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPreUpdate provides PreUpdate() which will be executed before UPDATE statement.
|
||||||
|
type HasPreUpdate interface {
|
||||||
|
PreUpdate(SqlExecutor) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPreInsert provides PreInsert() which will be executed before INSERT statement.
|
||||||
|
type HasPreInsert interface {
|
||||||
|
PreInsert(SqlExecutor) error
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
// IndexMap represents a mapping between a Go struct field and a single
|
||||||
|
// index in a table.
|
||||||
|
// Unique and MaxSize only inform the
|
||||||
|
// CreateTables() function and are not used by Insert/Update/Delete/Get.
|
||||||
|
type IndexMap struct {
|
||||||
|
// Index name in db table
|
||||||
|
IndexName string
|
||||||
|
|
||||||
|
// If true, " unique" is added to create index statements.
|
||||||
|
// Not used elsewhere
|
||||||
|
Unique bool
|
||||||
|
|
||||||
|
// Index type supported by Dialect
|
||||||
|
// Postgres: B-tree, Hash, GiST and GIN.
|
||||||
|
// Mysql: Btree, Hash.
|
||||||
|
// Sqlite: nil.
|
||||||
|
IndexType string
|
||||||
|
|
||||||
|
// Columns name for single and multiple indexes
|
||||||
|
columns []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename allows you to specify the index name in the table
|
||||||
|
//
|
||||||
|
// Example: table.IndMap("customer_test_idx").Rename("customer_idx")
|
||||||
|
//
|
||||||
|
func (idx *IndexMap) Rename(indname string) *IndexMap {
|
||||||
|
idx.IndexName = indname
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUnique adds "unique" to the create index statements for this
|
||||||
|
// index, if b is true.
|
||||||
|
func (idx *IndexMap) SetUnique(b bool) *IndexMap {
|
||||||
|
idx.Unique = b
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIndexType specifies the index type supported by chousen SQL Dialect
|
||||||
|
func (idx *IndexMap) SetIndexType(indtype string) *IndexMap {
|
||||||
|
idx.IndexType = indtype
|
||||||
|
return idx
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OptimisticLockError is returned by Update() or Delete() if the
|
||||||
|
// struct being modified has a Version field and the value is not equal to
|
||||||
|
// the current value in the database
|
||||||
|
type OptimisticLockError struct {
|
||||||
|
// Table name where the lock error occurred
|
||||||
|
TableName string
|
||||||
|
|
||||||
|
// Primary key values of the row being updated/deleted
|
||||||
|
Keys []interface{}
|
||||||
|
|
||||||
|
// true if a row was found with those keys, indicating the
|
||||||
|
// LocalVersion is stale. false if no value was found with those
|
||||||
|
// keys, suggesting the row has been deleted since loaded, or
|
||||||
|
// was never inserted to begin with
|
||||||
|
RowExists bool
|
||||||
|
|
||||||
|
// Version value on the struct passed to Update/Delete. This value is
|
||||||
|
// out of sync with the database.
|
||||||
|
LocalVersion int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns a description of the cause of the lock error
|
||||||
|
func (e OptimisticLockError) Error() string {
|
||||||
|
if e.RowExists {
|
||||||
|
return fmt.Sprintf("gorp: OptimisticLockError table=%s keys=%v out of date version=%d", e.TableName, e.Keys, e.LocalVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("gorp: OptimisticLockError no row found for table=%s keys=%v", e.TableName, e.Keys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lockError(m *DbMap, exec SqlExecutor, tableName string,
|
||||||
|
existingVer int64, elem reflect.Value,
|
||||||
|
keys ...interface{}) (int64, error) {
|
||||||
|
|
||||||
|
existing, err := get(m, exec, elem.Interface(), keys...)
|
||||||
|
if err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ole := OptimisticLockError{tableName, keys, true, existingVer}
|
||||||
|
if existing == nil {
|
||||||
|
ole.RowExists = false
|
||||||
|
}
|
||||||
|
return -1, ole
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// GorpLogger is a deprecated alias of Logger.
|
||||||
|
type GorpLogger = Logger
|
||||||
|
|
||||||
|
// Logger is the type that gorp uses to log SQL statements.
|
||||||
|
// See DbMap.TraceOn.
|
||||||
|
type Logger interface {
|
||||||
|
Printf(format string, v ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceOn turns on SQL statement logging for this DbMap. After this is
|
||||||
|
// called, all SQL statements will be sent to the logger. If prefix is
|
||||||
|
// a non-empty string, it will be written to the front of all logged
|
||||||
|
// strings, which can aid in filtering log lines.
|
||||||
|
//
|
||||||
|
// Use TraceOn if you want to spy on the SQL statements that gorp
|
||||||
|
// generates.
|
||||||
|
//
|
||||||
|
// Note that the base log.Logger type satisfies Logger, but adapters can
|
||||||
|
// easily be written for other logging packages (e.g., the golang-sanctioned
|
||||||
|
// glog framework).
|
||||||
|
func (m *DbMap) TraceOn(prefix string, logger Logger) {
|
||||||
|
m.logger = logger
|
||||||
|
if prefix == "" {
|
||||||
|
m.logPrefix = prefix
|
||||||
|
} else {
|
||||||
|
m.logPrefix = fmt.Sprintf("%s ", prefix)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TraceOff turns off tracing. It is idempotent.
|
||||||
|
func (m *DbMap) TraceOff() {
|
||||||
|
m.logger = nil
|
||||||
|
m.logPrefix = ""
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A nullable Time value
|
||||||
|
type NullTime struct {
|
||||||
|
Time time.Time
|
||||||
|
Valid bool // Valid is true if Time is not NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan implements the Scanner interface.
|
||||||
|
func (nt *NullTime) Scan(value interface{}) error {
|
||||||
|
log.Printf("Time scan value is: %#v", value)
|
||||||
|
switch t := value.(type) {
|
||||||
|
case time.Time:
|
||||||
|
nt.Time, nt.Valid = t, true
|
||||||
|
case []byte:
|
||||||
|
v := strToTime(string(t))
|
||||||
|
if v != nil {
|
||||||
|
nt.Valid = true
|
||||||
|
nt.Time = *v
|
||||||
|
}
|
||||||
|
case string:
|
||||||
|
v := strToTime(t)
|
||||||
|
if v != nil {
|
||||||
|
nt.Valid = true
|
||||||
|
nt.Time = *v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func strToTime(v string) *time.Time {
|
||||||
|
for _, dtfmt := range []string{
|
||||||
|
"2006-01-02 15:04:05.999999999",
|
||||||
|
"2006-01-02T15:04:05.999999999",
|
||||||
|
"2006-01-02 15:04:05",
|
||||||
|
"2006-01-02T15:04:05",
|
||||||
|
"2006-01-02 15:04",
|
||||||
|
"2006-01-02T15:04",
|
||||||
|
"2006-01-02",
|
||||||
|
"2006-01-02 15:04:05-07:00",
|
||||||
|
} {
|
||||||
|
if t, err := time.Parse(dtfmt, v); err == nil {
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver Valuer interface.
|
||||||
|
func (nt NullTime) Value() (driver.Value, error) {
|
||||||
|
if !nt.Valid {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nt.Time, nil
|
||||||
|
}
|
|
@ -0,0 +1,359 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SelectInt executes the given query, which should be a SELECT statement for a single
|
||||||
|
// integer column, and returns the value of the first row returned. If no rows are
|
||||||
|
// found, zero is returned.
|
||||||
|
func SelectInt(e SqlExecutor, query string, args ...interface{}) (int64, error) {
|
||||||
|
var h int64
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullInt executes the given query, which should be a SELECT statement for a single
|
||||||
|
// integer column, and returns the value of the first row returned. If no rows are
|
||||||
|
// found, the empty sql.NullInt64 value is returned.
|
||||||
|
func SelectNullInt(e SqlExecutor, query string, args ...interface{}) (sql.NullInt64, error) {
|
||||||
|
var h sql.NullInt64
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFloat executes the given query, which should be a SELECT statement for a single
|
||||||
|
// float column, and returns the value of the first row returned. If no rows are
|
||||||
|
// found, zero is returned.
|
||||||
|
func SelectFloat(e SqlExecutor, query string, args ...interface{}) (float64, error) {
|
||||||
|
var h float64
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullFloat executes the given query, which should be a SELECT statement for a single
|
||||||
|
// float column, and returns the value of the first row returned. If no rows are
|
||||||
|
// found, the empty sql.NullInt64 value is returned.
|
||||||
|
func SelectNullFloat(e SqlExecutor, query string, args ...interface{}) (sql.NullFloat64, error) {
|
||||||
|
var h sql.NullFloat64
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectStr executes the given query, which should be a SELECT statement for a single
|
||||||
|
// char/varchar column, and returns the value of the first row returned. If no rows are
|
||||||
|
// found, an empty string is returned.
|
||||||
|
func SelectStr(e SqlExecutor, query string, args ...interface{}) (string, error) {
|
||||||
|
var h string
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullStr executes the given query, which should be a SELECT
|
||||||
|
// statement for a single char/varchar column, and returns the value
|
||||||
|
// of the first row returned. If no rows are found, the empty
|
||||||
|
// sql.NullString is returned.
|
||||||
|
func SelectNullStr(e SqlExecutor, query string, args ...interface{}) (sql.NullString, error) {
|
||||||
|
var h sql.NullString
|
||||||
|
err := selectVal(e, &h, query, args...)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return h, err
|
||||||
|
}
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectOne executes the given query (which should be a SELECT statement)
|
||||||
|
// and binds the result to holder, which must be a pointer.
|
||||||
|
//
|
||||||
|
// If no row is found, an error (sql.ErrNoRows specifically) will be returned
|
||||||
|
//
|
||||||
|
// If more than one row is found, an error will be returned.
|
||||||
|
//
|
||||||
|
func SelectOne(m *DbMap, e SqlExecutor, holder interface{}, query string, args ...interface{}) error {
|
||||||
|
t := reflect.TypeOf(holder)
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
t = t.Elem()
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("gorp: SelectOne holder must be a pointer, but got: %t", holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle pointer to pointer
|
||||||
|
isptr := false
|
||||||
|
if t.Kind() == reflect.Ptr {
|
||||||
|
isptr = true
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Kind() == reflect.Struct {
|
||||||
|
var nonFatalErr error
|
||||||
|
|
||||||
|
list, err := hookedselect(m, e, holder, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if !NonFatalError(err) { // FIXME: double negative, rename NonFatalError to FatalError
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nonFatalErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := reflect.ValueOf(holder)
|
||||||
|
if isptr {
|
||||||
|
dest = dest.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
if list != nil && len(list) > 0 { // FIXME: invert if/else
|
||||||
|
// check for multiple rows
|
||||||
|
if len(list) > 1 {
|
||||||
|
return fmt.Errorf("gorp: multiple rows returned for: %s - %v", query, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize if nil
|
||||||
|
if dest.IsNil() {
|
||||||
|
dest.Set(reflect.New(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
// only one row found
|
||||||
|
src := reflect.ValueOf(list[0])
|
||||||
|
dest.Elem().Set(src.Elem())
|
||||||
|
} else {
|
||||||
|
// No rows found, return a proper error.
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return nonFatalErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectVal(e, holder, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectVal(e SqlExecutor, holder interface{}, query string, args ...interface{}) error {
|
||||||
|
if len(args) == 1 {
|
||||||
|
switch m := e.(type) {
|
||||||
|
case *DbMap:
|
||||||
|
query, args = maybeExpandNamedQuery(m, query, args)
|
||||||
|
case *Transaction:
|
||||||
|
query, args = maybeExpandNamedQuery(m.dbmap, query, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows, err := e.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if !rows.Next() {
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows.Scan(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hookedselect(m *DbMap, exec SqlExecutor, i interface{}, query string,
|
||||||
|
args ...interface{}) ([]interface{}, error) {
|
||||||
|
|
||||||
|
var nonFatalErr error
|
||||||
|
|
||||||
|
list, err := rawselect(m, exec, i, query, args...)
|
||||||
|
if err != nil {
|
||||||
|
if !NonFatalError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonFatalErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine where the results are: written to i, or returned in list
|
||||||
|
if t, _ := toSliceType(i); t == nil {
|
||||||
|
for _, v := range list {
|
||||||
|
if v, ok := v.(HasPostGet); ok {
|
||||||
|
err := v.PostGet(exec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resultsValue := reflect.Indirect(reflect.ValueOf(i))
|
||||||
|
for i := 0; i < resultsValue.Len(); i++ {
|
||||||
|
if v, ok := resultsValue.Index(i).Interface().(HasPostGet); ok {
|
||||||
|
err := v.PostGet(exec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list, nonFatalErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func rawselect(m *DbMap, exec SqlExecutor, i interface{}, query string,
|
||||||
|
args ...interface{}) ([]interface{}, error) {
|
||||||
|
var (
|
||||||
|
appendToSlice = false // Write results to i directly?
|
||||||
|
intoStruct = true // Selecting into a struct?
|
||||||
|
pointerElements = true // Are the slice elements pointers (vs values)?
|
||||||
|
)
|
||||||
|
|
||||||
|
var nonFatalErr error
|
||||||
|
|
||||||
|
tableName := ""
|
||||||
|
var dynObj DynamicTable
|
||||||
|
isDynamic := false
|
||||||
|
if dynObj, isDynamic = i.(DynamicTable); isDynamic {
|
||||||
|
tableName = dynObj.TableName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// get type for i, verifying it's a supported destination
|
||||||
|
t, err := toType(i)
|
||||||
|
if err != nil {
|
||||||
|
var err2 error
|
||||||
|
if t, err2 = toSliceType(i); t == nil {
|
||||||
|
if err2 != nil {
|
||||||
|
return nil, err2
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pointerElements = t.Kind() == reflect.Ptr
|
||||||
|
if pointerElements {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
appendToSlice = true
|
||||||
|
intoStruct = t.Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the caller supplied a single struct/map argument, assume a "named
|
||||||
|
// parameter" query. Extract the named arguments from the struct/map, create
|
||||||
|
// the flat arg slice, and rewrite the query to use the dialect's placeholder.
|
||||||
|
if len(args) == 1 {
|
||||||
|
query, args = maybeExpandNamedQuery(m, query, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the query
|
||||||
|
rows, err := exec.Query(query, args...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
// Fetch the column names as returned from db
|
||||||
|
cols, err := rows.Columns()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !intoStruct && len(cols) > 1 {
|
||||||
|
return nil, fmt.Errorf("gorp: select into non-struct slice requires 1 column, got %d", len(cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
var colToFieldIndex [][]int
|
||||||
|
if intoStruct {
|
||||||
|
colToFieldIndex, err = columnToFieldIndex(m, t, tableName, cols)
|
||||||
|
if err != nil {
|
||||||
|
if !NonFatalError(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonFatalErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := m.TypeConverter
|
||||||
|
|
||||||
|
// Add results to one of these two slices.
|
||||||
|
var (
|
||||||
|
list = make([]interface{}, 0)
|
||||||
|
sliceValue = reflect.Indirect(reflect.ValueOf(i))
|
||||||
|
)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if !rows.Next() {
|
||||||
|
// if error occured return rawselect
|
||||||
|
if rows.Err() != nil {
|
||||||
|
return nil, rows.Err()
|
||||||
|
}
|
||||||
|
// time to exit from outer "for" loop
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v := reflect.New(t)
|
||||||
|
|
||||||
|
if isDynamic {
|
||||||
|
v.Interface().(DynamicTable).SetTableName(tableName)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := make([]interface{}, len(cols))
|
||||||
|
|
||||||
|
custScan := make([]CustomScanner, 0)
|
||||||
|
|
||||||
|
for x := range cols {
|
||||||
|
f := v.Elem()
|
||||||
|
if intoStruct {
|
||||||
|
index := colToFieldIndex[x]
|
||||||
|
if index == nil {
|
||||||
|
// this field is not present in the struct, so create a dummy
|
||||||
|
// value for rows.Scan to scan into
|
||||||
|
var dummy dummyField
|
||||||
|
dest[x] = &dummy
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f = f.FieldByIndex(index)
|
||||||
|
}
|
||||||
|
target := f.Addr().Interface()
|
||||||
|
if conv != nil {
|
||||||
|
scanner, ok := conv.FromDb(target)
|
||||||
|
if ok {
|
||||||
|
target = scanner.Holder
|
||||||
|
custScan = append(custScan, scanner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dest[x] = target
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rows.Scan(dest...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range custScan {
|
||||||
|
err = c.Bind()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appendToSlice {
|
||||||
|
if !pointerElements {
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
sliceValue.Set(reflect.Append(sliceValue, v))
|
||||||
|
} else {
|
||||||
|
list = append(list, v.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if appendToSlice && sliceValue.IsNil() {
|
||||||
|
sliceValue.Set(reflect.MakeSlice(sliceValue.Type(), 0, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
return list, nonFatalErr
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TableMap represents a mapping between a Go struct and a database table
|
||||||
|
// Use dbmap.AddTable() or dbmap.AddTableWithName() to create these
|
||||||
|
type TableMap struct {
|
||||||
|
// Name of database table.
|
||||||
|
TableName string
|
||||||
|
SchemaName string
|
||||||
|
gotype reflect.Type
|
||||||
|
Columns []*ColumnMap
|
||||||
|
keys []*ColumnMap
|
||||||
|
indexes []*IndexMap
|
||||||
|
uniqueTogether [][]string
|
||||||
|
version *ColumnMap
|
||||||
|
insertPlan bindPlan
|
||||||
|
updatePlan bindPlan
|
||||||
|
deletePlan bindPlan
|
||||||
|
getPlan bindPlan
|
||||||
|
dbmap *DbMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetSql removes cached insert/update/select/delete SQL strings
|
||||||
|
// associated with this TableMap. Call this if you've modified
|
||||||
|
// any column names or the table name itself.
|
||||||
|
func (t *TableMap) ResetSql() {
|
||||||
|
t.insertPlan = bindPlan{}
|
||||||
|
t.updatePlan = bindPlan{}
|
||||||
|
t.deletePlan = bindPlan{}
|
||||||
|
t.getPlan = bindPlan{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetKeys lets you specify the fields on a struct that map to primary
|
||||||
|
// key columns on the table. If isAutoIncr is set, result.LastInsertId()
|
||||||
|
// will be used after INSERT to bind the generated id to the Go struct.
|
||||||
|
//
|
||||||
|
// Automatically calls ResetSql() to ensure SQL statements are regenerated.
|
||||||
|
//
|
||||||
|
// Panics if isAutoIncr is true, and fieldNames length != 1
|
||||||
|
//
|
||||||
|
func (t *TableMap) SetKeys(isAutoIncr bool, fieldNames ...string) *TableMap {
|
||||||
|
if isAutoIncr && len(fieldNames) != 1 {
|
||||||
|
panic(fmt.Sprintf(
|
||||||
|
"gorp: SetKeys: fieldNames length must be 1 if key is auto-increment. (Saw %v fieldNames)",
|
||||||
|
len(fieldNames)))
|
||||||
|
}
|
||||||
|
t.keys = make([]*ColumnMap, 0)
|
||||||
|
for _, name := range fieldNames {
|
||||||
|
colmap := t.ColMap(name)
|
||||||
|
colmap.isPK = true
|
||||||
|
colmap.isAutoIncr = isAutoIncr
|
||||||
|
t.keys = append(t.keys, colmap)
|
||||||
|
}
|
||||||
|
t.ResetSql()
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUniqueTogether lets you specify uniqueness constraints across multiple
|
||||||
|
// columns on the table. Each call adds an additional constraint for the
|
||||||
|
// specified columns.
|
||||||
|
//
|
||||||
|
// Automatically calls ResetSql() to ensure SQL statements are regenerated.
|
||||||
|
//
|
||||||
|
// Panics if fieldNames length < 2.
|
||||||
|
//
|
||||||
|
func (t *TableMap) SetUniqueTogether(fieldNames ...string) *TableMap {
|
||||||
|
if len(fieldNames) < 2 {
|
||||||
|
panic(fmt.Sprintf(
|
||||||
|
"gorp: SetUniqueTogether: must provide at least two fieldNames to set uniqueness constraint."))
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := make([]string, 0, len(fieldNames))
|
||||||
|
for _, name := range fieldNames {
|
||||||
|
columns = append(columns, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, existingColumns := range t.uniqueTogether {
|
||||||
|
if equal(existingColumns, columns) {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.uniqueTogether = append(t.uniqueTogether, columns)
|
||||||
|
t.ResetSql()
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColMap returns the ColumnMap pointer matching the given struct field
|
||||||
|
// name. It panics if the struct does not contain a field matching this
|
||||||
|
// name.
|
||||||
|
func (t *TableMap) ColMap(field string) *ColumnMap {
|
||||||
|
col := colMapOrNil(t, field)
|
||||||
|
if col == nil {
|
||||||
|
e := fmt.Sprintf("No ColumnMap in table %s type %s with field %s",
|
||||||
|
t.TableName, t.gotype.Name(), field)
|
||||||
|
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func colMapOrNil(t *TableMap, field string) *ColumnMap {
|
||||||
|
for _, col := range t.Columns {
|
||||||
|
if col.fieldName == field || col.ColumnName == field {
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IdxMap returns the IndexMap pointer matching the given index name.
|
||||||
|
func (t *TableMap) IdxMap(field string) *IndexMap {
|
||||||
|
for _, idx := range t.indexes {
|
||||||
|
if idx.IndexName == field {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddIndex registers the index with gorp for specified table with given parameters.
|
||||||
|
// This operation is idempotent. If index is already mapped, the
|
||||||
|
// existing *IndexMap is returned
|
||||||
|
// Function will panic if one of the given for index columns does not exists
|
||||||
|
//
|
||||||
|
// Automatically calls ResetSql() to ensure SQL statements are regenerated.
|
||||||
|
//
|
||||||
|
func (t *TableMap) AddIndex(name string, idxtype string, columns []string) *IndexMap {
|
||||||
|
// check if we have a index with this name already
|
||||||
|
for _, idx := range t.indexes {
|
||||||
|
if idx.IndexName == name {
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, icol := range columns {
|
||||||
|
if res := t.ColMap(icol); res == nil {
|
||||||
|
e := fmt.Sprintf("No ColumnName in table %s to create index on", t.TableName)
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := &IndexMap{IndexName: name, Unique: false, IndexType: idxtype, columns: columns}
|
||||||
|
t.indexes = append(t.indexes, idx)
|
||||||
|
t.ResetSql()
|
||||||
|
return idx
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVersionCol sets the column to use as the Version field. By default
|
||||||
|
// the "Version" field is used. Returns the column found, or panics
|
||||||
|
// if the struct does not contain a field matching this name.
|
||||||
|
//
|
||||||
|
// Automatically calls ResetSql() to ensure SQL statements are regenerated.
|
||||||
|
func (t *TableMap) SetVersionCol(field string) *ColumnMap {
|
||||||
|
c := t.ColMap(field)
|
||||||
|
t.version = c
|
||||||
|
t.ResetSql()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// SqlForCreateTable gets a sequence of SQL commands that will create
|
||||||
|
// the specified table and any associated schema
|
||||||
|
func (t *TableMap) SqlForCreate(ifNotExists bool) string {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
dialect := t.dbmap.Dialect
|
||||||
|
|
||||||
|
if strings.TrimSpace(t.SchemaName) != "" {
|
||||||
|
schemaCreate := "create schema"
|
||||||
|
if ifNotExists {
|
||||||
|
s.WriteString(dialect.IfSchemaNotExists(schemaCreate, t.SchemaName))
|
||||||
|
} else {
|
||||||
|
s.WriteString(schemaCreate)
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" %s;", t.SchemaName))
|
||||||
|
}
|
||||||
|
|
||||||
|
tableCreate := "create table"
|
||||||
|
if ifNotExists {
|
||||||
|
s.WriteString(dialect.IfTableNotExists(tableCreate, t.SchemaName, t.TableName))
|
||||||
|
} else {
|
||||||
|
s.WriteString(tableCreate)
|
||||||
|
}
|
||||||
|
s.WriteString(fmt.Sprintf(" %s (", dialect.QuotedTableForQuery(t.SchemaName, t.TableName)))
|
||||||
|
|
||||||
|
x := 0
|
||||||
|
for _, col := range t.Columns {
|
||||||
|
if !col.Transient {
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
stype := dialect.ToSqlType(col.gotype, col.MaxSize, col.isAutoIncr)
|
||||||
|
s.WriteString(fmt.Sprintf("%s %s", dialect.QuoteField(col.ColumnName), stype))
|
||||||
|
|
||||||
|
if col.isPK || col.isNotNull {
|
||||||
|
s.WriteString(" not null")
|
||||||
|
}
|
||||||
|
if col.isPK && len(t.keys) == 1 {
|
||||||
|
s.WriteString(" primary key")
|
||||||
|
}
|
||||||
|
if col.Unique {
|
||||||
|
s.WriteString(" unique")
|
||||||
|
}
|
||||||
|
if col.isAutoIncr {
|
||||||
|
s.WriteString(fmt.Sprintf(" %s", dialect.AutoIncrStr()))
|
||||||
|
}
|
||||||
|
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(t.keys) > 1 {
|
||||||
|
s.WriteString(", primary key (")
|
||||||
|
for x := range t.keys {
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
s.WriteString(dialect.QuoteField(t.keys[x].ColumnName))
|
||||||
|
}
|
||||||
|
s.WriteString(")")
|
||||||
|
}
|
||||||
|
if len(t.uniqueTogether) > 0 {
|
||||||
|
for _, columns := range t.uniqueTogether {
|
||||||
|
s.WriteString(", unique (")
|
||||||
|
for i, column := range columns {
|
||||||
|
if i > 0 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
s.WriteString(dialect.QuoteField(column))
|
||||||
|
}
|
||||||
|
s.WriteString(")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.WriteString(") ")
|
||||||
|
s.WriteString(dialect.CreateTableSuffix())
|
||||||
|
s.WriteString(dialect.QuerySuffix())
|
||||||
|
return s.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func equal(a, b []string) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,305 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomScanner binds a database column value to a Go type
|
||||||
|
type CustomScanner struct {
|
||||||
|
// After a row is scanned, Holder will contain the value from the database column.
|
||||||
|
// Initialize the CustomScanner with the concrete Go type you wish the database
|
||||||
|
// driver to scan the raw column into.
|
||||||
|
Holder interface{}
|
||||||
|
// Target typically holds a pointer to the target struct field to bind the Holder
|
||||||
|
// value to.
|
||||||
|
Target interface{}
|
||||||
|
// Binder is a custom function that converts the holder value to the target type
|
||||||
|
// and sets target accordingly. This function should return error if a problem
|
||||||
|
// occurs converting the holder to the target.
|
||||||
|
Binder func(holder interface{}, target interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to filter columns when selectively updating
|
||||||
|
type ColumnFilter func(*ColumnMap) bool
|
||||||
|
|
||||||
|
func acceptAllFilter(col *ColumnMap) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind is called automatically by gorp after Scan()
|
||||||
|
func (me CustomScanner) Bind() error {
|
||||||
|
return me.Binder(me.Holder, me.Target)
|
||||||
|
}
|
||||||
|
|
||||||
|
type bindPlan struct {
|
||||||
|
query string
|
||||||
|
argFields []string
|
||||||
|
keyFields []string
|
||||||
|
versField string
|
||||||
|
autoIncrIdx int
|
||||||
|
autoIncrFieldName string
|
||||||
|
once sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (plan *bindPlan) createBindInstance(elem reflect.Value, conv TypeConverter) (bindInstance, error) {
|
||||||
|
bi := bindInstance{query: plan.query, autoIncrIdx: plan.autoIncrIdx, autoIncrFieldName: plan.autoIncrFieldName, versField: plan.versField}
|
||||||
|
if plan.versField != "" {
|
||||||
|
bi.existingVersion = elem.FieldByName(plan.versField).Int()
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for i := 0; i < len(plan.argFields); i++ {
|
||||||
|
k := plan.argFields[i]
|
||||||
|
if k == versFieldConst {
|
||||||
|
newVer := bi.existingVersion + 1
|
||||||
|
bi.args = append(bi.args, newVer)
|
||||||
|
if bi.existingVersion == 0 {
|
||||||
|
elem.FieldByName(plan.versField).SetInt(int64(newVer))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val := elem.FieldByName(k).Interface()
|
||||||
|
if conv != nil {
|
||||||
|
val, err = conv.ToDb(val)
|
||||||
|
if err != nil {
|
||||||
|
return bindInstance{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bi.args = append(bi.args, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(plan.keyFields); i++ {
|
||||||
|
k := plan.keyFields[i]
|
||||||
|
val := elem.FieldByName(k).Interface()
|
||||||
|
if conv != nil {
|
||||||
|
val, err = conv.ToDb(val)
|
||||||
|
if err != nil {
|
||||||
|
return bindInstance{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bi.keys = append(bi.keys, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type bindInstance struct {
|
||||||
|
query string
|
||||||
|
args []interface{}
|
||||||
|
keys []interface{}
|
||||||
|
existingVersion int64
|
||||||
|
versField string
|
||||||
|
autoIncrIdx int
|
||||||
|
autoIncrFieldName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableMap) bindInsert(elem reflect.Value) (bindInstance, error) {
|
||||||
|
plan := &t.insertPlan
|
||||||
|
plan.once.Do(func() {
|
||||||
|
plan.autoIncrIdx = -1
|
||||||
|
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s2 := bytes.Buffer{}
|
||||||
|
s.WriteString(fmt.Sprintf("insert into %s (", t.dbmap.Dialect.QuotedTableForQuery(t.SchemaName, t.TableName)))
|
||||||
|
|
||||||
|
x := 0
|
||||||
|
first := true
|
||||||
|
for y := range t.Columns {
|
||||||
|
col := t.Columns[y]
|
||||||
|
if !(col.isAutoIncr && t.dbmap.Dialect.AutoIncrBindValue() == "") {
|
||||||
|
if !col.Transient {
|
||||||
|
if !first {
|
||||||
|
s.WriteString(",")
|
||||||
|
s2.WriteString(",")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(col.ColumnName))
|
||||||
|
|
||||||
|
if col.isAutoIncr {
|
||||||
|
s2.WriteString(t.dbmap.Dialect.AutoIncrBindValue())
|
||||||
|
plan.autoIncrIdx = y
|
||||||
|
plan.autoIncrFieldName = col.fieldName
|
||||||
|
} else {
|
||||||
|
if col.DefaultValue == "" {
|
||||||
|
s2.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
if col == t.version {
|
||||||
|
plan.versField = col.fieldName
|
||||||
|
plan.argFields = append(plan.argFields, versFieldConst)
|
||||||
|
} else {
|
||||||
|
plan.argFields = append(plan.argFields, col.fieldName)
|
||||||
|
}
|
||||||
|
x++
|
||||||
|
} else {
|
||||||
|
s2.WriteString(col.DefaultValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
plan.autoIncrIdx = y
|
||||||
|
plan.autoIncrFieldName = col.fieldName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.WriteString(") values (")
|
||||||
|
s.WriteString(s2.String())
|
||||||
|
s.WriteString(")")
|
||||||
|
if plan.autoIncrIdx > -1 {
|
||||||
|
s.WriteString(t.dbmap.Dialect.AutoIncrInsertSuffix(t.Columns[plan.autoIncrIdx]))
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuerySuffix())
|
||||||
|
|
||||||
|
plan.query = s.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
return plan.createBindInstance(elem, t.dbmap.TypeConverter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableMap) bindUpdate(elem reflect.Value, colFilter ColumnFilter) (bindInstance, error) {
|
||||||
|
if colFilter == nil {
|
||||||
|
colFilter = acceptAllFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
plan := &t.updatePlan
|
||||||
|
plan.once.Do(func() {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s.WriteString(fmt.Sprintf("update %s set ", t.dbmap.Dialect.QuotedTableForQuery(t.SchemaName, t.TableName)))
|
||||||
|
x := 0
|
||||||
|
|
||||||
|
for y := range t.Columns {
|
||||||
|
col := t.Columns[y]
|
||||||
|
if !col.isAutoIncr && !col.Transient && colFilter(col) {
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(", ")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(col.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
|
||||||
|
if col == t.version {
|
||||||
|
plan.versField = col.fieldName
|
||||||
|
plan.argFields = append(plan.argFields, versFieldConst)
|
||||||
|
} else {
|
||||||
|
plan.argFields = append(plan.argFields, col.fieldName)
|
||||||
|
}
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(" where ")
|
||||||
|
for y := range t.keys {
|
||||||
|
col := t.keys[y]
|
||||||
|
if y > 0 {
|
||||||
|
s.WriteString(" and ")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(col.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
|
||||||
|
plan.argFields = append(plan.argFields, col.fieldName)
|
||||||
|
plan.keyFields = append(plan.keyFields, col.fieldName)
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
if plan.versField != "" {
|
||||||
|
s.WriteString(" and ")
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(t.version.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
plan.argFields = append(plan.argFields, plan.versField)
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuerySuffix())
|
||||||
|
|
||||||
|
plan.query = s.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
return plan.createBindInstance(elem, t.dbmap.TypeConverter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableMap) bindDelete(elem reflect.Value) (bindInstance, error) {
|
||||||
|
plan := &t.deletePlan
|
||||||
|
plan.once.Do(func() {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s.WriteString(fmt.Sprintf("delete from %s", t.dbmap.Dialect.QuotedTableForQuery(t.SchemaName, t.TableName)))
|
||||||
|
|
||||||
|
for y := range t.Columns {
|
||||||
|
col := t.Columns[y]
|
||||||
|
if !col.Transient {
|
||||||
|
if col == t.version {
|
||||||
|
plan.versField = col.fieldName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.WriteString(" where ")
|
||||||
|
for x := range t.keys {
|
||||||
|
k := t.keys[x]
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(" and ")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(k.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
|
||||||
|
plan.keyFields = append(plan.keyFields, k.fieldName)
|
||||||
|
plan.argFields = append(plan.argFields, k.fieldName)
|
||||||
|
}
|
||||||
|
if plan.versField != "" {
|
||||||
|
s.WriteString(" and ")
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(t.version.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(len(plan.argFields)))
|
||||||
|
|
||||||
|
plan.argFields = append(plan.argFields, plan.versField)
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuerySuffix())
|
||||||
|
|
||||||
|
plan.query = s.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
return plan.createBindInstance(elem, t.dbmap.TypeConverter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TableMap) bindGet() *bindPlan {
|
||||||
|
plan := &t.getPlan
|
||||||
|
plan.once.Do(func() {
|
||||||
|
s := bytes.Buffer{}
|
||||||
|
s.WriteString("select ")
|
||||||
|
|
||||||
|
x := 0
|
||||||
|
for _, col := range t.Columns {
|
||||||
|
if !col.Transient {
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(",")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(col.ColumnName))
|
||||||
|
plan.argFields = append(plan.argFields, col.fieldName)
|
||||||
|
x++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.WriteString(" from ")
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuotedTableForQuery(t.SchemaName, t.TableName))
|
||||||
|
s.WriteString(" where ")
|
||||||
|
for x := range t.keys {
|
||||||
|
col := t.keys[x]
|
||||||
|
if x > 0 {
|
||||||
|
s.WriteString(" and ")
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuoteField(col.ColumnName))
|
||||||
|
s.WriteString("=")
|
||||||
|
s.WriteString(t.dbmap.Dialect.BindVar(x))
|
||||||
|
|
||||||
|
plan.keyFields = append(plan.keyFields, col.fieldName)
|
||||||
|
}
|
||||||
|
s.WriteString(t.dbmap.Dialect.QuerySuffix())
|
||||||
|
|
||||||
|
plan.query = s.String()
|
||||||
|
})
|
||||||
|
|
||||||
|
return plan
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#!/bin/bash -ex
|
||||||
|
|
||||||
|
# on macs, you may need to:
|
||||||
|
# export GOBUILDFLAG=-ldflags -linkmode=external
|
||||||
|
|
||||||
|
echo "Running unit tests"
|
||||||
|
go test -race
|
||||||
|
|
||||||
|
echo "Testing against postgres"
|
||||||
|
export GORP_TEST_DSN="host=postgres user=gorptest password=gorptest dbname=gorptest sslmode=disable"
|
||||||
|
export GORP_TEST_DIALECT=postgres
|
||||||
|
go test -tags integration $GOBUILDFLAG $@ .
|
||||||
|
|
||||||
|
echo "Testing against sqlite"
|
||||||
|
export GORP_TEST_DSN=/tmp/gorptest.bin
|
||||||
|
export GORP_TEST_DIALECT=sqlite
|
||||||
|
go test -tags integration $GOBUILDFLAG $@ .
|
||||||
|
rm -f /tmp/gorptest.bin
|
||||||
|
|
||||||
|
echo "Testing against mysql"
|
||||||
|
export GORP_TEST_DSN="gorptest:gorptest@tcp(mysql)/gorptest"
|
||||||
|
export GORP_TEST_DIALECT=mysql
|
||||||
|
go test -tags integration $GOBUILDFLAG $@ .
|
|
@ -0,0 +1,239 @@
|
||||||
|
// Copyright 2012 James Cooper. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gorp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Transaction represents a database transaction.
|
||||||
|
// Insert/Update/Delete/Get/Exec operations will be run in the context
|
||||||
|
// of that transaction. Transactions should be terminated with
|
||||||
|
// a call to Commit() or Rollback()
|
||||||
|
type Transaction struct {
|
||||||
|
ctx context.Context
|
||||||
|
dbmap *DbMap
|
||||||
|
tx *sql.Tx
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) WithContext(ctx context.Context) SqlExecutor {
|
||||||
|
copy := &Transaction{}
|
||||||
|
*copy = *t
|
||||||
|
copy.ctx = ctx
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert has the same behavior as DbMap.Insert(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Insert(list ...interface{}) error {
|
||||||
|
return insert(t.dbmap, t, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update had the same behavior as DbMap.Update(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Update(list ...interface{}) (int64, error) {
|
||||||
|
return update(t.dbmap, t, nil, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateColumns had the same behavior as DbMap.UpdateColumns(), but runs in a transaction.
|
||||||
|
func (t *Transaction) UpdateColumns(filter ColumnFilter, list ...interface{}) (int64, error) {
|
||||||
|
return update(t.dbmap, t, filter, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete has the same behavior as DbMap.Delete(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Delete(list ...interface{}) (int64, error) {
|
||||||
|
return delete(t.dbmap, t, list...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get has the same behavior as DbMap.Get(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Get(i interface{}, keys ...interface{}) (interface{}, error) {
|
||||||
|
return get(t.dbmap, t, i, keys...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select has the same behavior as DbMap.Select(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Select(i interface{}, query string, args ...interface{}) ([]interface{}, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hookedselect(t.dbmap, t, i, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exec has the same behavior as DbMap.Exec(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, args...)
|
||||||
|
}
|
||||||
|
return maybeExpandNamedQueryAndExec(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectInt is a convenience wrapper around the gorp.SelectInt function.
|
||||||
|
func (t *Transaction) SelectInt(query string, args ...interface{}) (int64, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectInt(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullInt is a convenience wrapper around the gorp.SelectNullInt function.
|
||||||
|
func (t *Transaction) SelectNullInt(query string, args ...interface{}) (sql.NullInt64, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullInt(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectFloat is a convenience wrapper around the gorp.SelectFloat function.
|
||||||
|
func (t *Transaction) SelectFloat(query string, args ...interface{}) (float64, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectFloat(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullFloat is a convenience wrapper around the gorp.SelectNullFloat function.
|
||||||
|
func (t *Transaction) SelectNullFloat(query string, args ...interface{}) (sql.NullFloat64, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullFloat(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectStr is a convenience wrapper around the gorp.SelectStr function.
|
||||||
|
func (t *Transaction) SelectStr(query string, args ...interface{}) (string, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectStr(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectNullStr is a convenience wrapper around the gorp.SelectNullStr function.
|
||||||
|
func (t *Transaction) SelectNullStr(query string, args ...interface{}) (sql.NullString, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectNullStr(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectOne is a convenience wrapper around the gorp.SelectOne function.
|
||||||
|
func (t *Transaction) SelectOne(holder interface{}, query string, args ...interface{}) error {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return SelectOne(t.dbmap, t, holder, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit commits the underlying database transaction.
|
||||||
|
func (t *Transaction) Commit() error {
|
||||||
|
if !t.closed {
|
||||||
|
t.closed = true
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, "commit;")
|
||||||
|
}
|
||||||
|
return t.tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.ErrTxDone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rollback rolls back the underlying database transaction.
|
||||||
|
func (t *Transaction) Rollback() error {
|
||||||
|
if !t.closed {
|
||||||
|
t.closed = true
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, "rollback;")
|
||||||
|
}
|
||||||
|
return t.tx.Rollback()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql.ErrTxDone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Savepoint creates a savepoint with the given name. The name is interpolated
|
||||||
|
// directly into the SQL SAVEPOINT statement, so you must sanitize it if it is
|
||||||
|
// derived from user input.
|
||||||
|
func (t *Transaction) Savepoint(name string) error {
|
||||||
|
query := "savepoint " + t.dbmap.Dialect.QuoteField(name)
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, nil)
|
||||||
|
}
|
||||||
|
_, err := exec(t, query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RollbackToSavepoint rolls back to the savepoint with the given name. The
|
||||||
|
// name is interpolated directly into the SQL SAVEPOINT statement, so you must
|
||||||
|
// sanitize it if it is derived from user input.
|
||||||
|
func (t *Transaction) RollbackToSavepoint(savepoint string) error {
|
||||||
|
query := "rollback to savepoint " + t.dbmap.Dialect.QuoteField(savepoint)
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, nil)
|
||||||
|
}
|
||||||
|
_, err := exec(t, query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseSavepint releases the savepoint with the given name. The name is
|
||||||
|
// interpolated directly into the SQL SAVEPOINT statement, so you must sanitize
|
||||||
|
// it if it is derived from user input.
|
||||||
|
func (t *Transaction) ReleaseSavepoint(savepoint string) error {
|
||||||
|
query := "release savepoint " + t.dbmap.Dialect.QuoteField(savepoint)
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, nil)
|
||||||
|
}
|
||||||
|
_, err := exec(t, query)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare has the same behavior as DbMap.Prepare(), but runs in a transaction.
|
||||||
|
func (t *Transaction) Prepare(query string) (*sql.Stmt, error) {
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, nil)
|
||||||
|
}
|
||||||
|
return prepare(t, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) QueryRow(query string, args ...interface{}) *sql.Row {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, query, args...)
|
||||||
|
}
|
||||||
|
return queryRow(t, query, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Transaction) Query(q string, args ...interface{}) (*sql.Rows, error) {
|
||||||
|
if t.dbmap.ExpandSliceArgs {
|
||||||
|
expandSliceArgs(&q, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.dbmap.logger != nil {
|
||||||
|
now := time.Now()
|
||||||
|
defer t.dbmap.trace(now, q, args...)
|
||||||
|
}
|
||||||
|
return query(t, q, args...)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
.DS_Store
|
|
@ -0,0 +1,16 @@
|
||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.8
|
||||||
|
- 1.9
|
||||||
|
- tip
|
||||||
|
|
||||||
|
install:
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go list -f '{{range .Imports}}{{.}} {{end}}' ./... | xargs go get -v
|
||||||
|
- go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs go get -v
|
||||||
|
|
||||||
|
script:
|
||||||
|
- go test -v -covermode=count -coverprofile=coverage.out
|
||||||
|
- $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
|
|
@ -0,0 +1,17 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
Copyright (c) 2016, Qiang Xue
|
||||||
|
|
||||||
|
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.
|
|
@ -0,0 +1,585 @@
|
||||||
|
# ozzo-validation
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/github.com/go-ozzo/ozzo-validation?status.png)](http://godoc.org/github.com/go-ozzo/ozzo-validation)
|
||||||
|
[![Build Status](https://travis-ci.org/go-ozzo/ozzo-validation.svg?branch=master)](https://travis-ci.org/go-ozzo/ozzo-validation)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/go-ozzo/ozzo-validation/badge.svg?branch=master)](https://coveralls.io/github/go-ozzo/ozzo-validation?branch=master)
|
||||||
|
[![Go Report](https://goreportcard.com/badge/github.com/go-ozzo/ozzo-validation)](https://goreportcard.com/report/github.com/go-ozzo/ozzo-validation)
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
ozzo-validation is a Go package that provides configurable and extensible data validation capabilities.
|
||||||
|
It has the following features:
|
||||||
|
|
||||||
|
* use normal programming constructs rather than error-prone struct tags to specify how data should be validated.
|
||||||
|
* can validate data of different types, e.g., structs, strings, byte slices, slices, maps, arrays.
|
||||||
|
* can validate custom data types as long as they implement the `Validatable` interface.
|
||||||
|
* can validate data types that implement the `sql.Valuer` interface (e.g. `sql.NullString`).
|
||||||
|
* customizable and well-formatted validation errors.
|
||||||
|
* provide a rich set of validation rules right out of box.
|
||||||
|
* extremely easy to create and use custom validation rules.
|
||||||
|
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
Go 1.8 or above.
|
||||||
|
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
The ozzo-validation package mainly includes a set of validation rules and two validation methods. You use
|
||||||
|
validation rules to describe how a value should be considered valid, and you call either `validation.Validate()`
|
||||||
|
or `validation.ValidateStruct()` to validate the value.
|
||||||
|
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
Run the following command to install the package:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-ozzo/ozzo-validation
|
||||||
|
go get github.com/go-ozzo/ozzo-validation/is
|
||||||
|
```
|
||||||
|
|
||||||
|
### Validating a Simple Value
|
||||||
|
|
||||||
|
For a simple value, such as a string or an integer, you may use `validation.Validate()` to validate it. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-ozzo/ozzo-validation"
|
||||||
|
"github.com/go-ozzo/ozzo-validation/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
data := "example"
|
||||||
|
err := validation.Validate(data,
|
||||||
|
validation.Required, // not empty
|
||||||
|
validation.Length(5, 100), // length between 5 and 100
|
||||||
|
is.URL, // is a valid URL
|
||||||
|
)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// must be a valid URL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The method `validation.Validate()` will run through the rules in the order that they are listed. If a rule fails
|
||||||
|
the validation, the method will return the corresponding error and skip the rest of the rules. The method will
|
||||||
|
return nil if the value passes all validation rules.
|
||||||
|
|
||||||
|
|
||||||
|
### Validating a Struct
|
||||||
|
|
||||||
|
For a struct value, you usually want to check if its fields are valid. For example, in a RESTful application, you
|
||||||
|
may unmarshal the request payload into a struct and then validate the struct fields. If one or multiple fields
|
||||||
|
are invalid, you may want to get an error describing which fields are invalid. You can use `validation.ValidateStruct()`
|
||||||
|
to achieve this purpose. A single struct can have rules for multiple fields, and a field can be associated with multiple
|
||||||
|
rules. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/go-ozzo/ozzo-validation"
|
||||||
|
"github.com/go-ozzo/ozzo-validation/is"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Address struct {
|
||||||
|
Street string
|
||||||
|
City string
|
||||||
|
State string
|
||||||
|
Zip string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Address) Validate() error {
|
||||||
|
return validation.ValidateStruct(&a,
|
||||||
|
// Street cannot be empty, and the length must between 5 and 50
|
||||||
|
validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
|
||||||
|
// City cannot be empty, and the length must between 5 and 50
|
||||||
|
validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
|
||||||
|
// State cannot be empty, and must be a string consisting of two letters in upper case
|
||||||
|
validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
|
||||||
|
// State cannot be empty, and must be a string consisting of five digits
|
||||||
|
validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := Address{
|
||||||
|
Street: "123",
|
||||||
|
City: "Unknown",
|
||||||
|
State: "Virginia",
|
||||||
|
Zip: "12345",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.Validate()
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// Street: the length must be between 5 and 50; State: must be in a valid format.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that when calling `validation.ValidateStruct` to validate a struct, you should pass to the method a pointer
|
||||||
|
to the struct instead of the struct itself. Similarly, when calling `validation.Field` to specify the rules
|
||||||
|
for a struct field, you should use a pointer to the struct field.
|
||||||
|
|
||||||
|
When the struct validation is performed, the fields are validated in the order they are specified in `ValidateStruct`.
|
||||||
|
And when each field is validated, its rules are also evaluated in the order they are associated with the field.
|
||||||
|
If a rule fails, an error is recorded for that field, and the validation will continue with the next field.
|
||||||
|
|
||||||
|
|
||||||
|
### Validation Errors
|
||||||
|
|
||||||
|
The `validation.ValidateStruct` method returns validation errors found in struct fields in terms of `validation.Errors`
|
||||||
|
which is a map of fields and their corresponding errors. Nil is returned if validation passes.
|
||||||
|
|
||||||
|
By default, `validation.Errors` uses the struct tags named `json` to determine what names should be used to
|
||||||
|
represent the invalid fields. The type also implements the `json.Marshaler` interface so that it can be marshaled
|
||||||
|
into a proper JSON object. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Address struct {
|
||||||
|
Street string `json:"street"`
|
||||||
|
City string `json:"city"`
|
||||||
|
State string `json:"state"`
|
||||||
|
Zip string `json:"zip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...perform validation here...
|
||||||
|
|
||||||
|
err := a.Validate()
|
||||||
|
b, _ := json.Marshal(err)
|
||||||
|
fmt.Println(string(b))
|
||||||
|
// Output:
|
||||||
|
// {"street":"the length must be between 5 and 50","state":"must be in a valid format"}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may modify `validation.ErrorTag` to use a different struct tag name.
|
||||||
|
|
||||||
|
If you do not like the magic that `ValidateStruct` determines error keys based on struct field names or corresponding
|
||||||
|
tag values, you may use the following alternative approach:
|
||||||
|
|
||||||
|
```go
|
||||||
|
c := Customer{
|
||||||
|
Name: "Qiang Xue",
|
||||||
|
Email: "q",
|
||||||
|
Address: Address{
|
||||||
|
State: "Virginia",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validation.Errors{
|
||||||
|
"name": validation.Validate(c.Name, validation.Required, validation.Length(5, 20)),
|
||||||
|
"email": validation.Validate(c.Name, validation.Required, is.Email),
|
||||||
|
"zip": validation.Validate(c.Address.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
|
||||||
|
}.Filter()
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// email: must be a valid email address; zip: cannot be blank.
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we build a `validation.Errors` by a list of names and the corresponding validation results.
|
||||||
|
At the end we call `Errors.Filter()` to remove from `Errors` all nils which correspond to those successful validation
|
||||||
|
results. The method will return nil if `Errors` is empty.
|
||||||
|
|
||||||
|
The above approach is very flexible as it allows you to freely build up your validation error structure. You can use
|
||||||
|
it to validate both struct and non-struct values. Compared to using `ValidateStruct` to validate a struct,
|
||||||
|
it has the drawback that you have to redundantly specify the error keys while `ValidateStruct` can automatically
|
||||||
|
find them out.
|
||||||
|
|
||||||
|
|
||||||
|
### Internal Errors
|
||||||
|
|
||||||
|
Internal errors are different from validation errors in that internal errors are caused by malfunctioning code (e.g.
|
||||||
|
a validator making a remote call to validate some data when the remote service is down) rather
|
||||||
|
than the data being validated. When an internal error happens during data validation, you may allow the user to resubmit
|
||||||
|
the same data to perform validation again, hoping the program resumes functioning. On the other hand, if data validation
|
||||||
|
fails due to data error, the user should generally not resubmit the same data again.
|
||||||
|
|
||||||
|
To differentiate internal errors from validation errors, when an internal error occurs in a validator, wrap it
|
||||||
|
into `validation.InternalError` by calling `validation.NewInternalError()`. The user of the validator can then check
|
||||||
|
if a returned error is an internal error or not. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := a.Validate(); err != nil {
|
||||||
|
if e, ok := err.(validation.InternalError); ok {
|
||||||
|
// an internal error happened
|
||||||
|
fmt.Println(e.InternalError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Validatable Types
|
||||||
|
|
||||||
|
A type is validatable if it implements the `validation.Validatable` interface.
|
||||||
|
|
||||||
|
When `validation.Validate` is used to validate a validatable value, if it does not find any error with the
|
||||||
|
given validation rules, it will further call the value's `Validate()` method.
|
||||||
|
|
||||||
|
Similarly, when `validation.ValidateStruct` is validating a struct field whose type is validatable, it will call
|
||||||
|
the field's `Validate` method after it passes the listed rules.
|
||||||
|
|
||||||
|
In the following example, the `Address` field of `Customer` is validatable because `Address` implements
|
||||||
|
`validation.Validatable`. Therefore, when validating a `Customer` struct with `validation.ValidateStruct`,
|
||||||
|
validation will "dive" into the `Address` field.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Customer struct {
|
||||||
|
Name string
|
||||||
|
Gender string
|
||||||
|
Email string
|
||||||
|
Address Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Customer) Validate() error {
|
||||||
|
return validation.ValidateStruct(&c,
|
||||||
|
// Name cannot be empty, and the length must be between 5 and 20.
|
||||||
|
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
|
||||||
|
// Gender is optional, and should be either "Female" or "Male".
|
||||||
|
validation.Field(&c.Gender, validation.In("Female", "Male")),
|
||||||
|
// Email cannot be empty and should be in a valid email format.
|
||||||
|
validation.Field(&c.Email, validation.Required, is.Email),
|
||||||
|
// Validate Address using its own validation rules
|
||||||
|
validation.Field(&c.Address),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Customer{
|
||||||
|
Name: "Qiang Xue",
|
||||||
|
Email: "q",
|
||||||
|
Address: Address{
|
||||||
|
Street: "123 Main Street",
|
||||||
|
City: "Unknown",
|
||||||
|
State: "Virginia",
|
||||||
|
Zip: "12345",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Validate()
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// Address: (State: must be in a valid format.); Email: must be a valid email address.
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes, you may want to skip the invocation of a type's `Validate` method. To do so, simply associate
|
||||||
|
a `validation.Skip` rule with the value being validated.
|
||||||
|
|
||||||
|
### Maps/Slices/Arrays of Validatables
|
||||||
|
|
||||||
|
When validating an iterable (map, slice, or array), whose element type implements the `validation.Validatable` interface,
|
||||||
|
the `validation.Validate` method will call the `Validate` method of every non-nil element.
|
||||||
|
The validation errors of the elements will be returned as `validation.Errors` which maps the keys of the
|
||||||
|
invalid elements to their corresponding validation errors. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
addresses := []Address{
|
||||||
|
Address{State: "MD", Zip: "12345"},
|
||||||
|
Address{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
|
||||||
|
Address{City: "Unknown", State: "NC", Zip: "123"},
|
||||||
|
}
|
||||||
|
err := validation.Validate(addresses)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).
|
||||||
|
```
|
||||||
|
|
||||||
|
When using `validation.ValidateStruct` to validate a struct, the above validation procedure also applies to those struct
|
||||||
|
fields which are map/slices/arrays of validatables.
|
||||||
|
|
||||||
|
#### Each
|
||||||
|
|
||||||
|
An other option is to use the `validation.Each` method.
|
||||||
|
This method allows you to define the rules for the iterables within a struct.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Customer struct {
|
||||||
|
Name string
|
||||||
|
Emails []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Customer) Validate() error {
|
||||||
|
return validation.ValidateStruct(&c,
|
||||||
|
// Name cannot be empty, and the length must be between 5 and 20.
|
||||||
|
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
|
||||||
|
// Emails are optional, but if given must be valid.
|
||||||
|
validation.Field(&c.Emails, validation.Each(is.Email)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := Customer{
|
||||||
|
Name: "Qiang Xue",
|
||||||
|
Emails: []Email{
|
||||||
|
"valid@example.com",
|
||||||
|
"invalid",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Validate()
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// Emails: (1: must be a valid email address.).
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pointers
|
||||||
|
|
||||||
|
When a value being validated is a pointer, most validation rules will validate the actual value pointed to by the pointer.
|
||||||
|
If the pointer is nil, these rules will skip the validation.
|
||||||
|
|
||||||
|
An exception is the `validation.Required` and `validation.NotNil` rules. When a pointer is nil, they
|
||||||
|
will report a validation error.
|
||||||
|
|
||||||
|
|
||||||
|
### Types Implementing `sql.Valuer`
|
||||||
|
|
||||||
|
If a data type implements the `sql.Valuer` interface (e.g. `sql.NullString`), the built-in validation rules will handle
|
||||||
|
it properly. In particular, when a rule is validating such data, it will call the `Value()` method and validate
|
||||||
|
the returned value instead.
|
||||||
|
|
||||||
|
|
||||||
|
### Required vs. Not Nil
|
||||||
|
|
||||||
|
When validating input values, there are two different scenarios about checking if input values are provided or not.
|
||||||
|
|
||||||
|
In the first scenario, an input value is considered missing if it is not entered or it is entered as a zero value
|
||||||
|
(e.g. an empty string, a zero integer). You can use the `validation.Required` rule in this case.
|
||||||
|
|
||||||
|
In the second scenario, an input value is considered missing only if it is not entered. A pointer field is usually
|
||||||
|
used in this case so that you can detect if a value is entered or not by checking if the pointer is nil or not.
|
||||||
|
You can use the `validation.NotNil` rule to ensure a value is entered (even if it is a zero value).
|
||||||
|
|
||||||
|
|
||||||
|
### Embedded Structs
|
||||||
|
|
||||||
|
The `validation.ValidateStruct` method will properly validate a struct that contains embedded structs. In particular,
|
||||||
|
the fields of an embedded struct are treated as if they belong directly to the containing struct. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Employee struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ()
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
Employee
|
||||||
|
Level int
|
||||||
|
}
|
||||||
|
|
||||||
|
m := Manager{}
|
||||||
|
err := validation.ValidateStruct(&m,
|
||||||
|
validation.Field(&m.Name, validation.Required),
|
||||||
|
validation.Field(&m.Level, validation.Required),
|
||||||
|
)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// Level: cannot be blank; Name: cannot be blank.
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above code, we use `&m.Name` to specify the validation of the `Name` field of the embedded struct `Employee`.
|
||||||
|
And the validation error uses `Name` as the key for the error associated with the `Name` field as if `Name` a field
|
||||||
|
directly belonging to `Manager`.
|
||||||
|
|
||||||
|
If `Employee` implements the `validation.Validatable` interface, we can also use the following code to validate
|
||||||
|
`Manager`, which generates the same validation result:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (e Employee) Validate() error {
|
||||||
|
return validation.ValidateStruct(&e,
|
||||||
|
validation.Field(&e.Name, validation.Required),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validation.ValidateStruct(&m,
|
||||||
|
validation.Field(&m.Employee),
|
||||||
|
validation.Field(&m.Level, validation.Required),
|
||||||
|
)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// Level: cannot be blank; Name: cannot be blank.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Built-in Validation Rules
|
||||||
|
|
||||||
|
The following rules are provided in the `validation` package:
|
||||||
|
|
||||||
|
* `In(...interface{})`: checks if a value can be found in the given list of values.
|
||||||
|
* `Length(min, max int)`: checks if the length of a value is within the specified range.
|
||||||
|
This rule should only be used for validating strings, slices, maps, and arrays.
|
||||||
|
* `RuneLength(min, max int)`: checks if the length of a string is within the specified range.
|
||||||
|
This rule is similar as `Length` except that when the value being validated is a string, it checks
|
||||||
|
its rune length instead of byte length.
|
||||||
|
* `Min(min interface{})` and `Max(max interface{})`: checks if a value is within the specified range.
|
||||||
|
These two rules should only be used for validating int, uint, float and time.Time types.
|
||||||
|
* `Match(*regexp.Regexp)`: checks if a value matches the specified regular expression.
|
||||||
|
This rule should only be used for strings and byte slices.
|
||||||
|
* `Date(layout string)`: checks if a string value is a date whose format is specified by the layout.
|
||||||
|
By calling `Min()` and/or `Max()`, you can check additionally if the date is within the specified range.
|
||||||
|
* `Required`: checks if a value is not empty (neither nil nor zero).
|
||||||
|
* `NotNil`: checks if a pointer value is not nil. Non-pointer values are considered valid.
|
||||||
|
* `NilOrNotEmpty`: checks if a value is a nil pointer or a non-empty value. This differs from `Required` in that it treats a nil pointer as valid.
|
||||||
|
* `Skip`: this is a special rule used to indicate that all rules following it should be skipped (including the nested ones).
|
||||||
|
* `MultipleOf`: checks if the value is a multiple of the specified range.
|
||||||
|
* `Each(rules ...Rule)`: checks the elements within an iterable (map/slice/array) with other rules.
|
||||||
|
|
||||||
|
The `is` sub-package provides a list of commonly used string validation rules that can be used to check if the format
|
||||||
|
of a value satisfies certain requirements. Note that these rules only handle strings and byte slices and if a string
|
||||||
|
or byte slice is empty, it is considered valid. You may use a `Required` rule to ensure a value is not empty.
|
||||||
|
Below is the whole list of the rules provided by the `is` package:
|
||||||
|
|
||||||
|
* `Email`: validates if a string is an email or not
|
||||||
|
* `URL`: validates if a string is a valid URL
|
||||||
|
* `RequestURL`: validates if a string is a valid request URL
|
||||||
|
* `RequestURI`: validates if a string is a valid request URI
|
||||||
|
* `Alpha`: validates if a string contains English letters only (a-zA-Z)
|
||||||
|
* `Digit`: validates if a string contains digits only (0-9)
|
||||||
|
* `Alphanumeric`: validates if a string contains English letters and digits only (a-zA-Z0-9)
|
||||||
|
* `UTFLetter`: validates if a string contains unicode letters only
|
||||||
|
* `UTFDigit`: validates if a string contains unicode decimal digits only
|
||||||
|
* `UTFLetterNumeric`: validates if a string contains unicode letters and numbers only
|
||||||
|
* `UTFNumeric`: validates if a string contains unicode number characters (category N) only
|
||||||
|
* `LowerCase`: validates if a string contains lower case unicode letters only
|
||||||
|
* `UpperCase`: validates if a string contains upper case unicode letters only
|
||||||
|
* `Hexadecimal`: validates if a string is a valid hexadecimal number
|
||||||
|
* `HexColor`: validates if a string is a valid hexadecimal color code
|
||||||
|
* `RGBColor`: validates if a string is a valid RGB color in the form of rgb(R, G, B)
|
||||||
|
* `Int`: validates if a string is a valid integer number
|
||||||
|
* `Float`: validates if a string is a floating point number
|
||||||
|
* `UUIDv3`: validates if a string is a valid version 3 UUID
|
||||||
|
* `UUIDv4`: validates if a string is a valid version 4 UUID
|
||||||
|
* `UUIDv5`: validates if a string is a valid version 5 UUID
|
||||||
|
* `UUID`: validates if a string is a valid UUID
|
||||||
|
* `CreditCard`: validates if a string is a valid credit card number
|
||||||
|
* `ISBN10`: validates if a string is an ISBN version 10
|
||||||
|
* `ISBN13`: validates if a string is an ISBN version 13
|
||||||
|
* `ISBN`: validates if a string is an ISBN (either version 10 or 13)
|
||||||
|
* `JSON`: validates if a string is in valid JSON format
|
||||||
|
* `ASCII`: validates if a string contains ASCII characters only
|
||||||
|
* `PrintableASCII`: validates if a string contains printable ASCII characters only
|
||||||
|
* `Multibyte`: validates if a string contains multibyte characters
|
||||||
|
* `FullWidth`: validates if a string contains full-width characters
|
||||||
|
* `HalfWidth`: validates if a string contains half-width characters
|
||||||
|
* `VariableWidth`: validates if a string contains both full-width and half-width characters
|
||||||
|
* `Base64`: validates if a string is encoded in Base64
|
||||||
|
* `DataURI`: validates if a string is a valid base64-encoded data URI
|
||||||
|
* `E164`: validates if a string is a valid E164 phone number (+19251232233)
|
||||||
|
* `CountryCode2`: validates if a string is a valid ISO3166 Alpha 2 country code
|
||||||
|
* `CountryCode3`: validates if a string is a valid ISO3166 Alpha 3 country code
|
||||||
|
* `DialString`: validates if a string is a valid dial string that can be passed to Dial()
|
||||||
|
* `MAC`: validates if a string is a MAC address
|
||||||
|
* `IP`: validates if a string is a valid IP address (either version 4 or 6)
|
||||||
|
* `IPv4`: validates if a string is a valid version 4 IP address
|
||||||
|
* `IPv6`: validates if a string is a valid version 6 IP address
|
||||||
|
* `Subdomain`: validates if a string is valid subdomain
|
||||||
|
* `Domain`: validates if a string is valid domain
|
||||||
|
* `DNSName`: validates if a string is valid DNS name
|
||||||
|
* `Host`: validates if a string is a valid IP (both v4 and v6) or a valid DNS name
|
||||||
|
* `Port`: validates if a string is a valid port number
|
||||||
|
* `MongoID`: validates if a string is a valid Mongo ID
|
||||||
|
* `Latitude`: validates if a string is a valid latitude
|
||||||
|
* `Longitude`: validates if a string is a valid longitude
|
||||||
|
* `SSN`: validates if a string is a social security number (SSN)
|
||||||
|
* `Semver`: validates if a string is a valid semantic version
|
||||||
|
|
||||||
|
### Customizing Error Messages
|
||||||
|
|
||||||
|
All built-in validation rules allow you to customize error messages. To do so, simply call the `Error()` method
|
||||||
|
of the rules. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
data := "2123"
|
||||||
|
err := validation.Validate(data,
|
||||||
|
validation.Required.Error("is required"),
|
||||||
|
validation.Match(regexp.MustCompile("^[0-9]{5}$")).Error("must be a string with five digits"),
|
||||||
|
)
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output:
|
||||||
|
// must be a string with five digits
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Creating Custom Rules
|
||||||
|
|
||||||
|
Creating a custom rule is as simple as implementing the `validation.Rule` interface. The interface contains a single
|
||||||
|
method as shown below, which should validate the value and return the validation error, if any:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Validate validates a value and returns an error if validation fails.
|
||||||
|
Validate(value interface{}) error
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have a function with the same signature as shown above, you can call `validation.By()` to turn
|
||||||
|
it into a validation rule. For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
func checkAbc(value interface{}) error {
|
||||||
|
s, _ := value.(string)
|
||||||
|
if s != "abc" {
|
||||||
|
return errors.New("must be abc")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validation.Validate("xyz", validation.By(checkAbc))
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: must be abc
|
||||||
|
```
|
||||||
|
|
||||||
|
If your validation function takes additional parameters, you can use the following closure trick:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func stringEquals(str string) validation.RuleFunc {
|
||||||
|
return func(value interface{}) error {
|
||||||
|
s, _ := value.(string)
|
||||||
|
if s != str {
|
||||||
|
return errors.New("unexpected string")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := validation.Validate("xyz", validation.By(stringEquals("abc")))
|
||||||
|
fmt.Println(err)
|
||||||
|
// Output: unexpected string
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Rule Groups
|
||||||
|
|
||||||
|
When a combination of several rules are used in multiple places, you may use the following trick to create a
|
||||||
|
rule group so that your code is more maintainable.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var NameRule = []validation.Rule{
|
||||||
|
validation.Required,
|
||||||
|
validation.Length(5, 20),
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
FirstName string
|
||||||
|
LastName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u User) Validate() error {
|
||||||
|
return validation.ValidateStruct(&u,
|
||||||
|
validation.Field(&u.FirstName, NameRule...),
|
||||||
|
validation.Field(&u.LastName, NameRule...),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, we create a rule group `NameRule` which consists of two validation rules. We then use this rule
|
||||||
|
group to validate both `FirstName` and `LastName`.
|
||||||
|
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
The `is` sub-package wraps the excellent validators provided by the [govalidator](https://github.com/asaskevich/govalidator) package.
|
|
@ -0,0 +1,46 @@
|
||||||
|
# Upgrade Instructions
|
||||||
|
|
||||||
|
## Upgrade from 2.x to 3.x
|
||||||
|
|
||||||
|
* Instead of using `StructRules` to define struct validation rules, use `ValidateStruct()` to declare and perform
|
||||||
|
struct validation. The following code snippet shows how to modify your code:
|
||||||
|
```go
|
||||||
|
// 2.x usage
|
||||||
|
err := validation.StructRules{}.
|
||||||
|
Add("Street", validation.Required, validation.Length(5, 50)).
|
||||||
|
Add("City", validation.Required, validation.Length(5, 50)).
|
||||||
|
Add("State", validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))).
|
||||||
|
Add("Zip", validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))).
|
||||||
|
Validate(a)
|
||||||
|
|
||||||
|
// 3.x usage
|
||||||
|
err := validation.ValidateStruct(&a,
|
||||||
|
validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
|
||||||
|
validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
|
||||||
|
validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
|
||||||
|
validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
* Instead of using `Rules` to declare a rule list and use it to validate a value, call `Validate()` with the rules directly.
|
||||||
|
```go
|
||||||
|
data := "example"
|
||||||
|
|
||||||
|
// 2.x usage
|
||||||
|
rules := validation.Rules{
|
||||||
|
validation.Required,
|
||||||
|
validation.Length(5, 100),
|
||||||
|
is.URL,
|
||||||
|
}
|
||||||
|
err := rules.Validate(data)
|
||||||
|
|
||||||
|
// 3.x usage
|
||||||
|
err := validation.Validate(data,
|
||||||
|
validation.Required,
|
||||||
|
validation.Length(5, 100),
|
||||||
|
is.URL,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
* The default struct tags used for determining error keys is changed from `validation` to `json`. You may modify
|
||||||
|
`validation.ErrorTag` to change it back.
|
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DateRule struct {
|
||||||
|
layout string
|
||||||
|
min, max time.Time
|
||||||
|
message string
|
||||||
|
rangeMessage string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date returns a validation rule that checks if a string value is in a format that can be parsed into a date.
|
||||||
|
// The format of the date should be specified as the layout parameter which accepts the same value as that for time.Parse.
|
||||||
|
// For example,
|
||||||
|
// validation.Date(time.ANSIC)
|
||||||
|
// validation.Date("02 Jan 06 15:04 MST")
|
||||||
|
// validation.Date("2006-01-02")
|
||||||
|
//
|
||||||
|
// By calling Min() and/or Max(), you can let the Date rule to check if a parsed date value is within
|
||||||
|
// the specified date range.
|
||||||
|
//
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
func Date(layout string) *DateRule {
|
||||||
|
return &DateRule{
|
||||||
|
layout: layout,
|
||||||
|
message: "must be a valid date",
|
||||||
|
rangeMessage: "the data is out of range",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message that is used when the value being validated is not a valid date.
|
||||||
|
func (r *DateRule) Error(message string) *DateRule {
|
||||||
|
r.message = message
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// RangeError sets the error message that is used when the value being validated is out of the specified Min/Max date range.
|
||||||
|
func (r *DateRule) RangeError(message string) *DateRule {
|
||||||
|
r.rangeMessage = message
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min sets the minimum date range. A zero value means skipping the minimum range validation.
|
||||||
|
func (r *DateRule) Min(min time.Time) *DateRule {
|
||||||
|
r.min = min
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max sets the maximum date range. A zero value means skipping the maximum range validation.
|
||||||
|
func (r *DateRule) Max(max time.Time) *DateRule {
|
||||||
|
r.max = max
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is a valid date.
|
||||||
|
func (r *DateRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil || IsEmpty(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
str, err := EnsureString(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
date, err := time.Parse(r.layout, str)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.min.IsZero() && r.min.After(date) || !r.max.IsZero() && date.After(r.max) {
|
||||||
|
return errors.New(r.rangeMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Each returns a validation rule that loops through an iterable (map, slice or array)
|
||||||
|
// and validates each value inside with the provided rules.
|
||||||
|
// An empty iterable is considered valid. Use the Required rule to make sure the iterable is not empty.
|
||||||
|
func Each(rules ...Rule) *EachRule {
|
||||||
|
return &EachRule{
|
||||||
|
rules: rules,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type EachRule struct {
|
||||||
|
rules []Rule
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loops through the given iterable and calls the Ozzo Validate() method for each value.
|
||||||
|
func (r *EachRule) Validate(value interface{}) error {
|
||||||
|
errs := Errors{}
|
||||||
|
|
||||||
|
v := reflect.ValueOf(value)
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Map:
|
||||||
|
for _, k := range v.MapKeys() {
|
||||||
|
val := r.getInterface(v.MapIndex(k))
|
||||||
|
if err := Validate(val, r.rules...); err != nil {
|
||||||
|
errs[r.getString(k)] = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Slice, reflect.Array:
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
val := r.getInterface(v.Index(i))
|
||||||
|
if err := Validate(val, r.rules...); err != nil {
|
||||||
|
errs[strconv.Itoa(i)] = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("must be an iterable (map, slice or array)")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EachRule) getInterface(value reflect.Value) interface{} {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Interface:
|
||||||
|
if value.IsNil() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return value.Elem().Interface()
|
||||||
|
default:
|
||||||
|
return value.Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EachRule) getString(value reflect.Value) string {
|
||||||
|
switch value.Kind() {
|
||||||
|
case reflect.Ptr, reflect.Interface:
|
||||||
|
if value.IsNil() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return value.Elem().String()
|
||||||
|
default:
|
||||||
|
return value.String()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Errors represents the validation errors that are indexed by struct field names, map or slice keys.
|
||||||
|
Errors map[string]error
|
||||||
|
|
||||||
|
// InternalError represents an error that should NOT be treated as a validation error.
|
||||||
|
InternalError interface {
|
||||||
|
error
|
||||||
|
InternalError() error
|
||||||
|
}
|
||||||
|
|
||||||
|
internalError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewInternalError wraps a given error into an InternalError.
|
||||||
|
func NewInternalError(err error) InternalError {
|
||||||
|
return &internalError{error: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalError returns the actual error that it wraps around.
|
||||||
|
func (e *internalError) InternalError() error {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error returns the error string of Errors.
|
||||||
|
func (es Errors) Error() string {
|
||||||
|
if len(es) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{}
|
||||||
|
for key := range es {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
s := ""
|
||||||
|
for i, key := range keys {
|
||||||
|
if i > 0 {
|
||||||
|
s += "; "
|
||||||
|
}
|
||||||
|
if errs, ok := es[key].(Errors); ok {
|
||||||
|
s += fmt.Sprintf("%v: (%v)", key, errs)
|
||||||
|
} else {
|
||||||
|
s += fmt.Sprintf("%v: %v", key, es[key].Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s + "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON converts the Errors into a valid JSON.
|
||||||
|
func (es Errors) MarshalJSON() ([]byte, error) {
|
||||||
|
errs := map[string]interface{}{}
|
||||||
|
for key, err := range es {
|
||||||
|
if ms, ok := err.(json.Marshaler); ok {
|
||||||
|
errs[key] = ms
|
||||||
|
} else {
|
||||||
|
errs[key] = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.Marshal(errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter removes all nils from Errors and returns back the updated Errors as an error.
|
||||||
|
// If the length of Errors becomes 0, it will return nil.
|
||||||
|
func (es Errors) Filter() error {
|
||||||
|
for key, value := range es {
|
||||||
|
if value == nil {
|
||||||
|
delete(es, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(es) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return es
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// In returns a validation rule that checks if a value can be found in the given list of values.
|
||||||
|
// Note that the value being checked and the possible range of values must be of the same type.
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
func In(values ...interface{}) *InRule {
|
||||||
|
return &InRule{
|
||||||
|
elements: values,
|
||||||
|
message: "must be a valid value",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InRule struct {
|
||||||
|
elements []interface{}
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (r *InRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil || IsEmpty(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range r.elements {
|
||||||
|
if e == value {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (r *InRule) Error(message string) *InRule {
|
||||||
|
r.message = message
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package is provides a list of commonly used string validation rules.
|
||||||
|
package is
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"github.com/asaskevich/govalidator"
|
||||||
|
"github.com/go-ozzo/ozzo-validation"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Email validates if a string is an email or not.
|
||||||
|
Email = validation.NewStringRule(govalidator.IsEmail, "must be a valid email address")
|
||||||
|
// URL validates if a string is a valid URL
|
||||||
|
URL = validation.NewStringRule(govalidator.IsURL, "must be a valid URL")
|
||||||
|
// RequestURL validates if a string is a valid request URL
|
||||||
|
RequestURL = validation.NewStringRule(govalidator.IsRequestURL, "must be a valid request URL")
|
||||||
|
// RequestURI validates if a string is a valid request URI
|
||||||
|
RequestURI = validation.NewStringRule(govalidator.IsRequestURI, "must be a valid request URI")
|
||||||
|
// Alpha validates if a string contains English letters only (a-zA-Z)
|
||||||
|
Alpha = validation.NewStringRule(govalidator.IsAlpha, "must contain English letters only")
|
||||||
|
// Digit validates if a string contains digits only (0-9)
|
||||||
|
Digit = validation.NewStringRule(isDigit, "must contain digits only")
|
||||||
|
// Alphanumeric validates if a string contains English letters and digits only (a-zA-Z0-9)
|
||||||
|
Alphanumeric = validation.NewStringRule(govalidator.IsAlphanumeric, "must contain English letters and digits only")
|
||||||
|
// UTFLetter validates if a string contains unicode letters only
|
||||||
|
UTFLetter = validation.NewStringRule(govalidator.IsUTFLetter, "must contain unicode letter characters only")
|
||||||
|
// UTFDigit validates if a string contains unicode decimal digits only
|
||||||
|
UTFDigit = validation.NewStringRule(govalidator.IsUTFDigit, "must contain unicode decimal digits only")
|
||||||
|
// UTFLetterNumeric validates if a string contains unicode letters and numbers only
|
||||||
|
UTFLetterNumeric = validation.NewStringRule(govalidator.IsUTFLetterNumeric, "must contain unicode letters and numbers only")
|
||||||
|
// UTFNumeric validates if a string contains unicode number characters (category N) only
|
||||||
|
UTFNumeric = validation.NewStringRule(isUTFNumeric, "must contain unicode number characters only")
|
||||||
|
// LowerCase validates if a string contains lower case unicode letters only
|
||||||
|
LowerCase = validation.NewStringRule(govalidator.IsLowerCase, "must be in lower case")
|
||||||
|
// UpperCase validates if a string contains upper case unicode letters only
|
||||||
|
UpperCase = validation.NewStringRule(govalidator.IsUpperCase, "must be in upper case")
|
||||||
|
// Hexadecimal validates if a string is a valid hexadecimal number
|
||||||
|
Hexadecimal = validation.NewStringRule(govalidator.IsHexadecimal, "must be a valid hexadecimal number")
|
||||||
|
// HexColor validates if a string is a valid hexadecimal color code
|
||||||
|
HexColor = validation.NewStringRule(govalidator.IsHexcolor, "must be a valid hexadecimal color code")
|
||||||
|
// RGBColor validates if a string is a valid RGB color in the form of rgb(R, G, B)
|
||||||
|
RGBColor = validation.NewStringRule(govalidator.IsRGBcolor, "must be a valid RGB color code")
|
||||||
|
// Int validates if a string is a valid integer number
|
||||||
|
Int = validation.NewStringRule(govalidator.IsInt, "must be an integer number")
|
||||||
|
// Float validates if a string is a floating point number
|
||||||
|
Float = validation.NewStringRule(govalidator.IsFloat, "must be a floating point number")
|
||||||
|
// UUIDv3 validates if a string is a valid version 3 UUID
|
||||||
|
UUIDv3 = validation.NewStringRule(govalidator.IsUUIDv3, "must be a valid UUID v3")
|
||||||
|
// UUIDv4 validates if a string is a valid version 4 UUID
|
||||||
|
UUIDv4 = validation.NewStringRule(govalidator.IsUUIDv4, "must be a valid UUID v4")
|
||||||
|
// UUIDv5 validates if a string is a valid version 5 UUID
|
||||||
|
UUIDv5 = validation.NewStringRule(govalidator.IsUUIDv5, "must be a valid UUID v5")
|
||||||
|
// UUID validates if a string is a valid UUID
|
||||||
|
UUID = validation.NewStringRule(govalidator.IsUUID, "must be a valid UUID")
|
||||||
|
// CreditCard validates if a string is a valid credit card number
|
||||||
|
CreditCard = validation.NewStringRule(govalidator.IsCreditCard, "must be a valid credit card number")
|
||||||
|
// ISBN10 validates if a string is an ISBN version 10
|
||||||
|
ISBN10 = validation.NewStringRule(govalidator.IsISBN10, "must be a valid ISBN-10")
|
||||||
|
// ISBN13 validates if a string is an ISBN version 13
|
||||||
|
ISBN13 = validation.NewStringRule(govalidator.IsISBN13, "must be a valid ISBN-13")
|
||||||
|
// ISBN validates if a string is an ISBN (either version 10 or 13)
|
||||||
|
ISBN = validation.NewStringRule(isISBN, "must be a valid ISBN")
|
||||||
|
// JSON validates if a string is in valid JSON format
|
||||||
|
JSON = validation.NewStringRule(govalidator.IsJSON, "must be in valid JSON format")
|
||||||
|
// ASCII validates if a string contains ASCII characters only
|
||||||
|
ASCII = validation.NewStringRule(govalidator.IsASCII, "must contain ASCII characters only")
|
||||||
|
// PrintableASCII validates if a string contains printable ASCII characters only
|
||||||
|
PrintableASCII = validation.NewStringRule(govalidator.IsPrintableASCII, "must contain printable ASCII characters only")
|
||||||
|
// Multibyte validates if a string contains multibyte characters
|
||||||
|
Multibyte = validation.NewStringRule(govalidator.IsMultibyte, "must contain multibyte characters")
|
||||||
|
// FullWidth validates if a string contains full-width characters
|
||||||
|
FullWidth = validation.NewStringRule(govalidator.IsFullWidth, "must contain full-width characters")
|
||||||
|
// HalfWidth validates if a string contains half-width characters
|
||||||
|
HalfWidth = validation.NewStringRule(govalidator.IsHalfWidth, "must contain half-width characters")
|
||||||
|
// VariableWidth validates if a string contains both full-width and half-width characters
|
||||||
|
VariableWidth = validation.NewStringRule(govalidator.IsVariableWidth, "must contain both full-width and half-width characters")
|
||||||
|
// Base64 validates if a string is encoded in Base64
|
||||||
|
Base64 = validation.NewStringRule(govalidator.IsBase64, "must be encoded in Base64")
|
||||||
|
// DataURI validates if a string is a valid base64-encoded data URI
|
||||||
|
DataURI = validation.NewStringRule(govalidator.IsDataURI, "must be a Base64-encoded data URI")
|
||||||
|
// E164 validates if a string is a valid ISO3166 Alpha 2 country code
|
||||||
|
E164 = validation.NewStringRule(isE164Number, "must be a valid E164 number")
|
||||||
|
// CountryCode2 validates if a string is a valid ISO3166 Alpha 2 country code
|
||||||
|
CountryCode2 = validation.NewStringRule(govalidator.IsISO3166Alpha2, "must be a valid two-letter country code")
|
||||||
|
// CountryCode3 validates if a string is a valid ISO3166 Alpha 3 country code
|
||||||
|
CountryCode3 = validation.NewStringRule(govalidator.IsISO3166Alpha3, "must be a valid three-letter country code")
|
||||||
|
// CurrencyCode validates if a string is a valid IsISO4217 currency code.
|
||||||
|
CurrencyCode = validation.NewStringRule(govalidator.IsISO4217, "must be valid ISO 4217 currency code")
|
||||||
|
// DialString validates if a string is a valid dial string that can be passed to Dial()
|
||||||
|
DialString = validation.NewStringRule(govalidator.IsDialString, "must be a valid dial string")
|
||||||
|
// MAC validates if a string is a MAC address
|
||||||
|
MAC = validation.NewStringRule(govalidator.IsMAC, "must be a valid MAC address")
|
||||||
|
// IP validates if a string is a valid IP address (either version 4 or 6)
|
||||||
|
IP = validation.NewStringRule(govalidator.IsIP, "must be a valid IP address")
|
||||||
|
// IPv4 validates if a string is a valid version 4 IP address
|
||||||
|
IPv4 = validation.NewStringRule(govalidator.IsIPv4, "must be a valid IPv4 address")
|
||||||
|
// IPv6 validates if a string is a valid version 6 IP address
|
||||||
|
IPv6 = validation.NewStringRule(govalidator.IsIPv6, "must be a valid IPv6 address")
|
||||||
|
// Subdomain validates if a string is valid subdomain
|
||||||
|
Subdomain = validation.NewStringRule(isSubdomain, "must be a valid subdomain")
|
||||||
|
// Domain validates if a string is valid domain
|
||||||
|
Domain = validation.NewStringRule(isDomain, "must be a valid domain")
|
||||||
|
// DNSName validates if a string is valid DNS name
|
||||||
|
DNSName = validation.NewStringRule(govalidator.IsDNSName, "must be a valid DNS name")
|
||||||
|
// Host validates if a string is a valid IP (both v4 and v6) or a valid DNS name
|
||||||
|
Host = validation.NewStringRule(govalidator.IsHost, "must be a valid IP address or DNS name")
|
||||||
|
// Port validates if a string is a valid port number
|
||||||
|
Port = validation.NewStringRule(govalidator.IsPort, "must be a valid port number")
|
||||||
|
// MongoID validates if a string is a valid Mongo ID
|
||||||
|
MongoID = validation.NewStringRule(govalidator.IsMongoID, "must be a valid hex-encoded MongoDB ObjectId")
|
||||||
|
// Latitude validates if a string is a valid latitude
|
||||||
|
Latitude = validation.NewStringRule(govalidator.IsLatitude, "must be a valid latitude")
|
||||||
|
// Longitude validates if a string is a valid longitude
|
||||||
|
Longitude = validation.NewStringRule(govalidator.IsLongitude, "must be a valid longitude")
|
||||||
|
// SSN validates if a string is a social security number (SSN)
|
||||||
|
SSN = validation.NewStringRule(govalidator.IsSSN, "must be a valid social security number")
|
||||||
|
// Semver validates if a string is a valid semantic version
|
||||||
|
Semver = validation.NewStringRule(govalidator.IsSemver, "must be a valid semantic version")
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reDigit = regexp.MustCompile("^[0-9]+$")
|
||||||
|
// Subdomain regex source: https://stackoverflow.com/a/7933253
|
||||||
|
reSubdomain = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$`)
|
||||||
|
// E164 regex source: https://stackoverflow.com/a/23299989
|
||||||
|
reE164 = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
|
||||||
|
// Domain regex source: https://stackoverflow.com/a/7933253
|
||||||
|
// Slightly modified: Removed 255 max length validation since Go regex does not
|
||||||
|
// support lookarounds. More info: https://stackoverflow.com/a/38935027
|
||||||
|
reDomain = regexp.MustCompile(`^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+(?:[a-z]{1,63}| xn--[a-z0-9]{1,59})$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func isISBN(value string) bool {
|
||||||
|
return govalidator.IsISBN(value, 10) || govalidator.IsISBN(value, 13)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(value string) bool {
|
||||||
|
return reDigit.MatchString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isE164Number(value string) bool {
|
||||||
|
return reE164.MatchString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSubdomain(value string) bool {
|
||||||
|
return reSubdomain.MatchString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDomain(value string) bool {
|
||||||
|
if len(value) > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return reDomain.MatchString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUTFNumeric(value string) bool {
|
||||||
|
for _, c := range value {
|
||||||
|
if unicode.IsNumber(c) == false {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Length returns a validation rule that checks if a value's length is within the specified range.
|
||||||
|
// If max is 0, it means there is no upper bound for the length.
|
||||||
|
// This rule should only be used for validating strings, slices, maps, and arrays.
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
func Length(min, max int) *LengthRule {
|
||||||
|
message := "the value must be empty"
|
||||||
|
if min == 0 && max > 0 {
|
||||||
|
message = fmt.Sprintf("the length must be no more than %v", max)
|
||||||
|
} else if min > 0 && max == 0 {
|
||||||
|
message = fmt.Sprintf("the length must be no less than %v", min)
|
||||||
|
} else if min > 0 && max > 0 {
|
||||||
|
if min == max {
|
||||||
|
message = fmt.Sprintf("the length must be exactly %v", min)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("the length must be between %v and %v", min, max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &LengthRule{
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuneLength returns a validation rule that checks if a string's rune length is within the specified range.
|
||||||
|
// If max is 0, it means there is no upper bound for the length.
|
||||||
|
// This rule should only be used for validating strings, slices, maps, and arrays.
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
// If the value being validated is not a string, the rule works the same as Length.
|
||||||
|
func RuneLength(min, max int) *LengthRule {
|
||||||
|
r := Length(min, max)
|
||||||
|
r.rune = true
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type LengthRule struct {
|
||||||
|
min, max int
|
||||||
|
message string
|
||||||
|
rune bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (v *LengthRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil || IsEmpty(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
l int
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
if s, ok := value.(string); ok && v.rune {
|
||||||
|
l = utf8.RuneCountInString(s)
|
||||||
|
} else if l, err = LengthOfValue(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.min > 0 && l < v.min || v.max > 0 && l > v.max {
|
||||||
|
return errors.New(v.message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (v *LengthRule) Error(message string) *LengthRule {
|
||||||
|
v.message = message
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Match returns a validation rule that checks if a value matches the specified regular expression.
|
||||||
|
// This rule should only be used for validating strings and byte slices, or a validation error will be reported.
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
func Match(re *regexp.Regexp) *MatchRule {
|
||||||
|
return &MatchRule{
|
||||||
|
re: re,
|
||||||
|
message: "must be in a valid format",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MatchRule struct {
|
||||||
|
re *regexp.Regexp
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (v *MatchRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isString, str, isBytes, bs := StringOrBytes(value)
|
||||||
|
if isString && (str == "" || v.re.MatchString(str)) {
|
||||||
|
return nil
|
||||||
|
} else if isBytes && (len(bs) == 0 || v.re.Match(bs)) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return errors.New(v.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (v *MatchRule) Error(message string) *MatchRule {
|
||||||
|
v.message = message
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThresholdRule struct {
|
||||||
|
threshold interface{}
|
||||||
|
operator int
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
greaterThan = iota
|
||||||
|
greaterEqualThan
|
||||||
|
lessThan
|
||||||
|
lessEqualThan
|
||||||
|
)
|
||||||
|
|
||||||
|
// Min is a validation rule that checks if a value is greater or equal than the specified value.
|
||||||
|
// By calling Exclusive, the rule will check if the value is strictly greater than the specified value.
|
||||||
|
// Note that the value being checked and the threshold value must be of the same type.
|
||||||
|
// Only int, uint, float and time.Time types are supported.
|
||||||
|
// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
|
||||||
|
func Min(min interface{}) *ThresholdRule {
|
||||||
|
return &ThresholdRule{
|
||||||
|
threshold: min,
|
||||||
|
operator: greaterEqualThan,
|
||||||
|
message: fmt.Sprintf("must be no less than %v", min),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max is a validation rule that checks if a value is less or equal than the specified value.
|
||||||
|
// By calling Exclusive, the rule will check if the value is strictly less than the specified value.
|
||||||
|
// Note that the value being checked and the threshold value must be of the same type.
|
||||||
|
// Only int, uint, float and time.Time types are supported.
|
||||||
|
// An empty value is considered valid. Please use the Required rule to make sure a value is not empty.
|
||||||
|
func Max(max interface{}) *ThresholdRule {
|
||||||
|
return &ThresholdRule{
|
||||||
|
threshold: max,
|
||||||
|
operator: lessEqualThan,
|
||||||
|
message: fmt.Sprintf("must be no greater than %v", max),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exclusive sets the comparison to exclude the boundary value.
|
||||||
|
func (r *ThresholdRule) Exclusive() *ThresholdRule {
|
||||||
|
if r.operator == greaterEqualThan {
|
||||||
|
r.operator = greaterThan
|
||||||
|
r.message = fmt.Sprintf("must be greater than %v", r.threshold)
|
||||||
|
} else if r.operator == lessEqualThan {
|
||||||
|
r.operator = lessThan
|
||||||
|
r.message = fmt.Sprintf("must be less than %v", r.threshold)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (r *ThresholdRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil || IsEmpty(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(r.threshold)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
v, err := ToInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.compareInt(rv.Int(), v) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
v, err := ToUint(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.compareUint(rv.Uint(), v) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
v, err := ToFloat(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r.compareFloat(rv.Float(), v) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Struct:
|
||||||
|
t, ok := r.threshold.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("type not supported: %v", rv.Type())
|
||||||
|
}
|
||||||
|
v, ok := value.(time.Time)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("cannot convert %v to time.Time", reflect.TypeOf(value))
|
||||||
|
}
|
||||||
|
if v.IsZero() || r.compareTime(t, v) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type not supported: %v", rv.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (r *ThresholdRule) Error(message string) *ThresholdRule {
|
||||||
|
r.message = message
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) compareInt(threshold, value int64) bool {
|
||||||
|
switch r.operator {
|
||||||
|
case greaterThan:
|
||||||
|
return value > threshold
|
||||||
|
case greaterEqualThan:
|
||||||
|
return value >= threshold
|
||||||
|
case lessThan:
|
||||||
|
return value < threshold
|
||||||
|
default:
|
||||||
|
return value <= threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) compareUint(threshold, value uint64) bool {
|
||||||
|
switch r.operator {
|
||||||
|
case greaterThan:
|
||||||
|
return value > threshold
|
||||||
|
case greaterEqualThan:
|
||||||
|
return value >= threshold
|
||||||
|
case lessThan:
|
||||||
|
return value < threshold
|
||||||
|
default:
|
||||||
|
return value <= threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) compareFloat(threshold, value float64) bool {
|
||||||
|
switch r.operator {
|
||||||
|
case greaterThan:
|
||||||
|
return value > threshold
|
||||||
|
case greaterEqualThan:
|
||||||
|
return value >= threshold
|
||||||
|
case lessThan:
|
||||||
|
return value < threshold
|
||||||
|
default:
|
||||||
|
return value <= threshold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ThresholdRule) compareTime(threshold, value time.Time) bool {
|
||||||
|
switch r.operator {
|
||||||
|
case greaterThan:
|
||||||
|
return value.After(threshold)
|
||||||
|
case greaterEqualThan:
|
||||||
|
return value.After(threshold) || value.Equal(threshold)
|
||||||
|
case lessThan:
|
||||||
|
return value.Before(threshold)
|
||||||
|
default:
|
||||||
|
return value.Before(threshold) || value.Equal(threshold)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MultipleOf(threshold interface{}) *multipleOfRule {
|
||||||
|
return &multipleOfRule{
|
||||||
|
threshold,
|
||||||
|
fmt.Sprintf("must be multiple of %v", threshold),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type multipleOfRule struct {
|
||||||
|
threshold interface{}
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (r *multipleOfRule) Error(message string) *multipleOfRule {
|
||||||
|
r.message = message
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (r *multipleOfRule) Validate(value interface{}) error {
|
||||||
|
|
||||||
|
rv := reflect.ValueOf(r.threshold)
|
||||||
|
switch rv.Kind() {
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
v, err := ToInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if v%rv.Int() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||||
|
v, err := ToUint(value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if v%rv.Uint() == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("type not supported: %v", rv.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
// Copyright 2018 Qiang Xue, Google LLC. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NotIn returns a validation rule that checks if a value os absent from, the given list of values.
|
||||||
|
// Note that the value being checked and the possible range of values must be of the same type.
|
||||||
|
// An empty value is considered valid. Use the Required rule to make sure a value is not empty.
|
||||||
|
func NotIn(values ...interface{}) *NotInRule {
|
||||||
|
return &NotInRule{
|
||||||
|
elements: values,
|
||||||
|
message: "must not be in list",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotInRule struct {
|
||||||
|
elements []interface{}
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (r *NotInRule) Validate(value interface{}) error {
|
||||||
|
value, isNil := Indirect(value)
|
||||||
|
if isNil || IsEmpty(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, e := range r.elements {
|
||||||
|
if e == value {
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (r *NotInRule) Error(message string) *NotInRule {
|
||||||
|
r.message = message
|
||||||
|
return r
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2016 Qiang Xue. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validation
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// NotNil is a validation rule that checks if a value is not nil.
|
||||||
|
// NotNil only handles types including interface, pointer, slice, and map.
|
||||||
|
// All other types are considered valid.
|
||||||
|
var NotNil = ¬NilRule{message: "is required"}
|
||||||
|
|
||||||
|
type notNilRule struct {
|
||||||
|
message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the given value is valid or not.
|
||||||
|
func (r *notNilRule) Validate(value interface{}) error {
|
||||||
|
_, isNil := Indirect(value)
|
||||||
|
if isNil {
|
||||||
|
return errors.New(r.message)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error sets the error message for the rule.
|
||||||
|
func (r *notNilRule) Error(message string) *notNilRule {
|
||||||
|
return ¬NilRule{
|
||||||
|
message: message,
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue