Andrey Kovalev
3 years ago
3516 changed files with 956471 additions and 117 deletions
@ -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 |
||||||
|
|
||||||
|
# IDEs |
||||||
|
.idea/ |
@ -0,0 +1,10 @@ |
|||||||
|
language: go |
||||||
|
go: |
||||||
|
- 1.13 |
||||||
|
- 1.x |
||||||
|
- tip |
||||||
|
before_install: |
||||||
|
- go get github.com/mattn/goveralls |
||||||
|
- go get golang.org/x/tools/cmd/cover |
||||||
|
script: |
||||||
|
- $HOME/gopath/bin/goveralls -service=travis-ci |
@ -0,0 +1,20 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2014 Cenk Altı |
||||||
|
|
||||||
|
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,32 @@ |
|||||||
|
# Exponential Backoff [![GoDoc][godoc image]][godoc] [![Build Status][travis image]][travis] [![Coverage Status][coveralls image]][coveralls] |
||||||
|
|
||||||
|
This is a Go port of the exponential backoff algorithm from [Google's HTTP Client Library for Java][google-http-java-client]. |
||||||
|
|
||||||
|
[Exponential backoff][exponential backoff wiki] |
||||||
|
is an algorithm that uses feedback to multiplicatively decrease the rate of some process, |
||||||
|
in order to gradually find an acceptable rate. |
||||||
|
The retries exponentially increase and stop increasing when a certain threshold is met. |
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
Import path is `github.com/cenkalti/backoff/v4`. Please note the version part at the end. |
||||||
|
|
||||||
|
Use https://pkg.go.dev/github.com/cenkalti/backoff/v4 to view the documentation. |
||||||
|
|
||||||
|
## Contributing |
||||||
|
|
||||||
|
* I would like to keep this library as small as possible. |
||||||
|
* Please don't send a PR without opening an issue and discussing it first. |
||||||
|
* If proposed change is not a common use case, I will probably not accept it. |
||||||
|
|
||||||
|
[godoc]: https://pkg.go.dev/github.com/cenkalti/backoff/v4 |
||||||
|
[godoc image]: https://godoc.org/github.com/cenkalti/backoff?status.png |
||||||
|
[travis]: https://travis-ci.org/cenkalti/backoff |
||||||
|
[travis image]: https://travis-ci.org/cenkalti/backoff.png?branch=master |
||||||
|
[coveralls]: https://coveralls.io/github/cenkalti/backoff?branch=master |
||||||
|
[coveralls image]: https://coveralls.io/repos/github/cenkalti/backoff/badge.svg?branch=master |
||||||
|
|
||||||
|
[google-http-java-client]: https://github.com/google/google-http-java-client/blob/da1aa993e90285ec18579f1553339b00e19b3ab5/google-http-client/src/main/java/com/google/api/client/util/ExponentialBackOff.java |
||||||
|
[exponential backoff wiki]: http://en.wikipedia.org/wiki/Exponential_backoff |
||||||
|
|
||||||
|
[advanced example]: https://pkg.go.dev/github.com/cenkalti/backoff/v4?tab=doc#pkg-examples |
@ -0,0 +1,66 @@ |
|||||||
|
// Package backoff implements backoff algorithms for retrying operations.
|
||||||
|
//
|
||||||
|
// Use Retry function for retrying operations that may fail.
|
||||||
|
// If Retry does not meet your needs,
|
||||||
|
// copy/paste the function into your project and modify as you wish.
|
||||||
|
//
|
||||||
|
// There is also Ticker type similar to time.Ticker.
|
||||||
|
// You can use it if you need to work with channels.
|
||||||
|
//
|
||||||
|
// See Examples section below for usage examples.
|
||||||
|
package backoff |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
// BackOff is a backoff policy for retrying an operation.
|
||||||
|
type BackOff interface { |
||||||
|
// NextBackOff returns the duration to wait before retrying the operation,
|
||||||
|
// or backoff. Stop to indicate that no more retries should be made.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
//
|
||||||
|
// duration := backoff.NextBackOff();
|
||||||
|
// if (duration == backoff.Stop) {
|
||||||
|
// // Do not retry operation.
|
||||||
|
// } else {
|
||||||
|
// // Sleep for duration and retry operation.
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
NextBackOff() time.Duration |
||||||
|
|
||||||
|
// Reset to initial state.
|
||||||
|
Reset() |
||||||
|
} |
||||||
|
|
||||||
|
// Stop indicates that no more retries should be made for use in NextBackOff().
|
||||||
|
const Stop time.Duration = -1 |
||||||
|
|
||||||
|
// ZeroBackOff is a fixed backoff policy whose backoff time is always zero,
|
||||||
|
// meaning that the operation is retried immediately without waiting, indefinitely.
|
||||||
|
type ZeroBackOff struct{} |
||||||
|
|
||||||
|
func (b *ZeroBackOff) Reset() {} |
||||||
|
|
||||||
|
func (b *ZeroBackOff) NextBackOff() time.Duration { return 0 } |
||||||
|
|
||||||
|
// StopBackOff is a fixed backoff policy that always returns backoff.Stop for
|
||||||
|
// NextBackOff(), meaning that the operation should never be retried.
|
||||||
|
type StopBackOff struct{} |
||||||
|
|
||||||
|
func (b *StopBackOff) Reset() {} |
||||||
|
|
||||||
|
func (b *StopBackOff) NextBackOff() time.Duration { return Stop } |
||||||
|
|
||||||
|
// ConstantBackOff is a backoff policy that always returns the same backoff delay.
|
||||||
|
// This is in contrast to an exponential backoff policy,
|
||||||
|
// which returns a delay that grows longer as you call NextBackOff() over and over again.
|
||||||
|
type ConstantBackOff struct { |
||||||
|
Interval time.Duration |
||||||
|
} |
||||||
|
|
||||||
|
func (b *ConstantBackOff) Reset() {} |
||||||
|
func (b *ConstantBackOff) NextBackOff() time.Duration { return b.Interval } |
||||||
|
|
||||||
|
func NewConstantBackOff(d time.Duration) *ConstantBackOff { |
||||||
|
return &ConstantBackOff{Interval: d} |
||||||
|
} |
@ -0,0 +1,62 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// BackOffContext is a backoff policy that stops retrying after the context
|
||||||
|
// is canceled.
|
||||||
|
type BackOffContext interface { // nolint: golint
|
||||||
|
BackOff |
||||||
|
Context() context.Context |
||||||
|
} |
||||||
|
|
||||||
|
type backOffContext struct { |
||||||
|
BackOff |
||||||
|
ctx context.Context |
||||||
|
} |
||||||
|
|
||||||
|
// WithContext returns a BackOffContext with context ctx
|
||||||
|
//
|
||||||
|
// ctx must not be nil
|
||||||
|
func WithContext(b BackOff, ctx context.Context) BackOffContext { // nolint: golint
|
||||||
|
if ctx == nil { |
||||||
|
panic("nil context") |
||||||
|
} |
||||||
|
|
||||||
|
if b, ok := b.(*backOffContext); ok { |
||||||
|
return &backOffContext{ |
||||||
|
BackOff: b.BackOff, |
||||||
|
ctx: ctx, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return &backOffContext{ |
||||||
|
BackOff: b, |
||||||
|
ctx: ctx, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getContext(b BackOff) context.Context { |
||||||
|
if cb, ok := b.(BackOffContext); ok { |
||||||
|
return cb.Context() |
||||||
|
} |
||||||
|
if tb, ok := b.(*backOffTries); ok { |
||||||
|
return getContext(tb.delegate) |
||||||
|
} |
||||||
|
return context.Background() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *backOffContext) Context() context.Context { |
||||||
|
return b.ctx |
||||||
|
} |
||||||
|
|
||||||
|
func (b *backOffContext) NextBackOff() time.Duration { |
||||||
|
select { |
||||||
|
case <-b.ctx.Done(): |
||||||
|
return Stop |
||||||
|
default: |
||||||
|
return b.BackOff.NextBackOff() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,158 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import ( |
||||||
|
"math/rand" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
/* |
||||||
|
ExponentialBackOff is a backoff implementation that increases the backoff |
||||||
|
period for each retry attempt using a randomization function that grows exponentially. |
||||||
|
|
||||||
|
NextBackOff() is calculated using the following formula: |
||||||
|
|
||||||
|
randomized interval = |
||||||
|
RetryInterval * (random value in range [1 - RandomizationFactor, 1 + RandomizationFactor]) |
||||||
|
|
||||||
|
In other words NextBackOff() will range between the randomization factor |
||||||
|
percentage below and above the retry interval. |
||||||
|
|
||||||
|
For example, given the following parameters: |
||||||
|
|
||||||
|
RetryInterval = 2 |
||||||
|
RandomizationFactor = 0.5 |
||||||
|
Multiplier = 2 |
||||||
|
|
||||||
|
the actual backoff period used in the next retry attempt will range between 1 and 3 seconds, |
||||||
|
multiplied by the exponential, that is, between 2 and 6 seconds. |
||||||
|
|
||||||
|
Note: MaxInterval caps the RetryInterval and not the randomized interval. |
||||||
|
|
||||||
|
If the time elapsed since an ExponentialBackOff instance is created goes past the |
||||||
|
MaxElapsedTime, then the method NextBackOff() starts returning backoff.Stop. |
||||||
|
|
||||||
|
The elapsed time can be reset by calling Reset(). |
||||||
|
|
||||||
|
Example: Given the following default arguments, for 10 tries the sequence will be, |
||||||
|
and assuming we go over the MaxElapsedTime on the 10th try: |
||||||
|
|
||||||
|
Request # RetryInterval (seconds) Randomized Interval (seconds) |
||||||
|
|
||||||
|
1 0.5 [0.25, 0.75] |
||||||
|
2 0.75 [0.375, 1.125] |
||||||
|
3 1.125 [0.562, 1.687] |
||||||
|
4 1.687 [0.8435, 2.53] |
||||||
|
5 2.53 [1.265, 3.795] |
||||||
|
6 3.795 [1.897, 5.692] |
||||||
|
7 5.692 [2.846, 8.538] |
||||||
|
8 8.538 [4.269, 12.807] |
||||||
|
9 12.807 [6.403, 19.210] |
||||||
|
10 19.210 backoff.Stop |
||||||
|
|
||||||
|
Note: Implementation is not thread-safe. |
||||||
|
*/ |
||||||
|
type ExponentialBackOff struct { |
||||||
|
InitialInterval time.Duration |
||||||
|
RandomizationFactor float64 |
||||||
|
Multiplier float64 |
||||||
|
MaxInterval time.Duration |
||||||
|
// After MaxElapsedTime the ExponentialBackOff returns Stop.
|
||||||
|
// It never stops if MaxElapsedTime == 0.
|
||||||
|
MaxElapsedTime time.Duration |
||||||
|
Stop time.Duration |
||||||
|
Clock Clock |
||||||
|
|
||||||
|
currentInterval time.Duration |
||||||
|
startTime time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// Clock is an interface that returns current time for BackOff.
|
||||||
|
type Clock interface { |
||||||
|
Now() time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// Default values for ExponentialBackOff.
|
||||||
|
const ( |
||||||
|
DefaultInitialInterval = 500 * time.Millisecond |
||||||
|
DefaultRandomizationFactor = 0.5 |
||||||
|
DefaultMultiplier = 1.5 |
||||||
|
DefaultMaxInterval = 60 * time.Second |
||||||
|
DefaultMaxElapsedTime = 15 * time.Minute |
||||||
|
) |
||||||
|
|
||||||
|
// NewExponentialBackOff creates an instance of ExponentialBackOff using default values.
|
||||||
|
func NewExponentialBackOff() *ExponentialBackOff { |
||||||
|
b := &ExponentialBackOff{ |
||||||
|
InitialInterval: DefaultInitialInterval, |
||||||
|
RandomizationFactor: DefaultRandomizationFactor, |
||||||
|
Multiplier: DefaultMultiplier, |
||||||
|
MaxInterval: DefaultMaxInterval, |
||||||
|
MaxElapsedTime: DefaultMaxElapsedTime, |
||||||
|
Stop: Stop, |
||||||
|
Clock: SystemClock, |
||||||
|
} |
||||||
|
b.Reset() |
||||||
|
return b |
||||||
|
} |
||||||
|
|
||||||
|
type systemClock struct{} |
||||||
|
|
||||||
|
func (t systemClock) Now() time.Time { |
||||||
|
return time.Now() |
||||||
|
} |
||||||
|
|
||||||
|
// SystemClock implements Clock interface that uses time.Now().
|
||||||
|
var SystemClock = systemClock{} |
||||||
|
|
||||||
|
// Reset the interval back to the initial retry interval and restarts the timer.
|
||||||
|
// Reset must be called before using b.
|
||||||
|
func (b *ExponentialBackOff) Reset() { |
||||||
|
b.currentInterval = b.InitialInterval |
||||||
|
b.startTime = b.Clock.Now() |
||||||
|
} |
||||||
|
|
||||||
|
// NextBackOff calculates the next backoff interval using the formula:
|
||||||
|
// Randomized interval = RetryInterval * (1 ± RandomizationFactor)
|
||||||
|
func (b *ExponentialBackOff) NextBackOff() time.Duration { |
||||||
|
// Make sure we have not gone over the maximum elapsed time.
|
||||||
|
elapsed := b.GetElapsedTime() |
||||||
|
next := getRandomValueFromInterval(b.RandomizationFactor, rand.Float64(), b.currentInterval) |
||||||
|
b.incrementCurrentInterval() |
||||||
|
if b.MaxElapsedTime != 0 && elapsed+next > b.MaxElapsedTime { |
||||||
|
return b.Stop |
||||||
|
} |
||||||
|
return next |
||||||
|
} |
||||||
|
|
||||||
|
// GetElapsedTime returns the elapsed time since an ExponentialBackOff instance
|
||||||
|
// is created and is reset when Reset() is called.
|
||||||
|
//
|
||||||
|
// The elapsed time is computed using time.Now().UnixNano(). It is
|
||||||
|
// safe to call even while the backoff policy is used by a running
|
||||||
|
// ticker.
|
||||||
|
func (b *ExponentialBackOff) GetElapsedTime() time.Duration { |
||||||
|
return b.Clock.Now().Sub(b.startTime) |
||||||
|
} |
||||||
|
|
||||||
|
// Increments the current interval by multiplying it with the multiplier.
|
||||||
|
func (b *ExponentialBackOff) incrementCurrentInterval() { |
||||||
|
// Check for overflow, if overflow is detected set the current interval to the max interval.
|
||||||
|
if float64(b.currentInterval) >= float64(b.MaxInterval)/b.Multiplier { |
||||||
|
b.currentInterval = b.MaxInterval |
||||||
|
} else { |
||||||
|
b.currentInterval = time.Duration(float64(b.currentInterval) * b.Multiplier) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Returns a random value from the following interval:
|
||||||
|
// [currentInterval - randomizationFactor * currentInterval, currentInterval + randomizationFactor * currentInterval].
|
||||||
|
func getRandomValueFromInterval(randomizationFactor, random float64, currentInterval time.Duration) time.Duration { |
||||||
|
var delta = randomizationFactor * float64(currentInterval) |
||||||
|
var minInterval = float64(currentInterval) - delta |
||||||
|
var maxInterval = float64(currentInterval) + delta |
||||||
|
|
||||||
|
// Get a random value from the range [minInterval, maxInterval].
|
||||||
|
// The formula used below has a +1 because if the minInterval is 1 and the maxInterval is 3 then
|
||||||
|
// we want a 33% chance for selecting either 1, 2 or 3.
|
||||||
|
return time.Duration(minInterval + (random * (maxInterval - minInterval + 1))) |
||||||
|
} |
@ -0,0 +1,112 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// An Operation is executing by Retry() or RetryNotify().
|
||||||
|
// The operation will be retried using a backoff policy if it returns an error.
|
||||||
|
type Operation func() error |
||||||
|
|
||||||
|
// Notify is a notify-on-error function. It receives an operation error and
|
||||||
|
// backoff delay if the operation failed (with an error).
|
||||||
|
//
|
||||||
|
// NOTE that if the backoff policy stated to stop retrying,
|
||||||
|
// the notify function isn't called.
|
||||||
|
type Notify func(error, time.Duration) |
||||||
|
|
||||||
|
// Retry the operation o until it does not return error or BackOff stops.
|
||||||
|
// o is guaranteed to be run at least once.
|
||||||
|
//
|
||||||
|
// If o returns a *PermanentError, the operation is not retried, and the
|
||||||
|
// wrapped error is returned.
|
||||||
|
//
|
||||||
|
// Retry sleeps the goroutine for the duration returned by BackOff after a
|
||||||
|
// failed operation returns.
|
||||||
|
func Retry(o Operation, b BackOff) error { |
||||||
|
return RetryNotify(o, b, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// RetryNotify calls notify function with the error and wait duration
|
||||||
|
// for each failed attempt before sleep.
|
||||||
|
func RetryNotify(operation Operation, b BackOff, notify Notify) error { |
||||||
|
return RetryNotifyWithTimer(operation, b, notify, nil) |
||||||
|
} |
||||||
|
|
||||||
|
// RetryNotifyWithTimer calls notify function with the error and wait duration using the given Timer
|
||||||
|
// for each failed attempt before sleep.
|
||||||
|
// A default timer that uses system timer is used when nil is passed.
|
||||||
|
func RetryNotifyWithTimer(operation Operation, b BackOff, notify Notify, t Timer) error { |
||||||
|
var err error |
||||||
|
var next time.Duration |
||||||
|
if t == nil { |
||||||
|
t = &defaultTimer{} |
||||||
|
} |
||||||
|
|
||||||
|
defer func() { |
||||||
|
t.Stop() |
||||||
|
}() |
||||||
|
|
||||||
|
ctx := getContext(b) |
||||||
|
|
||||||
|
b.Reset() |
||||||
|
for { |
||||||
|
if err = operation(); err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
var permanent *PermanentError |
||||||
|
if errors.As(err, &permanent) { |
||||||
|
return permanent.Err |
||||||
|
} |
||||||
|
|
||||||
|
if next = b.NextBackOff(); next == Stop { |
||||||
|
if cerr := ctx.Err(); cerr != nil { |
||||||
|
return cerr |
||||||
|
} |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if notify != nil { |
||||||
|
notify(err, next) |
||||||
|
} |
||||||
|
|
||||||
|
t.Start(next) |
||||||
|
|
||||||
|
select { |
||||||
|
case <-ctx.Done(): |
||||||
|
return ctx.Err() |
||||||
|
case <-t.C(): |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PermanentError signals that the operation should not be retried.
|
||||||
|
type PermanentError struct { |
||||||
|
Err error |
||||||
|
} |
||||||
|
|
||||||
|
func (e *PermanentError) Error() string { |
||||||
|
return e.Err.Error() |
||||||
|
} |
||||||
|
|
||||||
|
func (e *PermanentError) Unwrap() error { |
||||||
|
return e.Err |
||||||
|
} |
||||||
|
|
||||||
|
func (e *PermanentError) Is(target error) bool { |
||||||
|
_, ok := target.(*PermanentError) |
||||||
|
return ok |
||||||
|
} |
||||||
|
|
||||||
|
// Permanent wraps the given err in a *PermanentError.
|
||||||
|
func Permanent(err error) error { |
||||||
|
if err == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return &PermanentError{ |
||||||
|
Err: err, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Ticker holds a channel that delivers `ticks' of a clock at times reported by a BackOff.
|
||||||
|
//
|
||||||
|
// Ticks will continue to arrive when the previous operation is still running,
|
||||||
|
// so operations that take a while to fail could run in quick succession.
|
||||||
|
type Ticker struct { |
||||||
|
C <-chan time.Time |
||||||
|
c chan time.Time |
||||||
|
b BackOff |
||||||
|
ctx context.Context |
||||||
|
timer Timer |
||||||
|
stop chan struct{} |
||||||
|
stopOnce sync.Once |
||||||
|
} |
||||||
|
|
||||||
|
// NewTicker returns a new Ticker containing a channel that will send
|
||||||
|
// the time at times specified by the BackOff argument. Ticker is
|
||||||
|
// guaranteed to tick at least once. The channel is closed when Stop
|
||||||
|
// method is called or BackOff stops. It is not safe to manipulate the
|
||||||
|
// provided backoff policy (notably calling NextBackOff or Reset)
|
||||||
|
// while the ticker is running.
|
||||||
|
func NewTicker(b BackOff) *Ticker { |
||||||
|
return NewTickerWithTimer(b, &defaultTimer{}) |
||||||
|
} |
||||||
|
|
||||||
|
// NewTickerWithTimer returns a new Ticker with a custom timer.
|
||||||
|
// A default timer that uses system timer is used when nil is passed.
|
||||||
|
func NewTickerWithTimer(b BackOff, timer Timer) *Ticker { |
||||||
|
if timer == nil { |
||||||
|
timer = &defaultTimer{} |
||||||
|
} |
||||||
|
c := make(chan time.Time) |
||||||
|
t := &Ticker{ |
||||||
|
C: c, |
||||||
|
c: c, |
||||||
|
b: b, |
||||||
|
ctx: getContext(b), |
||||||
|
timer: timer, |
||||||
|
stop: make(chan struct{}), |
||||||
|
} |
||||||
|
t.b.Reset() |
||||||
|
go t.run() |
||||||
|
return t |
||||||
|
} |
||||||
|
|
||||||
|
// Stop turns off a ticker. After Stop, no more ticks will be sent.
|
||||||
|
func (t *Ticker) Stop() { |
||||||
|
t.stopOnce.Do(func() { close(t.stop) }) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Ticker) run() { |
||||||
|
c := t.c |
||||||
|
defer close(c) |
||||||
|
|
||||||
|
// Ticker is guaranteed to tick at least once.
|
||||||
|
afterC := t.send(time.Now()) |
||||||
|
|
||||||
|
for { |
||||||
|
if afterC == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
select { |
||||||
|
case tick := <-afterC: |
||||||
|
afterC = t.send(tick) |
||||||
|
case <-t.stop: |
||||||
|
t.c = nil // Prevent future ticks from being sent to the channel.
|
||||||
|
return |
||||||
|
case <-t.ctx.Done(): |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (t *Ticker) send(tick time.Time) <-chan time.Time { |
||||||
|
select { |
||||||
|
case t.c <- tick: |
||||||
|
case <-t.stop: |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
next := t.b.NextBackOff() |
||||||
|
if next == Stop { |
||||||
|
t.Stop() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
t.timer.Start(next) |
||||||
|
return t.timer.C() |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
type Timer interface { |
||||||
|
Start(duration time.Duration) |
||||||
|
Stop() |
||||||
|
C() <-chan time.Time |
||||||
|
} |
||||||
|
|
||||||
|
// defaultTimer implements Timer interface using time.Timer
|
||||||
|
type defaultTimer struct { |
||||||
|
timer *time.Timer |
||||||
|
} |
||||||
|
|
||||||
|
// C returns the timers channel which receives the current time when the timer fires.
|
||||||
|
func (t *defaultTimer) C() <-chan time.Time { |
||||||
|
return t.timer.C |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts the timer to fire after the given duration
|
||||||
|
func (t *defaultTimer) Start(duration time.Duration) { |
||||||
|
if t.timer == nil { |
||||||
|
t.timer = time.NewTimer(duration) |
||||||
|
} else { |
||||||
|
t.timer.Reset(duration) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Stop is called when the timer is not used anymore and resources may be freed.
|
||||||
|
func (t *defaultTimer) Stop() { |
||||||
|
if t.timer != nil { |
||||||
|
t.timer.Stop() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
package backoff |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
/* |
||||||
|
WithMaxRetries creates a wrapper around another BackOff, which will |
||||||
|
return Stop if NextBackOff() has been called too many times since |
||||||
|
the last time Reset() was called |
||||||
|
|
||||||
|
Note: Implementation is not thread-safe. |
||||||
|
*/ |
||||||
|
func WithMaxRetries(b BackOff, max uint64) BackOff { |
||||||
|
return &backOffTries{delegate: b, maxTries: max} |
||||||
|
} |
||||||
|
|
||||||
|
type backOffTries struct { |
||||||
|
delegate BackOff |
||||||
|
maxTries uint64 |
||||||
|
numTries uint64 |
||||||
|
} |
||||||
|
|
||||||
|
func (b *backOffTries) NextBackOff() time.Duration { |
||||||
|
if b.maxTries == 0 { |
||||||
|
return Stop |
||||||
|
} |
||||||
|
if b.maxTries > 0 { |
||||||
|
if b.maxTries <= b.numTries { |
||||||
|
return Stop |
||||||
|
} |
||||||
|
b.numTries++ |
||||||
|
} |
||||||
|
return b.delegate.NextBackOff() |
||||||
|
} |
||||||
|
|
||||||
|
func (b *backOffTries) Reset() { |
||||||
|
b.numTries = 0 |
||||||
|
b.delegate.Reset() |
||||||
|
} |
@ -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,69 @@ |
|||||||
|
# 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](http://cyan4973.github.io/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 |
||||||
|
``` |
||||||
|
|
||||||
|
This implementation provides a fast pure-Go implementation and an even faster |
||||||
|
assembly implementation for amd64. |
||||||
|
|
||||||
|
## 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 | |
||||||
|
| --- | --- | --- | |
||||||
|
| 5 B | 979.66 MB/s | 1291.17 MB/s | |
||||||
|
| 100 B | 7475.26 MB/s | 7973.40 MB/s | |
||||||
|
| 4 KB | 17573.46 MB/s | 17602.65 MB/s | |
||||||
|
| 10 MB | 17131.46 MB/s | 17142.16 MB/s | |
||||||
|
|
||||||
|
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using |
||||||
|
the following commands under Go 1.11.2: |
||||||
|
|
||||||
|
``` |
||||||
|
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes' |
||||||
|
$ go test -benchtime 10s -bench '/xxhash,direct,bytes' |
||||||
|
``` |
||||||
|
|
||||||
|
## 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,235 @@ |
|||||||
|
// 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 |
||||||
|
) |
||||||
|
|
||||||
|
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
|
||||||
|
// possible in the Go code is worth a small (but measurable) performance boost
|
||||||
|
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
|
||||||
|
// convenience in the Go code in a few places where we need to intentionally
|
||||||
|
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
|
||||||
|
// result overflows a uint64).
|
||||||
|
var ( |
||||||
|
prime1v = prime1 |
||||||
|
prime2v = prime2 |
||||||
|
prime3v = prime3 |
||||||
|
prime4v = prime4 |
||||||
|
prime5v = 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 = prime1v + prime2 |
||||||
|
d.v2 = prime2 |
||||||
|
d.v3 = 0 |
||||||
|
d.v4 = -prime1v |
||||||
|
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) |
||||||
|
|
||||||
|
if d.n+n < 32 { |
||||||
|
// This new data doesn't even fill the current block.
|
||||||
|
copy(d.mem[d.n:], b) |
||||||
|
d.n += n |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if d.n > 0 { |
||||||
|
// Finish off the partial block.
|
||||||
|
copy(d.mem[d.n:], 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[32-d.n:] |
||||||
|
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 |
||||||
|
|
||||||
|
i, end := 0, d.n |
||||||
|
for ; i+8 <= end; i += 8 { |
||||||
|
k1 := round(0, u64(d.mem[i:i+8])) |
||||||
|
h ^= k1 |
||||||
|
h = rol27(h)*prime1 + prime4 |
||||||
|
} |
||||||
|
if i+4 <= end { |
||||||
|
h ^= uint64(u32(d.mem[i:i+4])) * prime1 |
||||||
|
h = rol23(h)*prime2 + prime3 |
||||||
|
i += 4 |
||||||
|
} |
||||||
|
for i < end { |
||||||
|
h ^= uint64(d.mem[i]) * prime5 |
||||||
|
h = rol11(h) * prime1 |
||||||
|
i++ |
||||||
|
} |
||||||
|
|
||||||
|
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,13 @@ |
|||||||
|
// +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,215 @@ |
|||||||
|
// +build !appengine |
||||||
|
// +build gc |
||||||
|
// +build !purego |
||||||
|
|
||||||
|
#include "textflag.h" |
||||||
|
|
||||||
|
// Register allocation: |
||||||
|
// AX h |
||||||
|
// SI pointer to advance through b |
||||||
|
// DX n |
||||||
|
// BX loop end |
||||||
|
// R8 v1, k1 |
||||||
|
// R9 v2 |
||||||
|
// R10 v3 |
||||||
|
// R11 v4 |
||||||
|
// R12 tmp |
||||||
|
// R13 prime1v |
||||||
|
// R14 prime2v |
||||||
|
// DI prime4v |
||||||
|
|
||||||
|
// round reads from and advances the buffer pointer in SI. |
||||||
|
// It assumes that R13 has prime1v and R14 has prime2v. |
||||||
|
#define round(r) \ |
||||||
|
MOVQ (SI), R12 \ |
||||||
|
ADDQ $8, SI \ |
||||||
|
IMULQ R14, R12 \ |
||||||
|
ADDQ R12, r \ |
||||||
|
ROLQ $31, r \ |
||||||
|
IMULQ R13, r |
||||||
|
|
||||||
|
// mergeRound applies a merge round on the two registers acc and val. |
||||||
|
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v. |
||||||
|
#define mergeRound(acc, val) \ |
||||||
|
IMULQ R14, val \ |
||||||
|
ROLQ $31, val \ |
||||||
|
IMULQ R13, val \ |
||||||
|
XORQ val, acc \ |
||||||
|
IMULQ R13, acc \ |
||||||
|
ADDQ DI, acc |
||||||
|
|
||||||
|
// func Sum64(b []byte) uint64 |
||||||
|
TEXT ·Sum64(SB), NOSPLIT, $0-32 |
||||||
|
// Load fixed primes. |
||||||
|
MOVQ ·prime1v(SB), R13 |
||||||
|
MOVQ ·prime2v(SB), R14 |
||||||
|
MOVQ ·prime4v(SB), DI |
||||||
|
|
||||||
|
// Load slice. |
||||||
|
MOVQ b_base+0(FP), SI |
||||||
|
MOVQ b_len+8(FP), DX |
||||||
|
LEAQ (SI)(DX*1), BX |
||||||
|
|
||||||
|
// The first loop limit will be len(b)-32. |
||||||
|
SUBQ $32, BX |
||||||
|
|
||||||
|
// Check whether we have at least one block. |
||||||
|
CMPQ DX, $32 |
||||||
|
JLT noBlocks |
||||||
|
|
||||||
|
// Set up initial state (v1, v2, v3, v4). |
||||||
|
MOVQ R13, R8 |
||||||
|
ADDQ R14, R8 |
||||||
|
MOVQ R14, R9 |
||||||
|
XORQ R10, R10 |
||||||
|
XORQ R11, R11 |
||||||
|
SUBQ R13, R11 |
||||||
|
|
||||||
|
// Loop until SI > BX. |
||||||
|
blockLoop: |
||||||
|
round(R8) |
||||||
|
round(R9) |
||||||
|
round(R10) |
||||||
|
round(R11) |
||||||
|
|
||||||
|
CMPQ SI, BX |
||||||
|
JLE blockLoop |
||||||
|
|
||||||
|
MOVQ R8, AX |
||||||
|
ROLQ $1, AX |
||||||
|
MOVQ R9, R12 |
||||||
|
ROLQ $7, R12 |
||||||
|
ADDQ R12, AX |
||||||
|
MOVQ R10, R12 |
||||||
|
ROLQ $12, R12 |
||||||
|
ADDQ R12, AX |
||||||
|
MOVQ R11, R12 |
||||||
|
ROLQ $18, R12 |
||||||
|
ADDQ R12, AX |
||||||
|
|
||||||
|
mergeRound(AX, R8) |
||||||
|
mergeRound(AX, R9) |
||||||
|
mergeRound(AX, R10) |
||||||
|
mergeRound(AX, R11) |
||||||
|
|
||||||
|
JMP afterBlocks |
||||||
|
|
||||||
|
noBlocks: |
||||||
|
MOVQ ·prime5v(SB), AX |
||||||
|
|
||||||
|
afterBlocks: |
||||||
|
ADDQ DX, AX |
||||||
|
|
||||||
|
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8. |
||||||
|
ADDQ $24, BX |
||||||
|
|
||||||
|
CMPQ SI, BX |
||||||
|
JG fourByte |
||||||
|
|
||||||
|
wordLoop: |
||||||
|
// Calculate k1. |
||||||
|
MOVQ (SI), R8 |
||||||
|
ADDQ $8, SI |
||||||
|
IMULQ R14, R8 |
||||||
|
ROLQ $31, R8 |
||||||
|
IMULQ R13, R8 |
||||||
|
|
||||||
|
XORQ R8, AX |
||||||
|
ROLQ $27, AX |
||||||
|
IMULQ R13, AX |
||||||
|
ADDQ DI, AX |
||||||
|
|
||||||
|
CMPQ SI, BX |
||||||
|
JLE wordLoop |
||||||
|
|
||||||
|
fourByte: |
||||||
|
ADDQ $4, BX |
||||||
|
CMPQ SI, BX |
||||||
|
JG singles |
||||||
|
|
||||||
|
MOVL (SI), R8 |
||||||
|
ADDQ $4, SI |
||||||
|
IMULQ R13, R8 |
||||||
|
XORQ R8, AX |
||||||
|
|
||||||
|
ROLQ $23, AX |
||||||
|
IMULQ R14, AX |
||||||
|
ADDQ ·prime3v(SB), AX |
||||||
|
|
||||||
|
singles: |
||||||
|
ADDQ $4, BX |
||||||
|
CMPQ SI, BX |
||||||
|
JGE finalize |
||||||
|
|
||||||
|
singlesLoop: |
||||||
|
MOVBQZX (SI), R12 |
||||||
|
ADDQ $1, SI |
||||||
|
IMULQ ·prime5v(SB), R12 |
||||||
|
XORQ R12, AX |
||||||
|
|
||||||
|
ROLQ $11, AX |
||||||
|
IMULQ R13, AX |
||||||
|
|
||||||
|
CMPQ SI, BX |
||||||
|
JL singlesLoop |
||||||
|
|
||||||
|
finalize: |
||||||
|
MOVQ AX, R12 |
||||||
|
SHRQ $33, R12 |
||||||
|
XORQ R12, AX |
||||||
|
IMULQ R14, AX |
||||||
|
MOVQ AX, R12 |
||||||
|
SHRQ $29, R12 |
||||||
|
XORQ R12, AX |
||||||
|
IMULQ ·prime3v(SB), AX |
||||||
|
MOVQ AX, R12 |
||||||
|
SHRQ $32, R12 |
||||||
|
XORQ R12, AX |
||||||
|
|
||||||
|
MOVQ AX, ret+24(FP) |
||||||
|
RET |
||||||
|
|
||||||
|
// writeBlocks uses the same registers as above except that it uses AX to store |
||||||
|
// the d pointer. |
||||||
|
|
||||||
|
// func writeBlocks(d *Digest, b []byte) int |
||||||
|
TEXT ·writeBlocks(SB), NOSPLIT, $0-40 |
||||||
|
// Load fixed primes needed for round. |
||||||
|
MOVQ ·prime1v(SB), R13 |
||||||
|
MOVQ ·prime2v(SB), R14 |
||||||
|
|
||||||
|
// Load slice. |
||||||
|
MOVQ b_base+8(FP), SI |
||||||
|
MOVQ b_len+16(FP), DX |
||||||
|
LEAQ (SI)(DX*1), BX |
||||||
|
SUBQ $32, BX |
||||||
|
|
||||||
|
// Load vN from d. |
||||||
|
MOVQ d+0(FP), AX |
||||||
|
MOVQ 0(AX), R8 // v1 |
||||||
|
MOVQ 8(AX), R9 // v2 |
||||||
|
MOVQ 16(AX), R10 // v3 |
||||||
|
MOVQ 24(AX), R11 // 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: |
||||||
|
round(R8) |
||||||
|
round(R9) |
||||||
|
round(R10) |
||||||
|
round(R11) |
||||||
|
|
||||||
|
CMPQ SI, BX |
||||||
|
JLE blockLoop |
||||||
|
|
||||||
|
// Copy vN back to d. |
||||||
|
MOVQ R8, 0(AX) |
||||||
|
MOVQ R9, 8(AX) |
||||||
|
MOVQ R10, 16(AX) |
||||||
|
MOVQ R11, 24(AX) |
||||||
|
|
||||||
|
// The number of bytes written is SI minus the old base pointer. |
||||||
|
SUBQ b_base+8(FP), SI |
||||||
|
MOVQ SI, ret+32(FP) |
||||||
|
|
||||||
|
RET |
@ -0,0 +1,76 @@ |
|||||||
|
// +build !amd64 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 := prime1v + prime2 |
||||||
|
v2 := prime2 |
||||||
|
v3 := uint64(0) |
||||||
|
v4 := -prime1v |
||||||
|
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) |
||||||
|
|
||||||
|
i, end := 0, len(b) |
||||||
|
for ; i+8 <= end; i += 8 { |
||||||
|
k1 := round(0, u64(b[i:i+8:len(b)])) |
||||||
|
h ^= k1 |
||||||
|
h = rol27(h)*prime1 + prime4 |
||||||
|
} |
||||||
|
if i+4 <= end { |
||||||
|
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1 |
||||||
|
h = rol23(h)*prime2 + prime3 |
||||||
|
i += 4 |
||||||
|
} |
||||||
|
for ; i < end; i++ { |
||||||
|
h ^= uint64(b[i]) * 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,15 @@ |
|||||||
|
// +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,57 @@ |
|||||||
|
// +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://golang.org/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,20 @@ |
|||||||
|
The MIT License (MIT) |
||||||
|
|
||||||
|
Copyright (c) 2013 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,178 @@ |
|||||||
|
# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color) |
||||||
|
|
||||||
|
Color lets you use colorized outputs in terms of [ANSI Escape |
||||||
|
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It |
||||||
|
has support for Windows too! The API can be used in several ways, pick one that |
||||||
|
suits you. |
||||||
|
|
||||||
|
![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) |
||||||
|
|
||||||
|
|
||||||
|
## Install |
||||||
|
|
||||||
|
```bash |
||||||
|
go get github.com/fatih/color |
||||||
|
``` |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
### Standard colors |
||||||
|
|
||||||
|
```go |
||||||
|
// Print with default helper functions |
||||||
|
color.Cyan("Prints text in cyan.") |
||||||
|
|
||||||
|
// A newline will be appended automatically |
||||||
|
color.Blue("Prints %s in blue.", "text") |
||||||
|
|
||||||
|
// These are using the default foreground colors |
||||||
|
color.Red("We have red") |
||||||
|
color.Magenta("And many others ..") |
||||||
|
|
||||||
|
``` |
||||||
|
|
||||||
|
### Mix and reuse colors |
||||||
|
|
||||||
|
```go |
||||||
|
// Create a new color object |
||||||
|
c := color.New(color.FgCyan).Add(color.Underline) |
||||||
|
c.Println("Prints cyan text with an underline.") |
||||||
|
|
||||||
|
// Or just add them to New() |
||||||
|
d := color.New(color.FgCyan, color.Bold) |
||||||
|
d.Printf("This prints bold cyan %s\n", "too!.") |
||||||
|
|
||||||
|
// Mix up foreground and background colors, create new mixes! |
||||||
|
red := color.New(color.FgRed) |
||||||
|
|
||||||
|
boldRed := red.Add(color.Bold) |
||||||
|
boldRed.Println("This will print text in bold red.") |
||||||
|
|
||||||
|
whiteBackground := red.Add(color.BgWhite) |
||||||
|
whiteBackground.Println("Red text with white background.") |
||||||
|
``` |
||||||
|
|
||||||
|
### Use your own output (io.Writer) |
||||||
|
|
||||||
|
```go |
||||||
|
// Use your own io.Writer output |
||||||
|
color.New(color.FgBlue).Fprintln(myWriter, "blue color!") |
||||||
|
|
||||||
|
blue := color.New(color.FgBlue) |
||||||
|
blue.Fprint(writer, "This will print text in blue.") |
||||||
|
``` |
||||||
|
|
||||||
|
### Custom print functions (PrintFunc) |
||||||
|
|
||||||
|
```go |
||||||
|
// Create a custom print function for convenience |
||||||
|
red := color.New(color.FgRed).PrintfFunc() |
||||||
|
red("Warning") |
||||||
|
red("Error: %s", err) |
||||||
|
|
||||||
|
// Mix up multiple attributes |
||||||
|
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() |
||||||
|
notice("Don't forget this...") |
||||||
|
``` |
||||||
|
|
||||||
|
### Custom fprint functions (FprintFunc) |
||||||
|
|
||||||
|
```go |
||||||
|
blue := color.New(color.FgBlue).FprintfFunc() |
||||||
|
blue(myWriter, "important notice: %s", stars) |
||||||
|
|
||||||
|
// Mix up with multiple attributes |
||||||
|
success := color.New(color.Bold, color.FgGreen).FprintlnFunc() |
||||||
|
success(myWriter, "Don't forget this...") |
||||||
|
``` |
||||||
|
|
||||||
|
### Insert into noncolor strings (SprintFunc) |
||||||
|
|
||||||
|
```go |
||||||
|
// Create SprintXxx functions to mix strings with other non-colorized strings: |
||||||
|
yellow := color.New(color.FgYellow).SprintFunc() |
||||||
|
red := color.New(color.FgRed).SprintFunc() |
||||||
|
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error")) |
||||||
|
|
||||||
|
info := color.New(color.FgWhite, color.BgGreen).SprintFunc() |
||||||
|
fmt.Printf("This %s rocks!\n", info("package")) |
||||||
|
|
||||||
|
// Use helper functions |
||||||
|
fmt.Println("This", color.RedString("warning"), "should be not neglected.") |
||||||
|
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.") |
||||||
|
|
||||||
|
// Windows supported too! Just don't forget to change the output to color.Output |
||||||
|
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) |
||||||
|
``` |
||||||
|
|
||||||
|
### Plug into existing code |
||||||
|
|
||||||
|
```go |
||||||
|
// Use handy standard colors |
||||||
|
color.Set(color.FgYellow) |
||||||
|
|
||||||
|
fmt.Println("Existing text will now be in yellow") |
||||||
|
fmt.Printf("This one %s\n", "too") |
||||||
|
|
||||||
|
color.Unset() // Don't forget to unset |
||||||
|
|
||||||
|
// You can mix up parameters |
||||||
|
color.Set(color.FgMagenta, color.Bold) |
||||||
|
defer color.Unset() // Use it in your function |
||||||
|
|
||||||
|
fmt.Println("All text will now be bold magenta.") |
||||||
|
``` |
||||||
|
|
||||||
|
### Disable/Enable color |
||||||
|
|
||||||
|
There might be a case where you want to explicitly disable/enable color output. the |
||||||
|
`go-isatty` package will automatically disable color output for non-tty output streams |
||||||
|
(for example if the output were piped directly to `less`). |
||||||
|
|
||||||
|
The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment |
||||||
|
variable is set (regardless of its value). |
||||||
|
|
||||||
|
`Color` has support to disable/enable colors programatically both globally and |
||||||
|
for single color definitions. For example suppose you have a CLI app and a |
||||||
|
`--no-color` bool flag. You can easily disable the color output with: |
||||||
|
|
||||||
|
```go |
||||||
|
var flagNoColor = flag.Bool("no-color", false, "Disable color output") |
||||||
|
|
||||||
|
if *flagNoColor { |
||||||
|
color.NoColor = true // disables colorized output |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
It also has support for single color definitions (local). You can |
||||||
|
disable/enable color output on the fly: |
||||||
|
|
||||||
|
```go |
||||||
|
c := color.New(color.FgCyan) |
||||||
|
c.Println("Prints cyan text") |
||||||
|
|
||||||
|
c.DisableColor() |
||||||
|
c.Println("This is printed without any color") |
||||||
|
|
||||||
|
c.EnableColor() |
||||||
|
c.Println("This prints again cyan...") |
||||||
|
``` |
||||||
|
|
||||||
|
## GitHub Actions |
||||||
|
|
||||||
|
To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams. |
||||||
|
|
||||||
|
## Todo |
||||||
|
|
||||||
|
* Save/Return previous values |
||||||
|
* Evaluate fmt.Formatter interface |
||||||
|
|
||||||
|
|
||||||
|
## Credits |
||||||
|
|
||||||
|
* [Fatih Arslan](https://github.com/fatih) |
||||||
|
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable) |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details |
@ -0,0 +1,618 @@ |
|||||||
|
package color |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"strconv" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"github.com/mattn/go-colorable" |
||||||
|
"github.com/mattn/go-isatty" |
||||||
|
) |
||||||
|
|
||||||
|
var ( |
||||||
|
// NoColor defines if the output is colorized or not. It's dynamically set to
|
||||||
|
// false or true based on the stdout's file descriptor referring to a terminal
|
||||||
|
// or not. It's also set to true if the NO_COLOR environment variable is
|
||||||
|
// set (regardless of its value). This is a global option and affects all
|
||||||
|
// colors. For more control over each color block use the methods
|
||||||
|
// DisableColor() individually.
|
||||||
|
NoColor = noColorExists() || os.Getenv("TERM") == "dumb" || |
||||||
|
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) |
||||||
|
|
||||||
|
// Output defines the standard output of the print functions. By default
|
||||||
|
// os.Stdout is used.
|
||||||
|
Output = colorable.NewColorableStdout() |
||||||
|
|
||||||
|
// Error defines a color supporting writer for os.Stderr.
|
||||||
|
Error = colorable.NewColorableStderr() |
||||||
|
|
||||||
|
// colorsCache is used to reduce the count of created Color objects and
|
||||||
|
// allows to reuse already created objects with required Attribute.
|
||||||
|
colorsCache = make(map[Attribute]*Color) |
||||||
|
colorsCacheMu sync.Mutex // protects colorsCache
|
||||||
|
) |
||||||
|
|
||||||
|
// noColorExists returns true if the environment variable NO_COLOR exists.
|
||||||
|
func noColorExists() bool { |
||||||
|
_, exists := os.LookupEnv("NO_COLOR") |
||||||
|
return exists |
||||||
|
} |
||||||
|
|
||||||
|
// Color defines a custom color object which is defined by SGR parameters.
|
||||||
|
type Color struct { |
||||||
|
params []Attribute |
||||||
|
noColor *bool |
||||||
|
} |
||||||
|
|
||||||
|
// Attribute defines a single SGR Code
|
||||||
|
type Attribute int |
||||||
|
|
||||||
|
const escape = "\x1b" |
||||||
|
|
||||||
|
// Base attributes
|
||||||
|
const ( |
||||||
|
Reset Attribute = iota |
||||||
|
Bold |
||||||
|
Faint |
||||||
|
Italic |
||||||
|
Underline |
||||||
|
BlinkSlow |
||||||
|
BlinkRapid |
||||||
|
ReverseVideo |
||||||
|
Concealed |
||||||
|
CrossedOut |
||||||
|
) |
||||||
|
|
||||||
|
// Foreground text colors
|
||||||
|
const ( |
||||||
|
FgBlack Attribute = iota + 30 |
||||||
|
FgRed |
||||||
|
FgGreen |
||||||
|
FgYellow |
||||||
|
FgBlue |
||||||
|
FgMagenta |
||||||
|
FgCyan |
||||||
|
FgWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Foreground Hi-Intensity text colors
|
||||||
|
const ( |
||||||
|
FgHiBlack Attribute = iota + 90 |
||||||
|
FgHiRed |
||||||
|
FgHiGreen |
||||||
|
FgHiYellow |
||||||
|
FgHiBlue |
||||||
|
FgHiMagenta |
||||||
|
FgHiCyan |
||||||
|
FgHiWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Background text colors
|
||||||
|
const ( |
||||||
|
BgBlack Attribute = iota + 40 |
||||||
|
BgRed |
||||||
|
BgGreen |
||||||
|
BgYellow |
||||||
|
BgBlue |
||||||
|
BgMagenta |
||||||
|
BgCyan |
||||||
|
BgWhite |
||||||
|
) |
||||||
|
|
||||||
|
// Background Hi-Intensity text colors
|
||||||
|
const ( |
||||||
|
BgHiBlack Attribute = iota + 100 |
||||||
|
BgHiRed |
||||||
|
BgHiGreen |
||||||
|
BgHiYellow |
||||||
|
BgHiBlue |
||||||
|
BgHiMagenta |
||||||
|
BgHiCyan |
||||||
|
BgHiWhite |
||||||
|
) |
||||||
|
|
||||||
|
// New returns a newly created color object.
|
||||||
|
func New(value ...Attribute) *Color { |
||||||
|
c := &Color{ |
||||||
|
params: make([]Attribute, 0), |
||||||
|
} |
||||||
|
|
||||||
|
if noColorExists() { |
||||||
|
c.noColor = boolPtr(true) |
||||||
|
} |
||||||
|
|
||||||
|
c.Add(value...) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets the given parameters immediately. It will change the color of
|
||||||
|
// output with the given SGR parameters until color.Unset() is called.
|
||||||
|
func Set(p ...Attribute) *Color { |
||||||
|
c := New(p...) |
||||||
|
c.Set() |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
// Unset resets all escape attributes and clears the output. Usually should
|
||||||
|
// be called after Set().
|
||||||
|
func Unset() { |
||||||
|
if NoColor { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Fprintf(Output, "%s[%dm", escape, Reset) |
||||||
|
} |
||||||
|
|
||||||
|
// Set sets the SGR sequence.
|
||||||
|
func (c *Color) Set() *Color { |
||||||
|
if c.isNoColorSet() { |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Fprintf(Output, c.format()) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) unset() { |
||||||
|
if c.isNoColorSet() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
Unset() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) setWriter(w io.Writer) *Color { |
||||||
|
if c.isNoColorSet() { |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Fprintf(w, c.format()) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) unsetWriter(w io.Writer) { |
||||||
|
if c.isNoColorSet() { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
if NoColor { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s[%dm", escape, Reset) |
||||||
|
} |
||||||
|
|
||||||
|
// Add is used to chain SGR parameters. Use as many as parameters to combine
|
||||||
|
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
|
||||||
|
func (c *Color) Add(value ...Attribute) *Color { |
||||||
|
c.params = append(c.params, value...) |
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) prepend(value Attribute) { |
||||||
|
c.params = append(c.params, 0) |
||||||
|
copy(c.params[1:], c.params[0:]) |
||||||
|
c.params[0] = value |
||||||
|
} |
||||||
|
|
||||||
|
// Fprint formats using the default formats for its operands and writes to w.
|
||||||
|
// Spaces are added between operands when neither is a string.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) { |
||||||
|
c.setWriter(w) |
||||||
|
defer c.unsetWriter(w) |
||||||
|
|
||||||
|
return fmt.Fprint(w, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Print formats using the default formats for its operands and writes to
|
||||||
|
// standard output. Spaces are added between operands when neither is a
|
||||||
|
// string. It returns the number of bytes written and any write error
|
||||||
|
// encountered. This is the standard fmt.Print() method wrapped with the given
|
||||||
|
// color.
|
||||||
|
func (c *Color) Print(a ...interface{}) (n int, err error) { |
||||||
|
c.Set() |
||||||
|
defer c.unset() |
||||||
|
|
||||||
|
return fmt.Fprint(Output, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fprintf formats according to a format specifier and writes to w.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { |
||||||
|
c.setWriter(w) |
||||||
|
defer c.unsetWriter(w) |
||||||
|
|
||||||
|
return fmt.Fprintf(w, format, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Printf formats according to a format specifier and writes to standard output.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// This is the standard fmt.Printf() method wrapped with the given color.
|
||||||
|
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) { |
||||||
|
c.Set() |
||||||
|
defer c.unset() |
||||||
|
|
||||||
|
return fmt.Fprintf(Output, format, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Fprintln formats using the default formats for its operands and writes to w.
|
||||||
|
// Spaces are always added between operands and a newline is appended.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) { |
||||||
|
c.setWriter(w) |
||||||
|
defer c.unsetWriter(w) |
||||||
|
|
||||||
|
return fmt.Fprintln(w, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Println formats using the default formats for its operands and writes to
|
||||||
|
// standard output. Spaces are always added between operands and a newline is
|
||||||
|
// appended. It returns the number of bytes written and any write error
|
||||||
|
// encountered. This is the standard fmt.Print() method wrapped with the given
|
||||||
|
// color.
|
||||||
|
func (c *Color) Println(a ...interface{}) (n int, err error) { |
||||||
|
c.Set() |
||||||
|
defer c.unset() |
||||||
|
|
||||||
|
return fmt.Fprintln(Output, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Sprint is just like Print, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprint(a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprint(a...)) |
||||||
|
} |
||||||
|
|
||||||
|
// Sprintln is just like Println, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprintln(a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprintln(a...)) |
||||||
|
} |
||||||
|
|
||||||
|
// Sprintf is just like Printf, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprintf(format string, a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprintf(format, a...)) |
||||||
|
} |
||||||
|
|
||||||
|
// FprintFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprint().
|
||||||
|
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) { |
||||||
|
return func(w io.Writer, a ...interface{}) { |
||||||
|
c.Fprint(w, a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PrintFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Print().
|
||||||
|
func (c *Color) PrintFunc() func(a ...interface{}) { |
||||||
|
return func(a ...interface{}) { |
||||||
|
c.Print(a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// FprintfFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprintf().
|
||||||
|
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) { |
||||||
|
return func(w io.Writer, format string, a ...interface{}) { |
||||||
|
c.Fprintf(w, format, a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PrintfFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Printf().
|
||||||
|
func (c *Color) PrintfFunc() func(format string, a ...interface{}) { |
||||||
|
return func(format string, a ...interface{}) { |
||||||
|
c.Printf(format, a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// FprintlnFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprintln().
|
||||||
|
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) { |
||||||
|
return func(w io.Writer, a ...interface{}) { |
||||||
|
c.Fprintln(w, a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// PrintlnFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Println().
|
||||||
|
func (c *Color) PrintlnFunc() func(a ...interface{}) { |
||||||
|
return func(a ...interface{}) { |
||||||
|
c.Println(a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SprintFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprint(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output, example:
|
||||||
|
//
|
||||||
|
// put := New(FgYellow).SprintFunc()
|
||||||
|
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
|
||||||
|
func (c *Color) SprintFunc() func(a ...interface{}) string { |
||||||
|
return func(a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprint(a...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SprintfFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output.
|
||||||
|
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string { |
||||||
|
return func(format string, a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprintf(format, a...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// SprintlnFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output.
|
||||||
|
func (c *Color) SprintlnFunc() func(a ...interface{}) string { |
||||||
|
return func(a ...interface{}) string { |
||||||
|
return c.wrap(fmt.Sprintln(a...)) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
|
||||||
|
// an example output might be: "1;36" -> bold cyan
|
||||||
|
func (c *Color) sequence() string { |
||||||
|
format := make([]string, len(c.params)) |
||||||
|
for i, v := range c.params { |
||||||
|
format[i] = strconv.Itoa(int(v)) |
||||||
|
} |
||||||
|
|
||||||
|
return strings.Join(format, ";") |
||||||
|
} |
||||||
|
|
||||||
|
// wrap wraps the s string with the colors attributes. The string is ready to
|
||||||
|
// be printed.
|
||||||
|
func (c *Color) wrap(s string) string { |
||||||
|
if c.isNoColorSet() { |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
return c.format() + s + c.unformat() |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) format() string { |
||||||
|
return fmt.Sprintf("%s[%sm", escape, c.sequence()) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) unformat() string { |
||||||
|
return fmt.Sprintf("%s[%dm", escape, Reset) |
||||||
|
} |
||||||
|
|
||||||
|
// DisableColor disables the color output. Useful to not change any existing
|
||||||
|
// code and still being able to output. Can be used for flags like
|
||||||
|
// "--no-color". To enable back use EnableColor() method.
|
||||||
|
func (c *Color) DisableColor() { |
||||||
|
c.noColor = boolPtr(true) |
||||||
|
} |
||||||
|
|
||||||
|
// EnableColor enables the color output. Use it in conjunction with
|
||||||
|
// DisableColor(). Otherwise this method has no side effects.
|
||||||
|
func (c *Color) EnableColor() { |
||||||
|
c.noColor = boolPtr(false) |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) isNoColorSet() bool { |
||||||
|
// check first if we have user set action
|
||||||
|
if c.noColor != nil { |
||||||
|
return *c.noColor |
||||||
|
} |
||||||
|
|
||||||
|
// if not return the global option, which is disabled by default
|
||||||
|
return NoColor |
||||||
|
} |
||||||
|
|
||||||
|
// Equals returns a boolean value indicating whether two colors are equal.
|
||||||
|
func (c *Color) Equals(c2 *Color) bool { |
||||||
|
if len(c.params) != len(c2.params) { |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
for _, attr := range c.params { |
||||||
|
if !c2.attrExists(attr) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
func (c *Color) attrExists(a Attribute) bool { |
||||||
|
for _, attr := range c.params { |
||||||
|
if attr == a { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func boolPtr(v bool) *bool { |
||||||
|
return &v |
||||||
|
} |
||||||
|
|
||||||
|
func getCachedColor(p Attribute) *Color { |
||||||
|
colorsCacheMu.Lock() |
||||||
|
defer colorsCacheMu.Unlock() |
||||||
|
|
||||||
|
c, ok := colorsCache[p] |
||||||
|
if !ok { |
||||||
|
c = New(p) |
||||||
|
colorsCache[p] = c |
||||||
|
} |
||||||
|
|
||||||
|
return c |
||||||
|
} |
||||||
|
|
||||||
|
func colorPrint(format string, p Attribute, a ...interface{}) { |
||||||
|
c := getCachedColor(p) |
||||||
|
|
||||||
|
if !strings.HasSuffix(format, "\n") { |
||||||
|
format += "\n" |
||||||
|
} |
||||||
|
|
||||||
|
if len(a) == 0 { |
||||||
|
c.Print(format) |
||||||
|
} else { |
||||||
|
c.Printf(format, a...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func colorString(format string, p Attribute, a ...interface{}) string { |
||||||
|
c := getCachedColor(p) |
||||||
|
|
||||||
|
if len(a) == 0 { |
||||||
|
return c.SprintFunc()(format) |
||||||
|
} |
||||||
|
|
||||||
|
return c.SprintfFunc()(format, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// Black is a convenient helper function to print with black foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) } |
||||||
|
|
||||||
|
// Red is a convenient helper function to print with red foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) } |
||||||
|
|
||||||
|
// Green is a convenient helper function to print with green foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) } |
||||||
|
|
||||||
|
// Yellow is a convenient helper function to print with yellow foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) } |
||||||
|
|
||||||
|
// Blue is a convenient helper function to print with blue foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) } |
||||||
|
|
||||||
|
// Magenta is a convenient helper function to print with magenta foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) } |
||||||
|
|
||||||
|
// Cyan is a convenient helper function to print with cyan foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) } |
||||||
|
|
||||||
|
// White is a convenient helper function to print with white foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) } |
||||||
|
|
||||||
|
// BlackString is a convenient helper function to return a string with black
|
||||||
|
// foreground.
|
||||||
|
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) } |
||||||
|
|
||||||
|
// RedString is a convenient helper function to return a string with red
|
||||||
|
// foreground.
|
||||||
|
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) } |
||||||
|
|
||||||
|
// GreenString is a convenient helper function to return a string with green
|
||||||
|
// foreground.
|
||||||
|
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) } |
||||||
|
|
||||||
|
// YellowString is a convenient helper function to return a string with yellow
|
||||||
|
// foreground.
|
||||||
|
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) } |
||||||
|
|
||||||
|
// BlueString is a convenient helper function to return a string with blue
|
||||||
|
// foreground.
|
||||||
|
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) } |
||||||
|
|
||||||
|
// MagentaString is a convenient helper function to return a string with magenta
|
||||||
|
// foreground.
|
||||||
|
func MagentaString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgMagenta, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// CyanString is a convenient helper function to return a string with cyan
|
||||||
|
// foreground.
|
||||||
|
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) } |
||||||
|
|
||||||
|
// WhiteString is a convenient helper function to return a string with white
|
||||||
|
// foreground.
|
||||||
|
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) } |
||||||
|
|
||||||
|
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) } |
||||||
|
|
||||||
|
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) } |
||||||
|
|
||||||
|
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) } |
||||||
|
|
||||||
|
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) } |
||||||
|
|
||||||
|
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) } |
||||||
|
|
||||||
|
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) } |
||||||
|
|
||||||
|
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) } |
||||||
|
|
||||||
|
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) } |
||||||
|
|
||||||
|
// HiBlackString is a convenient helper function to return a string with hi-intensity black
|
||||||
|
// foreground.
|
||||||
|
func HiBlackString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgHiBlack, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// HiRedString is a convenient helper function to return a string with hi-intensity red
|
||||||
|
// foreground.
|
||||||
|
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) } |
||||||
|
|
||||||
|
// HiGreenString is a convenient helper function to return a string with hi-intensity green
|
||||||
|
// foreground.
|
||||||
|
func HiGreenString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgHiGreen, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
|
||||||
|
// foreground.
|
||||||
|
func HiYellowString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgHiYellow, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
|
||||||
|
// foreground.
|
||||||
|
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) } |
||||||
|
|
||||||
|
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
|
||||||
|
// foreground.
|
||||||
|
func HiMagentaString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgHiMagenta, a...) |
||||||
|
} |
||||||
|
|
||||||
|
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
|
||||||
|
// foreground.
|
||||||
|
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) } |
||||||
|
|
||||||
|
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
|
||||||
|
// foreground.
|
||||||
|
func HiWhiteString(format string, a ...interface{}) string { |
||||||
|
return colorString(format, FgHiWhite, a...) |
||||||
|
} |
@ -0,0 +1,135 @@ |
|||||||
|
/* |
||||||
|
Package color is an ANSI color package to output colorized or SGR defined |
||||||
|
output to the standard output. The API can be used in several way, pick one |
||||||
|
that suits you. |
||||||
|
|
||||||
|
Use simple and default helper functions with predefined foreground colors: |
||||||
|
|
||||||
|
color.Cyan("Prints text in cyan.") |
||||||
|
|
||||||
|
// a newline will be appended automatically
|
||||||
|
color.Blue("Prints %s in blue.", "text") |
||||||
|
|
||||||
|
// More default foreground colors..
|
||||||
|
color.Red("We have red") |
||||||
|
color.Yellow("Yellow color too!") |
||||||
|
color.Magenta("And many others ..") |
||||||
|
|
||||||
|
// Hi-intensity colors
|
||||||
|
color.HiGreen("Bright green color.") |
||||||
|
color.HiBlack("Bright black means gray..") |
||||||
|
color.HiWhite("Shiny white color!") |
||||||
|
|
||||||
|
However there are times where custom color mixes are required. Below are some |
||||||
|
examples to create custom color objects and use the print functions of each |
||||||
|
separate color object. |
||||||
|
|
||||||
|
// Create a new color object
|
||||||
|
c := color.New(color.FgCyan).Add(color.Underline) |
||||||
|
c.Println("Prints cyan text with an underline.") |
||||||
|
|
||||||
|
// Or just add them to New()
|
||||||
|
d := color.New(color.FgCyan, color.Bold) |
||||||
|
d.Printf("This prints bold cyan %s\n", "too!.") |
||||||
|
|
||||||
|
|
||||||
|
// Mix up foreground and background colors, create new mixes!
|
||||||
|
red := color.New(color.FgRed) |
||||||
|
|
||||||
|
boldRed := red.Add(color.Bold) |
||||||
|
boldRed.Println("This will print text in bold red.") |
||||||
|
|
||||||
|
whiteBackground := red.Add(color.BgWhite) |
||||||
|
whiteBackground.Println("Red text with White background.") |
||||||
|
|
||||||
|
// Use your own io.Writer output
|
||||||
|
color.New(color.FgBlue).Fprintln(myWriter, "blue color!") |
||||||
|
|
||||||
|
blue := color.New(color.FgBlue) |
||||||
|
blue.Fprint(myWriter, "This will print text in blue.") |
||||||
|
|
||||||
|
You can create PrintXxx functions to simplify even more: |
||||||
|
|
||||||
|
// Create a custom print function for convenient
|
||||||
|
red := color.New(color.FgRed).PrintfFunc() |
||||||
|
red("warning") |
||||||
|
red("error: %s", err) |
||||||
|
|
||||||
|
// Mix up multiple attributes
|
||||||
|
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc() |
||||||
|
notice("don't forget this...") |
||||||
|
|
||||||
|
You can also FprintXxx functions to pass your own io.Writer: |
||||||
|
|
||||||
|
blue := color.New(FgBlue).FprintfFunc() |
||||||
|
blue(myWriter, "important notice: %s", stars) |
||||||
|
|
||||||
|
// Mix up with multiple attributes
|
||||||
|
success := color.New(color.Bold, color.FgGreen).FprintlnFunc() |
||||||
|
success(myWriter, don't forget this...") |
||||||
|
|
||||||
|
|
||||||
|
Or create SprintXxx functions to mix strings with other non-colorized strings: |
||||||
|
|
||||||
|
yellow := New(FgYellow).SprintFunc() |
||||||
|
red := New(FgRed).SprintFunc() |
||||||
|
|
||||||
|
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error")) |
||||||
|
|
||||||
|
info := New(FgWhite, BgGreen).SprintFunc() |
||||||
|
fmt.Printf("this %s rocks!\n", info("package")) |
||||||
|
|
||||||
|
Windows support is enabled by default. All Print functions work as intended. |
||||||
|
However only for color.SprintXXX functions, user should use fmt.FprintXXX and |
||||||
|
set the output to color.Output: |
||||||
|
|
||||||
|
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS")) |
||||||
|
|
||||||
|
info := New(FgWhite, BgGreen).SprintFunc() |
||||||
|
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package")) |
||||||
|
|
||||||
|
Using with existing code is possible. Just use the Set() method to set the |
||||||
|
standard output to the given parameters. That way a rewrite of an existing |
||||||
|
code is not required. |
||||||
|
|
||||||
|
// Use handy standard colors.
|
||||||
|
color.Set(color.FgYellow) |
||||||
|
|
||||||
|
fmt.Println("Existing text will be now in Yellow") |
||||||
|
fmt.Printf("This one %s\n", "too") |
||||||
|
|
||||||
|
color.Unset() // don't forget to unset
|
||||||
|
|
||||||
|
// You can mix up parameters
|
||||||
|
color.Set(color.FgMagenta, color.Bold) |
||||||
|
defer color.Unset() // use it in your function
|
||||||
|
|
||||||
|
fmt.Println("All text will be now bold magenta.") |
||||||
|
|
||||||
|
There might be a case where you want to disable color output (for example to |
||||||
|
pipe the standard output of your app to somewhere else). `Color` has support to |
||||||
|
disable colors both globally and for single color definition. For example |
||||||
|
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable |
||||||
|
the color output with: |
||||||
|
|
||||||
|
var flagNoColor = flag.Bool("no-color", false, "Disable color output") |
||||||
|
|
||||||
|
if *flagNoColor { |
||||||
|
color.NoColor = true // disables colorized output
|
||||||
|
} |
||||||
|
|
||||||
|
You can also disable the color by setting the NO_COLOR environment variable to any value. |
||||||
|
|
||||||
|
It also has support for single color definitions (local). You can |
||||||
|
disable/enable color output on the fly: |
||||||
|
|
||||||
|
c := color.New(color.FgCyan) |
||||||
|
c.Println("Prints cyan text") |
||||||
|
|
||||||
|
c.DisableColor() |
||||||
|
c.Println("This is printed without any color") |
||||||
|
|
||||||
|
c.EnableColor() |
||||||
|
c.Println("This prints again cyan...") |
||||||
|
*/ |
||||||
|
package color |
@ -0,0 +1,19 @@ |
|||||||
|
# Binaries for programs and plugins |
||||||
|
*.exe |
||||||
|
*.exe~ |
||||||
|
*.dll |
||||||
|
*.so |
||||||
|
*.dylib |
||||||
|
|
||||||
|
# Test binary, built with `go test -c` |
||||||
|
*.test |
||||||
|
local_testing |
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE |
||||||
|
*.out |
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it) |
||||||
|
vendor/ |
||||||
|
|
||||||
|
# IDE project files |
||||||
|
.idea |
@ -0,0 +1,49 @@ |
|||||||
|
run: |
||||||
|
timeout: 2m |
||||||
|
issues-exit-code: 1 |
||||||
|
tests: true |
||||||
|
|
||||||
|
issues: |
||||||
|
max-same-issues: 100 |
||||||
|
exclude-rules: |
||||||
|
- path: _test\.go |
||||||
|
linters: |
||||||
|
- bodyclose |
||||||
|
- errcheck |
||||||
|
- gosec |
||||||
|
|
||||||
|
linters: |
||||||
|
enable: |
||||||
|
- bodyclose |
||||||
|
- deadcode |
||||||
|
- errcheck |
||||||
|
- gofmt |
||||||
|
- revive |
||||||
|
- gosec |
||||||
|
- gosimple |
||||||
|
- govet |
||||||
|
- ineffassign |
||||||
|
- misspell |
||||||
|
- staticcheck |
||||||
|
- structcheck |
||||||
|
- typecheck |
||||||
|
- unused |
||||||
|
- varcheck |
||||||
|
|
||||||
|
output: |
||||||
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" |
||||||
|
format: colored-line-number |
||||||
|
# print lines of code with issue, default is true |
||||||
|
print-issued-lines: true |
||||||
|
# print linter name in the end of issue text, default is true |
||||||
|
print-linter-name: true |
||||||
|
# make issues output unique by line, default is true |
||||||
|
uniq-by-line: true |
||||||
|
# add a prefix to the output file references; default is no prefix |
||||||
|
path-prefix: "" |
||||||
|
# sorts results by: filepath, line and column |
||||||
|
sort-results: true |
||||||
|
|
||||||
|
linters-settings: |
||||||
|
golint: |
||||||
|
min-confidence: 0.8 |
@ -0,0 +1,73 @@ |
|||||||
|
# Contributor Covenant Code of Conduct |
||||||
|
|
||||||
|
## Our Pledge |
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as |
||||||
|
contributors and maintainers pledge to making participation in our project and |
||||||
|
our community a harassment-free experience for everyone. And we mean everyone! |
||||||
|
|
||||||
|
## Our Standards |
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment |
||||||
|
include: |
||||||
|
|
||||||
|
* Using welcoming and kind language |
||||||
|
* Being respectful of differing viewpoints and experiences |
||||||
|
* Gracefully accepting constructive criticism |
||||||
|
* Focusing on what is best for the community |
||||||
|
* Showing empathy towards other community members |
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include: |
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or |
||||||
|
advances |
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks |
||||||
|
* Public or private harassment |
||||||
|
* Publishing others' private information, such as a physical or electronic |
||||||
|
address, without explicit permission |
||||||
|
* Other conduct which could reasonably be considered inappropriate in a |
||||||
|
professional setting |
||||||
|
|
||||||
|
## Our Responsibilities |
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable |
||||||
|
behavior and are expected to take appropriate and fair corrective action in |
||||||
|
response to any instances of unacceptable behavior. |
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or |
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions |
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or |
||||||
|
permanently any contributor for other behaviors that they deem inappropriate, |
||||||
|
threatening, offensive, or harmful. |
||||||
|
|
||||||
|
## Scope |
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces |
||||||
|
when an individual is representing the project or its community. Examples of |
||||||
|
representing a project or community include using an official project e-mail |
||||||
|
address, posting via an official social media account, or acting as an appointed |
||||||
|
representative at an online or offline event. Representation of a project may be |
||||||
|
further defined and clarified by project maintainers. |
||||||
|
|
||||||
|
## Enforcement |
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be |
||||||
|
reported by contacting the project team initially on Slack to coordinate private communication. All |
||||||
|
complaints will be reviewed and investigated and will result in a response that |
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is |
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident. |
||||||
|
Further details of specific enforcement policies may be posted separately. |
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good |
||||||
|
faith may face temporary or permanent repercussions as determined by other |
||||||
|
members of the project's leadership. |
||||||
|
|
||||||
|
## Attribution |
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, |
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html |
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org |
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see |
||||||
|
https://www.contributor-covenant.org/faq |
@ -0,0 +1,40 @@ |
|||||||
|
# Contributing to gocron |
||||||
|
|
||||||
|
Thank you for coming to contribute to gocron! We welcome new ideas, PRs and general feedback. |
||||||
|
|
||||||
|
## Reporting Bugs |
||||||
|
|
||||||
|
If you find a bug then please let the project know by opening an issue after doing the following: |
||||||
|
|
||||||
|
- Do a quick search of the existing issues to make sure the bug isn't already reported |
||||||
|
- Try and make a minimal list of steps that can reliably reproduce the bug you are experiencing |
||||||
|
- Collect as much information as you can to help identify what the issue is (project version, configuration files, etc) |
||||||
|
|
||||||
|
## Suggesting Enhancements |
||||||
|
|
||||||
|
If you have a use case that you don't see a way to support yet, we would welcome the feedback in an issue. Before opening the issue, please consider: |
||||||
|
|
||||||
|
- Is this a common use case? |
||||||
|
- Is it simple to understand? |
||||||
|
|
||||||
|
You can help us out by doing the following before raising a new issue: |
||||||
|
|
||||||
|
- Check that the feature hasn't been requested already by searching existing issues |
||||||
|
- Try and reduce your enhancement into a single, concise and deliverable request, rather than a general idea |
||||||
|
- Explain your own use cases as the basis of the request |
||||||
|
|
||||||
|
## Adding Features |
||||||
|
|
||||||
|
Pull requests are always welcome. However, before going through the trouble of implementing a change it's worth creating a bug or feature request issue. |
||||||
|
This allows us to discuss the changes and make sure they are a good fit for the project. |
||||||
|
|
||||||
|
Please always make sure a pull request has been: |
||||||
|
|
||||||
|
- Unit tested with `make test` |
||||||
|
- Linted with `make lint` |
||||||
|
- Vetted with `make vet` |
||||||
|
- Formatted with `make fmt` or validated with `make check-fmt` |
||||||
|
|
||||||
|
## Writing Tests |
||||||
|
|
||||||
|
Tests should follow the [table driven test pattern](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go). See other tests in the code base for additional examples. |
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2014, 辣椒面 |
||||||
|
|
||||||
|
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,33 @@ |
|||||||
|
.PHONY: fmt check-fmt lint vet test |
||||||
|
|
||||||
|
GO_PKGS := $(shell go list -f {{.Dir}} ./...) |
||||||
|
|
||||||
|
fmt: |
||||||
|
@go list -f {{.Dir}} ./... | xargs -I{} gofmt -w -s {} |
||||||
|
|
||||||
|
check-fmt: |
||||||
|
@echo "Checking formatting..." |
||||||
|
@FMT="0"; \
|
||||||
|
for pkg in $(GO_PKGS); do \
|
||||||
|
OUTPUT=`gofmt -l $$pkg/*.go`; \
|
||||||
|
if [ -n "$$OUTPUT" ]; then \
|
||||||
|
echo "$$OUTPUT"; \
|
||||||
|
FMT="1"; \
|
||||||
|
fi; \
|
||||||
|
done ; \
|
||||||
|
if [ "$$FMT" -eq "1" ]; then \
|
||||||
|
echo "Problem with formatting in files above."; \
|
||||||
|
exit 1; \
|
||||||
|
else \
|
||||||
|
echo "Success - way to run gofmt!"; \
|
||||||
|
fi |
||||||
|
|
||||||
|
lint: |
||||||
|
# Add -set_exit_status=true when/if we want to enforce the linter rules
|
||||||
|
@golint -min_confidence 0.8 -set_exit_status $(GO_PKGS) |
||||||
|
|
||||||
|
vet: |
||||||
|
@go vet $(GO_FLAGS) $(GO_PKGS) |
||||||
|
|
||||||
|
test: |
||||||
|
@go test -race -v $(GO_FLAGS) -count=1 $(GO_PKGS) |
@ -0,0 +1,132 @@ |
|||||||
|
# gocron: A Golang Job Scheduling Package. |
||||||
|
|
||||||
|
[![CI State](https://github.com/go-co-op/gocron/workflows/Go%20Test/badge.svg)](https://github.com/go-co-op/gocron/actions?query=workflow%3A"lint") ![Go Report Card](https://goreportcard.com/badge/github.com/go-co-op/gocron) [![Go Doc](https://godoc.org/github.com/go-co-op/gocron?status.svg)](https://pkg.go.dev/github.com/go-co-op/gocron) |
||||||
|
|
||||||
|
gocron is a job scheduling package which lets you run Go functions at pre-determined intervals using a simple, human-friendly syntax. |
||||||
|
|
||||||
|
gocron is a Golang scheduler implementation similar to the Ruby module [clockwork](https://github.com/tomykaira/clockwork) and the Python job scheduling package [schedule](https://github.com/dbader/schedule). |
||||||
|
|
||||||
|
See also these two great articles that were used for design input: |
||||||
|
|
||||||
|
- [Rethinking Cron](http://adam.herokuapp.com/past/2010/4/13/rethinking_cron/) |
||||||
|
- [Replace Cron with Clockwork](http://adam.herokuapp.com/past/2010/6/30/replace_cron_with_clockwork/) |
||||||
|
|
||||||
|
If you want to chat, you can find us at Slack! [<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW) |
||||||
|
|
||||||
|
## Concepts |
||||||
|
|
||||||
|
- **Scheduler**: The scheduler tracks all the jobs assigned to it and makes sure they are passed to the executor when ready to be run. The scheduler is able to manage overall aspects of job behavior like limiting how many jobs are running at one time. |
||||||
|
- **Job**: The job is simply aware of the task (go function) it's provided and is therefore only able to perform actions related to that task like preventing itself from overruning a previous task that is taking a long time. |
||||||
|
- **Executor**: The executor, as it's name suggests, is simply responsible for calling the task (go function) that the job hands to it when sent by the scheduler. |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
```golang |
||||||
|
s := gocron.NewScheduler(time.UTC) |
||||||
|
|
||||||
|
s.Every(5).Seconds().Do(func(){ ... }) |
||||||
|
|
||||||
|
// strings parse to duration |
||||||
|
s.Every("5m").Do(func(){ ... }) |
||||||
|
|
||||||
|
s.Every(5).Days().Do(func(){ ... }) |
||||||
|
|
||||||
|
s.Every(1).Month(1, 2, 3).Do(func(){ ... }) |
||||||
|
|
||||||
|
// set time |
||||||
|
s.Every(1).Day().At("10:30").Do(func(){ ... }) |
||||||
|
|
||||||
|
// set multiple times |
||||||
|
s.Every(1).Day().At("10:30;08:00").Do(func(){ ... }) |
||||||
|
|
||||||
|
s.Every(1).Day().At("10:30").At("08:00").Do(func(){ ... }) |
||||||
|
|
||||||
|
// Schedule each last day of the month |
||||||
|
s.Every(1).MonthLastDay().Do(func(){ ... }) |
||||||
|
|
||||||
|
// Or each last day of every other month |
||||||
|
s.Every(2).MonthLastDay().Do(func(){ ... }) |
||||||
|
|
||||||
|
// cron expressions supported |
||||||
|
s.Cron("*/1 * * * *").Do(task) // every minute |
||||||
|
|
||||||
|
// you can start running the scheduler in two different ways: |
||||||
|
// starts the scheduler asynchronously |
||||||
|
s.StartAsync() |
||||||
|
// starts the scheduler and blocks current execution path |
||||||
|
s.StartBlocking() |
||||||
|
``` |
||||||
|
|
||||||
|
For more examples, take a look in our [go docs](https://pkg.go.dev/github.com/go-co-op/gocron#pkg-examples) |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
| Interval | Supported schedule options | |
||||||
|
| ------------ | ------------------------------------------------------------------- | |
||||||
|
| sub-second | `StartAt()` | |
||||||
|
| milliseconds | `StartAt()` | |
||||||
|
| seconds | `StartAt()` | |
||||||
|
| minutes | `StartAt()` | |
||||||
|
| hours | `StartAt()` | |
||||||
|
| days | `StartAt()`, `At()` | |
||||||
|
| weeks | `StartAt()`, `At()`, `Weekday()` (and all week day named functions) | |
||||||
|
| months | `StartAt()`, `At()` | |
||||||
|
|
||||||
|
There are several options available to restrict how jobs run: |
||||||
|
|
||||||
|
| Mode | Function | Behavior | |
||||||
|
| --------------- | ------------------------ | ------------------------------------------------------------------------------- | |
||||||
|
| Default | | jobs are rescheduled at every interval | |
||||||
|
| Job singleton | `SingletonMode()` | a long running job will not be rescheduled until the current run is completed | |
||||||
|
| Scheduler limit | `SetMaxConcurrentJobs()` | set a collective maximum number of concurrent jobs running across the scheduler | |
||||||
|
|
||||||
|
## Tags |
||||||
|
|
||||||
|
Jobs may have arbitrary tags added which can be useful when tracking many jobs. |
||||||
|
The scheduler supports both enforcing tags to be unique and when not unique, |
||||||
|
running all jobs with a given tag. |
||||||
|
|
||||||
|
```golang |
||||||
|
s := gocron.NewScheduler(time.UTC) |
||||||
|
s.TagsUnique() |
||||||
|
|
||||||
|
_, _ = s.Every(1).Week().Tag("foo").Do(task) |
||||||
|
_, err := s.Every(1).Week().Tag("foo").Do(task) |
||||||
|
// error!!! |
||||||
|
|
||||||
|
s := gocron.NewScheduler(time.UTC) |
||||||
|
|
||||||
|
s.Every(2).Day().Tag("tag").At("10:00").Do(task) |
||||||
|
s.Every(1).Minute().Tag("tag").Do(task) |
||||||
|
s.RunByTag("tag") |
||||||
|
// both jobs will run |
||||||
|
``` |
||||||
|
|
||||||
|
## FAQ |
||||||
|
|
||||||
|
- Q: I'm running multiple pods on a distributed environment. How can I make a job not run once per pod causing duplication? |
||||||
|
- A: We recommend using your own lock solution within the jobs themselves (you could use [Redis](https://redis.io/topics/distlock), for example) |
||||||
|
|
||||||
|
- Q: I've removed my job from the scheduler, but how can I stop a long-running job that has already been triggered? |
||||||
|
- A: We recommend using a means of canceling your job, e.g. a `context.WithCancel()`. |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
Looking to contribute? Try to follow these guidelines: |
||||||
|
|
||||||
|
- Use issues for everything |
||||||
|
- For a small change, just send a PR! |
||||||
|
- For bigger changes, please open an issue for discussion before sending a PR. |
||||||
|
- PRs should have: tests, documentation and examples (if it makes sense) |
||||||
|
- You can also contribute by: |
||||||
|
- Reporting issues |
||||||
|
- Suggesting new features or enhancements |
||||||
|
- Improving/fixing documentation |
||||||
|
|
||||||
|
--- |
||||||
|
|
||||||
|
## Design |
||||||
|
|
||||||
|
![design-diagram](https://user-images.githubusercontent.com/19351306/110375142-2ba88680-8017-11eb-80c3-554cc746b165.png) |
||||||
|
|
||||||
|
[Jetbrains](https://www.jetbrains.com/?from=gocron) supports this project with GoLand licenses. We appreciate their support for free and open source software! |
@ -0,0 +1,15 @@ |
|||||||
|
# Security Policy |
||||||
|
|
||||||
|
## Supported Versions |
||||||
|
|
||||||
|
The current plan is to maintain version 1 as long as possible incorporating any necessary security patches. |
||||||
|
|
||||||
|
| Version | Supported | |
||||||
|
| ------- | ------------------ | |
||||||
|
| 1.x.x | :white_check_mark: | |
||||||
|
|
||||||
|
## Reporting a Vulnerability |
||||||
|
|
||||||
|
Vulnerabilities can be reported by [opening an issue](https://github.com/go-co-op/gocron/issues/new/choose) or reaching out on Slack: [<img src="https://img.shields.io/badge/gophers-gocron-brightgreen?logo=slack">](https://gophers.slack.com/archives/CQ7T0T1FW) |
||||||
|
|
||||||
|
We will do our best to addrerss any vulnerabilites in an expeditious manner. |
@ -0,0 +1,111 @@ |
|||||||
|
package gocron |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"sync" |
||||||
|
|
||||||
|
"golang.org/x/sync/semaphore" |
||||||
|
) |
||||||
|
|
||||||
|
const ( |
||||||
|
// RescheduleMode - the default is that if a limit on maximum
|
||||||
|
// concurrent jobs is set and the limit is reached, a job will
|
||||||
|
// skip it's run and try again on the next occurrence in the schedule
|
||||||
|
RescheduleMode limitMode = iota |
||||||
|
|
||||||
|
// WaitMode - if a limit on maximum concurrent jobs is set
|
||||||
|
// and the limit is reached, a job will wait to try and run
|
||||||
|
// until a spot in the limit is freed up.
|
||||||
|
//
|
||||||
|
// Note: this mode can produce unpredictable results as
|
||||||
|
// job execution order isn't guaranteed. For example, a job that
|
||||||
|
// executes frequently may pile up in the wait queue and be executed
|
||||||
|
// many times back to back when the queue opens.
|
||||||
|
WaitMode |
||||||
|
) |
||||||
|
|
||||||
|
type executor struct { |
||||||
|
jobFunctions chan jobFunction |
||||||
|
stopCh chan struct{} |
||||||
|
limitMode limitMode |
||||||
|
maxRunningJobs *semaphore.Weighted |
||||||
|
} |
||||||
|
|
||||||
|
func newExecutor() executor { |
||||||
|
return executor{ |
||||||
|
jobFunctions: make(chan jobFunction, 1), |
||||||
|
stopCh: make(chan struct{}, 1), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *executor) start() { |
||||||
|
stopCtx, cancel := context.WithCancel(context.Background()) |
||||||
|
runningJobsWg := sync.WaitGroup{} |
||||||
|
|
||||||
|
for { |
||||||
|
select { |
||||||
|
case f := <-e.jobFunctions: |
||||||
|
runningJobsWg.Add(1) |
||||||
|
go func() { |
||||||
|
defer runningJobsWg.Done() |
||||||
|
|
||||||
|
if e.maxRunningJobs != nil { |
||||||
|
if !e.maxRunningJobs.TryAcquire(1) { |
||||||
|
|
||||||
|
switch e.limitMode { |
||||||
|
case RescheduleMode: |
||||||
|
return |
||||||
|
case WaitMode: |
||||||
|
for { |
||||||
|
select { |
||||||
|
case <-stopCtx.Done(): |
||||||
|
return |
||||||
|
case <-f.ctx.Done(): |
||||||
|
return |
||||||
|
default: |
||||||
|
} |
||||||
|
|
||||||
|
if e.maxRunningJobs.TryAcquire(1) { |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
defer e.maxRunningJobs.Release(1) |
||||||
|
} |
||||||
|
|
||||||
|
switch f.runConfig.mode { |
||||||
|
case defaultMode: |
||||||
|
f.incrementRunState() |
||||||
|
callJobFuncWithParams(f.function, f.parameters) |
||||||
|
f.decrementRunState() |
||||||
|
case singletonMode: |
||||||
|
_, _, _ = f.limiter.Do("main", func() (interface{}, error) { |
||||||
|
select { |
||||||
|
case <-stopCtx.Done(): |
||||||
|
return nil, nil |
||||||
|
case <-f.ctx.Done(): |
||||||
|
return nil, nil |
||||||
|
default: |
||||||
|
} |
||||||
|
f.incrementRunState() |
||||||
|
callJobFuncWithParams(f.function, f.parameters) |
||||||
|
f.decrementRunState() |
||||||
|
return nil, nil |
||||||
|
}) |
||||||
|
} |
||||||
|
}() |
||||||
|
case <-e.stopCh: |
||||||
|
cancel() |
||||||
|
runningJobsWg.Wait() |
||||||
|
e.stopCh <- struct{}{} |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *executor) stop() { |
||||||
|
e.stopCh <- struct{}{} |
||||||
|
<-e.stopCh |
||||||
|
} |
@ -0,0 +1,101 @@ |
|||||||
|
// Package gocron : A Golang Job Scheduling Package.
|
||||||
|
//
|
||||||
|
// An in-process scheduler for periodic jobs that uses the builder pattern
|
||||||
|
// for configuration. gocron lets you run Golang functions periodically
|
||||||
|
// at pre-determined intervals using a simple, human-friendly syntax.
|
||||||
|
//
|
||||||
|
package gocron |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"reflect" |
||||||
|
"regexp" |
||||||
|
"runtime" |
||||||
|
"time" |
||||||
|
) |
||||||
|
|
||||||
|
// Error declarations for gocron related errors
|
||||||
|
var ( |
||||||
|
ErrNotAFunction = errors.New("only functions can be scheduled into the job queue") |
||||||
|
ErrNotScheduledWeekday = errors.New("job not scheduled weekly on a weekday") |
||||||
|
ErrJobNotFoundWithTag = errors.New("no jobs found with given tag") |
||||||
|
ErrUnsupportedTimeFormat = errors.New("the given time format is not supported") |
||||||
|
ErrInvalidInterval = errors.New(".Every() interval must be greater than 0") |
||||||
|
ErrInvalidIntervalType = errors.New(".Every() interval must be int, time.Duration, or string") |
||||||
|
ErrInvalidIntervalUnitsSelection = errors.New(".Every(time.Duration) and .Cron() cannot be used with units (e.g. .Seconds())") |
||||||
|
|
||||||
|
ErrAtTimeNotSupported = errors.New("the At() method is not supported for this time unit") |
||||||
|
ErrWeekdayNotSupported = errors.New("weekday is not supported for time unit") |
||||||
|
ErrInvalidDayOfMonthEntry = errors.New("only days 1 through 28 are allowed for monthly schedules") |
||||||
|
ErrTagsUnique = func(tag string) error { return fmt.Errorf("a non-unique tag was set on the job: %s", tag) } |
||||||
|
ErrWrongParams = errors.New("wrong list of params") |
||||||
|
ErrUpdateCalledWithoutJob = errors.New("a call to Scheduler.Update() requires a call to Scheduler.Job() first") |
||||||
|
ErrCronParseFailure = errors.New("cron expression failed to be parsed") |
||||||
|
ErrInvalidDaysOfMonthDuplicateValue = errors.New("duplicate days of month is not allowed in Month() and Months() methods") |
||||||
|
) |
||||||
|
|
||||||
|
func wrapOrError(toWrap error, err error) error { |
||||||
|
var returnErr error |
||||||
|
if toWrap != nil && !errors.Is(err, toWrap) { |
||||||
|
returnErr = fmt.Errorf("%s: %w", err, toWrap) |
||||||
|
} else { |
||||||
|
returnErr = err |
||||||
|
} |
||||||
|
return returnErr |
||||||
|
} |
||||||
|
|
||||||
|
// regex patterns for supported time formats
|
||||||
|
var ( |
||||||
|
timeWithSeconds = regexp.MustCompile(`(?m)^\d{1,2}:\d\d:\d\d$`) |
||||||
|
timeWithoutSeconds = regexp.MustCompile(`(?m)^\d{1,2}:\d\d$`) |
||||||
|
) |
||||||
|
|
||||||
|
type schedulingUnit int |
||||||
|
|
||||||
|
const ( |
||||||
|
// default unit is seconds
|
||||||
|
milliseconds schedulingUnit = iota |
||||||
|
seconds |
||||||
|
minutes |
||||||
|
hours |
||||||
|
days |
||||||
|
weeks |
||||||
|
months |
||||||
|
duration |
||||||
|
crontab |
||||||
|
) |
||||||
|
|
||||||
|
func callJobFuncWithParams(jobFunc interface{}, params []interface{}) { |
||||||
|
f := reflect.ValueOf(jobFunc) |
||||||
|
if len(params) != f.Type().NumIn() { |
||||||
|
return |
||||||
|
} |
||||||
|
in := make([]reflect.Value, len(params)) |
||||||
|
for k, param := range params { |
||||||
|
in[k] = reflect.ValueOf(param) |
||||||
|
} |
||||||
|
f.Call(in) |
||||||
|
} |
||||||
|
|
||||||
|
func getFunctionName(fn interface{}) string { |
||||||
|
return runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() |
||||||
|
} |
||||||
|
|
||||||
|
func parseTime(t string) (hour, min, sec int, err error) { |
||||||
|
var timeLayout string |
||||||
|
switch { |
||||||
|
case timeWithSeconds.Match([]byte(t)): |
||||||
|
timeLayout = "15:04:05" |
||||||
|
case timeWithoutSeconds.Match([]byte(t)): |
||||||
|
timeLayout = "15:04" |
||||||
|
default: |
||||||
|
return 0, 0, 0, ErrUnsupportedTimeFormat |
||||||
|
} |
||||||
|
|
||||||
|
parsedTime, err := time.Parse(timeLayout, t) |
||||||
|
if err != nil { |
||||||
|
return 0, 0, 0, ErrUnsupportedTimeFormat |
||||||
|
} |
||||||
|
return parsedTime.Hour(), parsedTime.Minute(), parsedTime.Second(), nil |
||||||
|
} |
@ -0,0 +1,381 @@ |
|||||||
|
package gocron |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"fmt" |
||||||
|
"sort" |
||||||
|
"sync" |
||||||
|
"sync/atomic" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/robfig/cron/v3" |
||||||
|
"golang.org/x/sync/singleflight" |
||||||
|
) |
||||||
|
|
||||||
|
// Job struct stores the information necessary to run a Job
|
||||||
|
type Job struct { |
||||||
|
mu sync.RWMutex |
||||||
|
jobFunction |
||||||
|
interval int // pause interval * unit between runs
|
||||||
|
duration time.Duration // time duration between runs
|
||||||
|
unit schedulingUnit // time units, e.g. 'minutes', 'hours'...
|
||||||
|
startsImmediately bool // if the Job should run upon scheduler start
|
||||||
|
atTimes []time.Duration // optional time(s) at which this Job runs when interval is day
|
||||||
|
startAtTime time.Time // optional time at which the Job starts
|
||||||
|
error error // error related to Job
|
||||||
|
lastRun time.Time // datetime of last run
|
||||||
|
nextRun time.Time // datetime of next run
|
||||||
|
scheduledWeekdays []time.Weekday // Specific days of the week to start on
|
||||||
|
daysOfTheMonth []int // Specific days of the month to run the job
|
||||||
|
tags []string // allow the user to tag Jobs with certain labels
|
||||||
|
runCount int // number of times the job ran
|
||||||
|
timer *time.Timer // handles running tasks at specific time
|
||||||
|
cronSchedule cron.Schedule // stores the schedule when a task uses cron
|
||||||
|
} |
||||||
|
|
||||||
|
type jobFunction struct { |
||||||
|
function interface{} // task's function
|
||||||
|
parameters []interface{} // task's function parameters
|
||||||
|
name string //nolint the function name to run
|
||||||
|
runConfig runConfig // configuration for how many times to run the job
|
||||||
|
limiter *singleflight.Group // limits inflight runs of job to one
|
||||||
|
ctx context.Context // for cancellation
|
||||||
|
cancel context.CancelFunc // for cancellation
|
||||||
|
runState *int64 // will be non-zero when jobs are running
|
||||||
|
} |
||||||
|
|
||||||
|
func (jf *jobFunction) incrementRunState() { |
||||||
|
if jf.runState != nil { |
||||||
|
atomic.AddInt64(jf.runState, 1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (jf *jobFunction) decrementRunState() { |
||||||
|
if jf.runState != nil { |
||||||
|
atomic.AddInt64(jf.runState, -1) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
type runConfig struct { |
||||||
|
finiteRuns bool |
||||||
|
maxRuns int |
||||||
|
mode mode |
||||||
|
} |
||||||
|
|
||||||
|
// mode is the Job's running mode
|
||||||
|
type mode int8 |
||||||
|
|
||||||
|
const ( |
||||||
|
// defaultMode disable any mode
|
||||||
|
defaultMode mode = iota |
||||||
|
|
||||||
|
// singletonMode switch to single job mode
|
||||||
|
singletonMode |
||||||
|
) |
||||||
|
|
||||||
|
// newJob creates a new Job with the provided interval
|
||||||
|
func newJob(interval int, startImmediately bool, singletonMode bool) *Job { |
||||||
|
ctx, cancel := context.WithCancel(context.Background()) |
||||||
|
var zero int64 |
||||||
|
job := &Job{ |
||||||
|
interval: interval, |
||||||
|
unit: seconds, |
||||||
|
lastRun: time.Time{}, |
||||||
|
nextRun: time.Time{}, |
||||||
|
jobFunction: jobFunction{ |
||||||
|
ctx: ctx, |
||||||
|
cancel: cancel, |
||||||
|
runState: &zero, |
||||||
|
}, |
||||||
|
tags: []string{}, |
||||||
|
startsImmediately: startImmediately, |
||||||
|
} |
||||||
|
if singletonMode { |
||||||
|
job.SingletonMode() |
||||||
|
} |
||||||
|
return job |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) neverRan() bool { |
||||||
|
return j.lastRun.IsZero() |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getStartsImmediately() bool { |
||||||
|
return j.startsImmediately |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setStartsImmediately(b bool) { |
||||||
|
j.startsImmediately = b |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setTimer(t *time.Timer) { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.timer = t |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getFirstAtTime() time.Duration { |
||||||
|
var t time.Duration |
||||||
|
if len(j.atTimes) > 0 { |
||||||
|
t = j.atTimes[0] |
||||||
|
} |
||||||
|
|
||||||
|
return t |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getAtTime(lastRun time.Time) time.Duration { |
||||||
|
var r time.Duration |
||||||
|
if len(j.atTimes) == 0 { |
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
if len(j.atTimes) == 1 { |
||||||
|
return j.atTimes[0] |
||||||
|
} |
||||||
|
|
||||||
|
if lastRun.IsZero() { |
||||||
|
r = j.atTimes[0] |
||||||
|
} else { |
||||||
|
for _, d := range j.atTimes { |
||||||
|
nt := time.Date(lastRun.Year(), lastRun.Month(), lastRun.Day(), 0, 0, 0, 0, lastRun.Location()).Add(d) |
||||||
|
if nt.After(lastRun) { |
||||||
|
r = d |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) addAtTime(t time.Duration) { |
||||||
|
if len(j.atTimes) == 0 { |
||||||
|
j.atTimes = append(j.atTimes, t) |
||||||
|
return |
||||||
|
} |
||||||
|
exist := false |
||||||
|
index := sort.Search(len(j.atTimes), func(i int) bool { |
||||||
|
atTime := j.atTimes[i] |
||||||
|
b := atTime >= t |
||||||
|
if b { |
||||||
|
exist = atTime == t |
||||||
|
} |
||||||
|
return b |
||||||
|
}) |
||||||
|
|
||||||
|
// ignore if present
|
||||||
|
if exist { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
j.atTimes = append(j.atTimes, time.Duration(0)) |
||||||
|
copy(j.atTimes[index+1:], j.atTimes[index:]) |
||||||
|
j.atTimes[index] = t |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getStartAtTime() time.Time { |
||||||
|
return j.startAtTime |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setStartAtTime(t time.Time) { |
||||||
|
j.startAtTime = t |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getUnit() schedulingUnit { |
||||||
|
j.mu.RLock() |
||||||
|
defer j.mu.RUnlock() |
||||||
|
return j.unit |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setUnit(t schedulingUnit) { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.unit = t |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) getDuration() time.Duration { |
||||||
|
j.mu.RLock() |
||||||
|
defer j.mu.RUnlock() |
||||||
|
return j.duration |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setDuration(t time.Duration) { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.duration = t |
||||||
|
} |
||||||
|
|
||||||
|
// hasTags returns true if all tags are matched on this Job
|
||||||
|
func (j *Job) hasTags(tags ...string) bool { |
||||||
|
// Build map of all Job tags for easy comparison
|
||||||
|
jobTags := map[string]int{} |
||||||
|
for _, tag := range j.tags { |
||||||
|
jobTags[tag] = 0 |
||||||
|
} |
||||||
|
|
||||||
|
// Loop through required tags and if one doesn't exist, return false
|
||||||
|
for _, tag := range tags { |
||||||
|
_, ok := jobTags[tag] |
||||||
|
if !ok { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
// Error returns an error if one occurred while creating the Job.
|
||||||
|
// If multiple errors occurred, they will be wrapped and can be
|
||||||
|
// checked using the standard unwrap options.
|
||||||
|
func (j *Job) Error() error { |
||||||
|
return j.error |
||||||
|
} |
||||||
|
|
||||||
|
// Tag allows you to add arbitrary labels to a Job that do not
|
||||||
|
// impact the functionality of the Job
|
||||||
|
func (j *Job) Tag(tags ...string) { |
||||||
|
j.tags = append(j.tags, tags...) |
||||||
|
} |
||||||
|
|
||||||
|
// Untag removes a tag from a Job
|
||||||
|
func (j *Job) Untag(t string) { |
||||||
|
var newTags []string |
||||||
|
for _, tag := range j.tags { |
||||||
|
if t != tag { |
||||||
|
newTags = append(newTags, tag) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
j.tags = newTags |
||||||
|
} |
||||||
|
|
||||||
|
// Tags returns the tags attached to the Job
|
||||||
|
func (j *Job) Tags() []string { |
||||||
|
return j.tags |
||||||
|
} |
||||||
|
|
||||||
|
// ScheduledTime returns the time of the Job's next scheduled run
|
||||||
|
func (j *Job) ScheduledTime() time.Time { |
||||||
|
j.mu.RLock() |
||||||
|
defer j.mu.RUnlock() |
||||||
|
return j.nextRun |
||||||
|
} |
||||||
|
|
||||||
|
// ScheduledAtTime returns the specific time of day the Job will run at.
|
||||||
|
// If multiple times are set, the earliest time will be returned.
|
||||||
|
func (j *Job) ScheduledAtTime() string { |
||||||
|
if len(j.atTimes) == 0 { |
||||||
|
return "0:0" |
||||||
|
} |
||||||
|
|
||||||
|
return fmt.Sprintf("%d:%d", j.getFirstAtTime()/time.Hour, (j.getFirstAtTime()%time.Hour)/time.Minute) |
||||||
|
} |
||||||
|
|
||||||
|
// ScheduledAtTimes returns the specific times of day the Job will run at
|
||||||
|
func (j *Job) ScheduledAtTimes() []string { |
||||||
|
r := make([]string, len(j.atTimes)) |
||||||
|
for i, t := range j.atTimes { |
||||||
|
r[i] = fmt.Sprintf("%d:%d", t/time.Hour, (t%time.Hour)/time.Minute) |
||||||
|
} |
||||||
|
|
||||||
|
return r |
||||||
|
} |
||||||
|
|
||||||
|
// Weekday returns which day of the week the Job will run on and
|
||||||
|
// will return an error if the Job is not scheduled weekly
|
||||||
|
func (j *Job) Weekday() (time.Weekday, error) { |
||||||
|
if len(j.scheduledWeekdays) == 0 { |
||||||
|
return time.Sunday, ErrNotScheduledWeekday |
||||||
|
} |
||||||
|
return j.scheduledWeekdays[0], nil |
||||||
|
} |
||||||
|
|
||||||
|
// Weekdays returns a slice of time.Weekday that the Job will run in a week and
|
||||||
|
// will return an error if the Job is not scheduled weekly
|
||||||
|
func (j *Job) Weekdays() []time.Weekday { |
||||||
|
// appending on j.scheduledWeekdays may cause a side effect
|
||||||
|
if len(j.scheduledWeekdays) == 0 { |
||||||
|
return []time.Weekday{time.Sunday} |
||||||
|
} |
||||||
|
|
||||||
|
return j.scheduledWeekdays |
||||||
|
} |
||||||
|
|
||||||
|
// LimitRunsTo limits the number of executions of this job to n.
|
||||||
|
// Upon reaching the limit, the job is removed from the scheduler.
|
||||||
|
//
|
||||||
|
// Note: If a job is added to a running scheduler and this method is then used
|
||||||
|
// you may see the job run more than the set limit as job is scheduled immediately
|
||||||
|
// by default upon being added to the scheduler. It is recommended to use the
|
||||||
|
// LimitRunsTo() func on the scheduler chain when scheduling the job.
|
||||||
|
// For example: scheduler.LimitRunsTo(1).Do()
|
||||||
|
func (j *Job) LimitRunsTo(n int) { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.runConfig.finiteRuns = true |
||||||
|
j.runConfig.maxRuns = n |
||||||
|
} |
||||||
|
|
||||||
|
// SingletonMode prevents a new job from starting if the prior job has not yet
|
||||||
|
// completed it's run
|
||||||
|
// Note: If a job is added to a running scheduler and this method is then used
|
||||||
|
// you may see the job run overrun itself as job is scheduled immediately
|
||||||
|
// by default upon being added to the scheduler. It is recommended to use the
|
||||||
|
// SingletonMode() func on the scheduler chain when scheduling the job.
|
||||||
|
func (j *Job) SingletonMode() { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.runConfig.mode = singletonMode |
||||||
|
j.jobFunction.limiter = &singleflight.Group{} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// shouldRun evaluates if this job should run again
|
||||||
|
// based on the runConfig
|
||||||
|
func (j *Job) shouldRun() bool { |
||||||
|
j.mu.RLock() |
||||||
|
defer j.mu.RUnlock() |
||||||
|
return !j.runConfig.finiteRuns || j.runCount < j.runConfig.maxRuns |
||||||
|
} |
||||||
|
|
||||||
|
// LastRun returns the time the job was run last
|
||||||
|
func (j *Job) LastRun() time.Time { |
||||||
|
return j.lastRun |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setLastRun(t time.Time) { |
||||||
|
j.lastRun = t |
||||||
|
} |
||||||
|
|
||||||
|
// NextRun returns the time the job will run next
|
||||||
|
func (j *Job) NextRun() time.Time { |
||||||
|
j.mu.RLock() |
||||||
|
defer j.mu.RUnlock() |
||||||
|
return j.nextRun |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) setNextRun(t time.Time) { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
j.nextRun = t |
||||||
|
} |
||||||
|
|
||||||
|
// RunCount returns the number of time the job ran so far
|
||||||
|
func (j *Job) RunCount() int { |
||||||
|
return j.runCount |
||||||
|
} |
||||||
|
|
||||||
|
func (j *Job) stop() { |
||||||
|
j.mu.Lock() |
||||||
|
defer j.mu.Unlock() |
||||||
|
if j.timer != nil { |
||||||
|
j.timer.Stop() |
||||||
|
} |
||||||
|
if j.cancel != nil { |
||||||
|
j.cancel() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsRunning reports whether any instances of the job function are currently running
|
||||||
|
func (j *Job) IsRunning() bool { |
||||||
|
return atomic.LoadInt64(j.runState) != 0 |
||||||
|
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@ |
|||||||
|
package gocron |
||||||
|
|
||||||
|
import "time" |
||||||
|
|
||||||
|
var _ timeWrapper = (*trueTime)(nil) |
||||||
|
|
||||||
|
type timeWrapper interface { |
||||||
|
Now(*time.Location) time.Time |
||||||
|
Unix(int64, int64) time.Time |
||||||
|
Sleep(time.Duration) |
||||||
|
} |
||||||
|
|
||||||
|
type trueTime struct{} |
||||||
|
|
||||||
|
func (t *trueTime) Now(location *time.Location) time.Time { |
||||||
|
return time.Now().In(location) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *trueTime) Unix(sec int64, nsec int64) time.Time { |
||||||
|
return time.Unix(sec, nsec) |
||||||
|
} |
||||||
|
|
||||||
|
func (t *trueTime) Sleep(d time.Duration) { |
||||||
|
time.Sleep(d) |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
ignore: |
||||||
|
- cmd/**/*.go |
||||||
|
coverage: |
||||||
|
status: |
||||||
|
patch: false |
||||||
|
project: |
||||||
|
default: |
||||||
|
threshold: 0.5% |
@ -0,0 +1,25 @@ |
|||||||
|
# http://editorconfig.org/ |
||||||
|
|
||||||
|
root = true |
||||||
|
|
||||||
|
[*] |
||||||
|
charset = utf-8 |
||||||
|
insert_final_newline = true |
||||||
|
trim_trailing_whitespace = true |
||||||
|
end_of_line = lf |
||||||
|
|
||||||
|
[{*.go, go.mod}] |
||||||
|
indent_style = tab |
||||||
|
indent_size = 4 |
||||||
|
|
||||||
|
[{*.yml,*.yaml}] |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.py] |
||||||
|
indent_style = space |
||||||
|
indent_size = 4 |
||||||
|
|
||||||
|
# Makefiles always use tabs for indentation |
||||||
|
[Makefile] |
||||||
|
indent_style = tab |
@ -0,0 +1,8 @@ |
|||||||
|
.idea |
||||||
|
_bin/* |
||||||
|
./examples |
||||||
|
|
||||||
|
*-fuzz.zip |
||||||
|
|
||||||
|
*.out |
||||||
|
*.dump |
@ -0,0 +1,108 @@ |
|||||||
|
linters-settings: |
||||||
|
govet: |
||||||
|
check-shadowing: true |
||||||
|
gocyclo: |
||||||
|
min-complexity: 15 |
||||||
|
maligned: |
||||||
|
suggest-new: true |
||||||
|
dupl: |
||||||
|
threshold: 120 |
||||||
|
goconst: |
||||||
|
min-len: 2 |
||||||
|
min-occurrences: 3 |
||||||
|
misspell: |
||||||
|
locale: US |
||||||
|
lll: |
||||||
|
line-length: 140 |
||||||
|
goimports: |
||||||
|
local-prefixes: github.com/ogen/ |
||||||
|
gocritic: |
||||||
|
enabled-tags: |
||||||
|
- diagnostic |
||||||
|
- experimental |
||||||
|
- opinionated |
||||||
|
- performance |
||||||
|
- style |
||||||
|
disabled-checks: |
||||||
|
- hugeParam |
||||||
|
- rangeValCopy |
||||||
|
- exitAfterDefer |
||||||
|
- whyNoLint |
||||||
|
- singleCaseSwitch |
||||||
|
- commentedOutCode |
||||||
|
- appendAssign |
||||||
|
- unnecessaryBlock |
||||||
|
- redundantSprint |
||||||
|
|
||||||
|
linters: |
||||||
|
disable-all: true |
||||||
|
enable: |
||||||
|
- deadcode |
||||||
|
- depguard |
||||||
|
- dogsled |
||||||
|
- errcheck |
||||||
|
- goconst |
||||||
|
- gocritic |
||||||
|
- gofmt |
||||||
|
- goimports |
||||||
|
- revive |
||||||
|
- gosec |
||||||
|
- gosimple |
||||||
|
- govet |
||||||
|
- ineffassign |
||||||
|
- misspell |
||||||
|
- nakedret |
||||||
|
- staticcheck |
||||||
|
- structcheck |
||||||
|
- stylecheck |
||||||
|
- typecheck |
||||||
|
- unconvert |
||||||
|
- unparam |
||||||
|
- unused |
||||||
|
- varcheck |
||||||
|
- whitespace |
||||||
|
|
||||||
|
# Do not enable: |
||||||
|
# - wsl (too opinionated about newlines) |
||||||
|
# - godox (todos are OK) |
||||||
|
# - bodyclose (false positives on helper functions) |
||||||
|
# - prealloc (not worth it in scope of this project) |
||||||
|
# - maligned (same as prealloc) |
||||||
|
# - funlen (gocyclo is enough) |
||||||
|
# - gochecknoglobals (we know when it is ok to use globals) |
||||||
|
|
||||||
|
issues: |
||||||
|
exclude-use-default: false |
||||||
|
exclude-rules: |
||||||
|
# Disable linters that are annoying in tests. |
||||||
|
- path: _test\.go |
||||||
|
linters: |
||||||
|
- gocyclo |
||||||
|
- errcheck |
||||||
|
- dupl |
||||||
|
- gosec |
||||||
|
- funlen |
||||||
|
- goconst |
||||||
|
- gocognit |
||||||
|
- scopelint |
||||||
|
- lll |
||||||
|
|
||||||
|
- path: _test\.go |
||||||
|
text: "Combine" |
||||||
|
linters: [gocritic] |
||||||
|
|
||||||
|
# Ignore shadowing of err. |
||||||
|
- linters: [ govet ] |
||||||
|
text: 'declaration of "(err|ctx|log|c)"' |
||||||
|
|
||||||
|
# Ignore linters in main packages. |
||||||
|
- path: main\.go |
||||||
|
linters: [ goconst, funlen, gocognit, gocyclo ] |
||||||
|
|
||||||
|
- path: _test\.go |
||||||
|
text: "suspicious identical" |
||||||
|
linters: [gocritic] |
||||||
|
|
||||||
|
- path: _test\.go |
||||||
|
text: "identical expressions" |
||||||
|
linters: [staticcheck] |
@ -0,0 +1,27 @@ |
|||||||
|
Copyright (c) 2009 The Go 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,10 @@ |
|||||||
|
test: |
||||||
|
@./go.test.sh |
||||||
|
.PHONY: test |
||||||
|
|
||||||
|
coverage: |
||||||
|
@./go.coverage.sh |
||||||
|
.PHONY: coverage |
||||||
|
|
||||||
|
tidy: |
||||||
|
go mod tidy |
@ -0,0 +1,22 @@ |
|||||||
|
Additional IP Rights Grant (Patents) |
||||||
|
|
||||||
|
"This implementation" means the copyrightable works distributed by |
||||||
|
Google as part of the Go project. |
||||||
|
|
||||||
|
Google hereby grants to You a perpetual, worldwide, non-exclusive, |
||||||
|
no-charge, royalty-free, irrevocable (except as stated in this section) |
||||||
|
patent license to make, have made, use, offer to sell, sell, import, |
||||||
|
transfer and otherwise run, modify and propagate the contents of this |
||||||
|
implementation of Go, where such license applies only to those patent |
||||||
|
claims, both currently owned or controlled by Google and acquired in |
||||||
|
the future, licensable by Google that are necessarily infringed by this |
||||||
|
implementation of Go. This grant does not include claims that would be |
||||||
|
infringed only as a consequence of further modification of this |
||||||
|
implementation. If you or your agent or exclusive licensee institute or |
||||||
|
order or agree to the institution of patent litigation against any |
||||||
|
entity (including a cross-claim or counterclaim in a lawsuit) alleging |
||||||
|
that this implementation of Go or any code incorporated within this |
||||||
|
implementation of Go constitutes direct or contributory patent |
||||||
|
infringement, or inducement of patent infringement, then any patent |
||||||
|
rights granted to you under this License for this implementation of Go |
||||||
|
shall terminate as of the date such litigation is filed. |
@ -0,0 +1,36 @@ |
|||||||
|
# errors [![Go Reference](https://img.shields.io/badge/go-pkg-00ADD8)](https://pkg.go.dev/github.com/go-faster/errors#section-documentation) [![codecov](https://img.shields.io/codecov/c/github/go-faster/errors?label=cover)](https://codecov.io/gh/go-faster/errors) |
||||||
|
|
||||||
|
Fork of [xerrors](https://pkg.go.dev/golang.org/x/xerrors) with explicit [Wrap](https://pkg.go.dev/github.com/go-faster/errors#Wrap) instead of `%w`. |
||||||
|
|
||||||
|
> Clear is better than clever. |
||||||
|
|
||||||
|
``` |
||||||
|
go get github.com/go-faster/errors |
||||||
|
``` |
||||||
|
|
||||||
|
```go |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "something went wrong") |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Why |
||||||
|
* Using `Wrap` is the most explicit way to wrap errors |
||||||
|
* Wrapping with `fmt.Errorf("foo: %w", err)` is implicit, redundant and error-prone |
||||||
|
* Parsing `"foo: %w"` is implicit, redundant and slow |
||||||
|
* The [pkg/errors](https://github.com/pkg/errors) and [xerrrors](https://pkg.go.dev/golang.org/x/xerrors) are not maintainted |
||||||
|
* The [cockroachdb/errors](https://github.com/cockroachdb/errors) is too big |
||||||
|
* The `errors` has no caller stack trace |
||||||
|
|
||||||
|
## Don't need traces? |
||||||
|
Call `errors.DisableTrace` or use build tag `noerrtrace`. |
||||||
|
|
||||||
|
## Migration |
||||||
|
``` |
||||||
|
go get github.com/go-faster/errors/cmd/gowrapper@latest |
||||||
|
gowrapper ./... |
||||||
|
``` |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
BSD-3-Clause, same as Go sources |
@ -0,0 +1,193 @@ |
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"reflect" |
||||||
|
"strconv" |
||||||
|
) |
||||||
|
|
||||||
|
// FormatError calls the FormatError method of f with an errors.Printer
|
||||||
|
// configured according to s and verb, and writes the result to s.
|
||||||
|
func FormatError(f Formatter, s fmt.State, verb rune) { |
||||||
|
// Assuming this function is only called from the Format method, and given
|
||||||
|
// that FormatError takes precedence over Format, it cannot be called from
|
||||||
|
// any package that supports errors.Formatter. It is therefore safe to
|
||||||
|
// disregard that State may be a specific printer implementation and use one
|
||||||
|
// of our choice instead.
|
||||||
|
|
||||||
|
// limitations: does not support printing error as Go struct.
|
||||||
|
|
||||||
|
var ( |
||||||
|
sep = " " // separator before next error
|
||||||
|
p = &state{State: s} |
||||||
|
direct = true |
||||||
|
) |
||||||
|
|
||||||
|
var err error = f |
||||||
|
|
||||||
|
switch verb { |
||||||
|
// Note that this switch must match the preference order
|
||||||
|
// for ordinary string printing (%#v before %+v, and so on).
|
||||||
|
|
||||||
|
case 'v': |
||||||
|
if s.Flag('#') { |
||||||
|
if stringer, ok := err.(fmt.GoStringer); ok { |
||||||
|
p.buf.WriteString(stringer.GoString()) |
||||||
|
goto exit |
||||||
|
} |
||||||
|
// proceed as if it were %v
|
||||||
|
} else if s.Flag('+') { |
||||||
|
p.printDetail = true |
||||||
|
sep = "\n - " |
||||||
|
} |
||||||
|
case 's': |
||||||
|
case 'q', 'x', 'X': |
||||||
|
// Use an intermediate buffer in the rare cases that precision,
|
||||||
|
// truncation, or one of the alternative verbs (q, x, and X) are
|
||||||
|
// specified.
|
||||||
|
direct = false |
||||||
|
|
||||||
|
default: |
||||||
|
p.buf.WriteString("%!") |
||||||
|
p.buf.WriteRune(verb) |
||||||
|
p.buf.WriteByte('(') |
||||||
|
switch { |
||||||
|
case err != nil: |
||||||
|
p.buf.WriteString(reflect.TypeOf(f).String()) |
||||||
|
default: |
||||||
|
p.buf.WriteString("<nil>") |
||||||
|
} |
||||||
|
p.buf.WriteByte(')') |
||||||
|
_, _ = io.Copy(s, &p.buf) |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
loop: |
||||||
|
for { |
||||||
|
switch v := err.(type) { |
||||||
|
case Formatter: |
||||||
|
err = v.FormatError((*printer)(p)) |
||||||
|
case fmt.Formatter: |
||||||
|
v.Format(p, 'v') |
||||||
|
break loop |
||||||
|
default: |
||||||
|
_, _ = p.buf.WriteString(v.Error()) |
||||||
|
break loop |
||||||
|
} |
||||||
|
if err == nil { |
||||||
|
break |
||||||
|
} |
||||||
|
if p.needColon || !p.printDetail { |
||||||
|
p.buf.WriteByte(':') |
||||||
|
p.needColon = false |
||||||
|
} |
||||||
|
p.buf.WriteString(sep) |
||||||
|
p.inDetail = false |
||||||
|
p.needNewline = false |
||||||
|
} |
||||||
|
|
||||||
|
exit: |
||||||
|
width, okW := s.Width() |
||||||
|
prec, okP := s.Precision() |
||||||
|
|
||||||
|
if !direct || (okW && width > 0) || okP { |
||||||
|
// Construct format string from State s.
|
||||||
|
format := []byte{'%'} |
||||||
|
if s.Flag('-') { |
||||||
|
format = append(format, '-') |
||||||
|
} |
||||||
|
if s.Flag('+') { |
||||||
|
format = append(format, '+') |
||||||
|
} |
||||||
|
if s.Flag(' ') { |
||||||
|
format = append(format, ' ') |
||||||
|
} |
||||||
|
if okW { |
||||||
|
format = strconv.AppendInt(format, int64(width), 10) |
||||||
|
} |
||||||
|
if okP { |
||||||
|
format = append(format, '.') |
||||||
|
format = strconv.AppendInt(format, int64(prec), 10) |
||||||
|
} |
||||||
|
format = append(format, string(verb)...) |
||||||
|
_, _ = fmt.Fprintf(s, string(format), p.buf.String()) |
||||||
|
} else { |
||||||
|
_, _ = io.Copy(s, &p.buf) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var detailSep = []byte("\n ") |
||||||
|
|
||||||
|
// state tracks error printing state. It implements fmt.State.
|
||||||
|
type state struct { |
||||||
|
fmt.State |
||||||
|
buf bytes.Buffer |
||||||
|
|
||||||
|
printDetail bool |
||||||
|
inDetail bool |
||||||
|
needColon bool |
||||||
|
needNewline bool |
||||||
|
} |
||||||
|
|
||||||
|
func (s *state) Write(b []byte) (n int, err error) { |
||||||
|
if s.printDetail { |
||||||
|
if len(b) == 0 { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
if s.inDetail && s.needColon { |
||||||
|
s.needNewline = true |
||||||
|
if b[0] == '\n' { |
||||||
|
b = b[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
k := 0 |
||||||
|
for i, c := range b { |
||||||
|
if s.needNewline { |
||||||
|
if s.inDetail && s.needColon { |
||||||
|
s.buf.WriteByte(':') |
||||||
|
s.needColon = false |
||||||
|
} |
||||||
|
s.buf.Write(detailSep) |
||||||
|
s.needNewline = false |
||||||
|
} |
||||||
|
if c == '\n' { |
||||||
|
s.buf.Write(b[k:i]) |
||||||
|
k = i + 1 |
||||||
|
s.needNewline = true |
||||||
|
} |
||||||
|
} |
||||||
|
s.buf.Write(b[k:]) |
||||||
|
if !s.inDetail { |
||||||
|
s.needColon = true |
||||||
|
} |
||||||
|
} else if !s.inDetail { |
||||||
|
s.buf.Write(b) |
||||||
|
} |
||||||
|
return len(b), nil |
||||||
|
} |
||||||
|
|
||||||
|
// printer wraps a state to implement an xerrors.Printer.
|
||||||
|
type printer state |
||||||
|
|
||||||
|
func (s *printer) Print(args ...interface{}) { |
||||||
|
if !s.inDetail || s.printDetail { |
||||||
|
_, _ = fmt.Fprint((*state)(s), args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *printer) Printf(format string, args ...interface{}) { |
||||||
|
if !s.inDetail || s.printDetail { |
||||||
|
_, _ = fmt.Fprintf((*state)(s), format, args...) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (s *printer) Detail() bool { |
||||||
|
s.inDetail = true |
||||||
|
return s.printDetail |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
// Copyright 2019 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package errors implements functions to manipulate errors.
|
||||||
|
//
|
||||||
|
// This package expands "errors" with stack traces and explicit error
|
||||||
|
// wrapping.
|
||||||
|
package errors |
@ -0,0 +1,37 @@ |
|||||||
|
// Copyright 2011 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
// errorString is a trivial implementation of error.
|
||||||
|
type errorString struct { |
||||||
|
s string |
||||||
|
frame Frame |
||||||
|
} |
||||||
|
|
||||||
|
// New returns an error that formats as the given text.
|
||||||
|
//
|
||||||
|
// The returned error contains a Frame set to the caller's location and
|
||||||
|
// implements Formatter to show this information when printed with details.
|
||||||
|
func New(text string) error { |
||||||
|
if !Trace() { |
||||||
|
return errors.New(text) |
||||||
|
} |
||||||
|
return &errorString{text, Caller(1)} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *errorString) Error() string { return e.s } |
||||||
|
|
||||||
|
func (e *errorString) Format(s fmt.State, v rune) { FormatError(e, s, v) } |
||||||
|
|
||||||
|
func (e *errorString) FormatError(p Printer) (next error) { |
||||||
|
p.Print(e.s) |
||||||
|
e.frame.Format(p) |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,47 @@ |
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// A Formatter formats error messages.
|
||||||
|
type Formatter interface { |
||||||
|
error |
||||||
|
|
||||||
|
// FormatError prints the receiver's first error and returns the next error in
|
||||||
|
// the error chain, if any.
|
||||||
|
FormatError(p Printer) (next error) |
||||||
|
} |
||||||
|
|
||||||
|
// A Printer formats error messages.
|
||||||
|
//
|
||||||
|
// The most common implementation of Printer is the one provided by package fmt
|
||||||
|
// during Printf (as of Go 1.13). Localization packages such as golang.org/x/text/message
|
||||||
|
// typically provide their own implementations.
|
||||||
|
type Printer interface { |
||||||
|
// Print appends args to the message output.
|
||||||
|
Print(args ...interface{}) |
||||||
|
|
||||||
|
// Printf writes a formatted string.
|
||||||
|
Printf(format string, args ...interface{}) |
||||||
|
|
||||||
|
// Detail reports whether error detail is requested.
|
||||||
|
// After the first call to Detail, all text written to the Printer
|
||||||
|
// is formatted as additional detail, or ignored when
|
||||||
|
// detail has not been requested.
|
||||||
|
// If Detail returns false, the caller can avoid printing the detail at all.
|
||||||
|
Detail() bool |
||||||
|
} |
||||||
|
|
||||||
|
// Errorf creates new error with format.
|
||||||
|
func Errorf(format string, a ...interface{}) error { |
||||||
|
if !Trace() || strings.Contains(format, "%w") { |
||||||
|
return fmt.Errorf(format, a...) |
||||||
|
} |
||||||
|
return &errorString{fmt.Sprintf(format, a...), Caller(1)} |
||||||
|
} |
@ -0,0 +1,56 @@ |
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"runtime" |
||||||
|
) |
||||||
|
|
||||||
|
// A Frame contains part of a call stack.
|
||||||
|
type Frame struct { |
||||||
|
// Make room for three PCs: the one we were asked for, what it called,
|
||||||
|
// and possibly a PC for skipPleaseUseCallersFrames. See:
|
||||||
|
// https://go.googlesource.com/go/+/032678e0fb/src/runtime/extern.go#169
|
||||||
|
frames [3]uintptr |
||||||
|
} |
||||||
|
|
||||||
|
// Caller returns a Frame that describes a frame on the caller's stack.
|
||||||
|
// The argument skip is the number of frames to skip over.
|
||||||
|
// Caller(0) returns the frame for the caller of Caller.
|
||||||
|
func Caller(skip int) Frame { |
||||||
|
var s Frame |
||||||
|
runtime.Callers(skip+1, s.frames[:]) |
||||||
|
return s |
||||||
|
} |
||||||
|
|
||||||
|
// location reports the file, line, and function of a frame.
|
||||||
|
//
|
||||||
|
// The returned function may be "" even if file and line are not.
|
||||||
|
func (f Frame) location() (function, file string, line int) { |
||||||
|
frames := runtime.CallersFrames(f.frames[:]) |
||||||
|
if _, ok := frames.Next(); !ok { |
||||||
|
return "", "", 0 |
||||||
|
} |
||||||
|
fr, ok := frames.Next() |
||||||
|
if !ok { |
||||||
|
return "", "", 0 |
||||||
|
} |
||||||
|
return fr.Function, fr.File, fr.Line |
||||||
|
} |
||||||
|
|
||||||
|
// Format prints the stack as error detail.
|
||||||
|
// It should be called from an error's Format implementation
|
||||||
|
// after printing any other error detail.
|
||||||
|
func (f Frame) Format(p Printer) { |
||||||
|
if p.Detail() { |
||||||
|
function, file, line := f.location() |
||||||
|
if function != "" { |
||||||
|
p.Printf("%s\n ", function) |
||||||
|
} |
||||||
|
if file != "" { |
||||||
|
p.Printf("%s:%d\n", file, line) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
go test -v -coverpkg=./... -coverprofile=profile.out ./... |
||||||
|
go tool cover -func profile.out |
@ -0,0 +1,12 @@ |
|||||||
|
#!/usr/bin/env bash |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
# test with -race |
||||||
|
echo "with race:" |
||||||
|
go test --timeout 5m -race ./... |
||||||
|
|
||||||
|
# test with noerrtrace build tag |
||||||
|
tag=noerrtrace |
||||||
|
echo "with ${tag} build tag:" |
||||||
|
go test -tags "${tag}" --timeout 5m -race ./... |
@ -0,0 +1,13 @@ |
|||||||
|
//go:build noerrtrace
|
||||||
|
// +build noerrtrace
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
// enableTrace does nothing.
|
||||||
|
func enableTrace() {} |
||||||
|
|
||||||
|
// DisableTrace does nothing.
|
||||||
|
func DisableTrace() {} |
||||||
|
|
||||||
|
// Trace always returns false.
|
||||||
|
func Trace() bool { return false } |
@ -0,0 +1,37 @@ |
|||||||
|
//go:build !noerrtrace
|
||||||
|
// +build !noerrtrace
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"sync/atomic" |
||||||
|
) |
||||||
|
|
||||||
|
var traceFlag int64 |
||||||
|
|
||||||
|
const ( |
||||||
|
traceEnabled = 0 // enabled by default
|
||||||
|
traceDisabled = 1 |
||||||
|
) |
||||||
|
|
||||||
|
// setTrace sets tracing flag that controls capturing caller frames.
|
||||||
|
func setTrace(trace bool) { |
||||||
|
if trace { |
||||||
|
atomic.StoreInt64(&traceFlag, traceEnabled) |
||||||
|
} else { |
||||||
|
atomic.StoreInt64(&traceFlag, traceDisabled) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// enableTrace enables capturing caller frames.
|
||||||
|
//
|
||||||
|
// Intentionally left unexported.
|
||||||
|
func enableTrace() { setTrace(true) } |
||||||
|
|
||||||
|
// DisableTrace disables capturing caller frames.
|
||||||
|
func DisableTrace() { setTrace(false) } |
||||||
|
|
||||||
|
// Trace reports whether caller stack capture is enabled.
|
||||||
|
func Trace() bool { |
||||||
|
return atomic.LoadInt64(&traceFlag) == traceEnabled |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
// Copyright 2018 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package errors |
||||||
|
|
||||||
|
import ( |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
// A Wrapper provides context around another error.
|
||||||
|
type Wrapper interface { |
||||||
|
// Unwrap returns the next error in the error chain.
|
||||||
|
// If there is no next error, Unwrap returns nil.
|
||||||
|
Unwrap() error |
||||||
|
} |
||||||
|
|
||||||
|
// Opaque returns an error with the same error formatting as err
|
||||||
|
// but that does not match err and cannot be unwrapped.
|
||||||
|
func Opaque(err error) error { |
||||||
|
return noWrapper{err} |
||||||
|
} |
||||||
|
|
||||||
|
type noWrapper struct { |
||||||
|
error |
||||||
|
} |
||||||
|
|
||||||
|
func (e noWrapper) FormatError(p Printer) (next error) { |
||||||
|
if f, ok := e.error.(Formatter); ok { |
||||||
|
return f.FormatError(p) |
||||||
|
} |
||||||
|
p.Print(e.error) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Unwrap returns the result of calling the Unwrap method on err, if err's
|
||||||
|
// type contains an Unwrap method returning error.
|
||||||
|
// Otherwise, Unwrap returns nil.
|
||||||
|
func Unwrap(err error) error { |
||||||
|
return errors.Unwrap(err) |
||||||
|
} |
||||||
|
|
||||||
|
type wrapError struct { |
||||||
|
msg string |
||||||
|
err error |
||||||
|
frame Frame |
||||||
|
} |
||||||
|
|
||||||
|
func (e *wrapError) Error() string { |
||||||
|
return fmt.Sprint(e) |
||||||
|
} |
||||||
|
|
||||||
|
func (e *wrapError) Format(s fmt.State, v rune) { FormatError(e, s, v) } |
||||||
|
|
||||||
|
func (e *wrapError) FormatError(p Printer) (next error) { |
||||||
|
p.Print(e.msg) |
||||||
|
e.frame.Format(p) |
||||||
|
return e.err |
||||||
|
} |
||||||
|
|
||||||
|
func (e *wrapError) Unwrap() error { |
||||||
|
return e.err |
||||||
|
} |
||||||
|
|
||||||
|
// Wrap error with message and caller.
|
||||||
|
func Wrap(err error, message string) error { |
||||||
|
frame := Frame{} |
||||||
|
if Trace() { |
||||||
|
frame = Caller(1) |
||||||
|
} |
||||||
|
return &wrapError{msg: message, err: err, frame: frame} |
||||||
|
} |
||||||
|
|
||||||
|
// Wrapf wraps error with formatted message and caller.
|
||||||
|
func Wrapf(err error, format string, a ...interface{}) error { |
||||||
|
frame := Frame{} |
||||||
|
if Trace() { |
||||||
|
frame = Caller(1) |
||||||
|
} |
||||||
|
msg := fmt.Sprintf(format, a...) |
||||||
|
return &wrapError{msg: msg, err: err, frame: frame} |
||||||
|
} |
||||||
|
|
||||||
|
// Is reports whether any error in err's chain matches target.
|
||||||
|
//
|
||||||
|
// The chain consists of err itself followed by the sequence of errors obtained by
|
||||||
|
// repeatedly calling Unwrap.
|
||||||
|
//
|
||||||
|
// An error is considered to match a target if it is equal to that target or if
|
||||||
|
// it implements a method Is(error) bool such that Is(target) returns true.
|
||||||
|
//
|
||||||
|
// An error type might provide an Is method so it can be treated as equivalent
|
||||||
|
// to an existing error. For example, if MyError defines
|
||||||
|
//
|
||||||
|
// func (m MyError) Is(target error) bool { return target == fs.ErrExist }
|
||||||
|
//
|
||||||
|
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
|
||||||
|
// an example in the standard library.
|
||||||
|
func Is(err, target error) bool { |
||||||
|
return errors.Is(err, target) |
||||||
|
} |
||||||
|
|
||||||
|
// As finds the first error in err's chain that matches target, and if so, sets
|
||||||
|
// target to that error value and returns true. Otherwise, it returns false.
|
||||||
|
//
|
||||||
|
// The chain consists of err itself followed by the sequence of errors obtained by
|
||||||
|
// repeatedly calling Unwrap.
|
||||||
|
//
|
||||||
|
// An error matches target if the error's concrete value is assignable to the value
|
||||||
|
// pointed to by target, or if the error has a method As(interface{}) bool such that
|
||||||
|
// As(target) returns true. In the latter case, the As method is responsible for
|
||||||
|
// setting target.
|
||||||
|
//
|
||||||
|
// An error type might provide an As method so it can be treated as if it were a
|
||||||
|
// different error type.
|
||||||
|
//
|
||||||
|
// As panics if target is not a non-nil pointer to either a type that implements
|
||||||
|
// error, or to any interface type.
|
||||||
|
func As(err error, target interface{}) bool { return errors.As(err, target) } |
@ -0,0 +1,8 @@ |
|||||||
|
ignore: |
||||||
|
- tools/** |
||||||
|
coverage: |
||||||
|
status: |
||||||
|
patch: false |
||||||
|
project: |
||||||
|
default: |
||||||
|
threshold: 0.5% |
@ -0,0 +1,25 @@ |
|||||||
|
# http://editorconfig.org/ |
||||||
|
|
||||||
|
root = true |
||||||
|
|
||||||
|
[*] |
||||||
|
charset = utf-8 |
||||||
|
insert_final_newline = true |
||||||
|
trim_trailing_whitespace = true |
||||||
|
end_of_line = lf |
||||||
|
|
||||||
|
[{*.go, go.mod}] |
||||||
|
indent_style = tab |
||||||
|
indent_size = 4 |
||||||
|
|
||||||
|
[{*.yml,*.yaml}] |
||||||
|
indent_style = space |
||||||
|
indent_size = 2 |
||||||
|
|
||||||
|
[*.py] |
||||||
|
indent_style = space |
||||||
|
indent_size = 4 |
||||||
|
|
||||||
|
# Makefiles always use tabs for indentation |
||||||
|
[Makefile] |
||||||
|
indent_style = tab |
@ -0,0 +1,15 @@ |
|||||||
|
/vendor |
||||||
|
/bug_test.go |
||||||
|
/coverage.txt |
||||||
|
/.idea |
||||||
|
.idea |
||||||
|
_bin/* |
||||||
|
./examples |
||||||
|
|
||||||
|
*-fuzz.zip |
||||||
|
|
||||||
|
*.out |
||||||
|
*.dump |
||||||
|
*.test |
||||||
|
|
||||||
|
corpus |
@ -0,0 +1,108 @@ |
|||||||
|
linters-settings: |
||||||
|
govet: |
||||||
|
check-shadowing: true |
||||||
|
gocyclo: |
||||||
|
min-complexity: 15 |
||||||
|
maligned: |
||||||
|
suggest-new: true |
||||||
|
dupl: |
||||||
|
threshold: 120 |
||||||
|
goconst: |
||||||
|
min-len: 2 |
||||||
|
min-occurrences: 3 |
||||||
|
misspell: |
||||||
|
locale: US |
||||||
|
lll: |
||||||
|
line-length: 140 |
||||||
|
goimports: |
||||||
|
local-prefixes: github.com/ogen/ |
||||||
|
gocritic: |
||||||
|
enabled-tags: |
||||||
|
- diagnostic |
||||||
|
- experimental |
||||||
|
- opinionated |
||||||
|
- performance |
||||||
|
- style |
||||||
|
disabled-checks: |
||||||
|
- hugeParam |
||||||
|
- rangeValCopy |
||||||
|
- exitAfterDefer |
||||||
|
- whyNoLint |
||||||
|
- singleCaseSwitch |
||||||
|
- commentedOutCode |
||||||
|
- appendAssign |
||||||
|
- unnecessaryBlock |
||||||
|
- redundantSprint |
||||||
|
|
||||||
|
linters: |
||||||
|
disable-all: true |
||||||
|
enable: |
||||||
|
- deadcode |
||||||
|
- depguard |
||||||
|
- dogsled |
||||||
|
- errcheck |
||||||
|
- goconst |
||||||
|
- gocritic |
||||||
|
- gofmt |
||||||
|
- goimports |
||||||
|
- revive |
||||||
|
- gosec |
||||||
|
- gosimple |
||||||
|
- govet |
||||||
|
- ineffassign |
||||||
|
- misspell |
||||||
|
- nakedret |
||||||
|
- staticcheck |
||||||
|
- structcheck |
||||||
|
- stylecheck |
||||||
|
- typecheck |
||||||
|
- unconvert |
||||||
|
- unparam |
||||||
|
- unused |
||||||
|
- varcheck |
||||||
|
- whitespace |
||||||
|
|
||||||
|
# Do not enable: |
||||||
|
# - wsl (too opinionated about newlines) |
||||||
|
# - godox (todos are OK) |
||||||
|
# - bodyclose (false positives on helper functions) |
||||||
|
# - prealloc (not worth it in scope of this project) |
||||||
|
# - maligned (same as prealloc) |
||||||
|
# - funlen (gocyclo is enough) |
||||||
|
# - gochecknoglobals (we know when it is ok to use globals) |
||||||
|
|
||||||
|
issues: |
||||||
|
exclude-use-default: false |
||||||
|
exclude-rules: |
||||||
|
# Disable linters that are annoying in tests. |
||||||
|
- path: _test\.go |
||||||
|
linters: |
||||||
|
- gocyclo |
||||||
|
- errcheck |
||||||
|
- dupl |
||||||
|
- gosec |
||||||
|
- funlen |
||||||
|
- goconst |
||||||
|
- gocognit |
||||||
|
- scopelint |
||||||
|
- lll |
||||||
|
|
||||||
|
- path: _test\.go |
||||||
|
text: "Combine" |
||||||
|
linters: |
||||||
|
- gocritic |
||||||
|
|
||||||
|
# Check that equal to self is true |
||||||
|
- linters: [gocritic] |
||||||
|
source: '(assert|require).+Equal' |
||||||
|
text: 'dupArg' |
||||||
|
path: _test\.go |
||||||
|
|
||||||
|
# Ignore shadowing of err. |
||||||
|
- linters: [ govet ] |
||||||
|
text: 'declaration of "(err|ctx|log|c)"' |
||||||
|
|
||||||
|
# Ignore linters in main packages. |
||||||
|
- path: main\.go |
||||||
|
linters: [ goconst, funlen, gocognit, gocyclo ] |
||||||
|
|
@ -0,0 +1,21 @@ |
|||||||
|
MIT License |
||||||
|
|
||||||
|
Copyright (c) 2016 json-iterator |
||||||
|
|
||||||
|
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,13 @@ |
|||||||
|
test: |
||||||
|
@./go.test.sh |
||||||
|
.PHONY: test |
||||||
|
|
||||||
|
coverage: |
||||||
|
@./go.coverage.sh |
||||||
|
.PHONY: coverage |
||||||
|
|
||||||
|
test_fast: |
||||||
|
go test ./... |
||||||
|
|
||||||
|
tidy: |
||||||
|
go mod tidy |
@ -0,0 +1,306 @@ |
|||||||
|
# jx [![](https://img.shields.io/badge/go-pkg-00ADD8)](https://pkg.go.dev/github.com/go-faster/jx#section-documentation) [![](https://img.shields.io/codecov/c/github/go-faster/jx?label=cover)](https://codecov.io/gh/go-faster/jx) [![experimental](https://img.shields.io/badge/-experimental-blueviolet)](https://go-faster.org/docs/projects/status#experimental) |
||||||
|
|
||||||
|
Package jx implements encoding and decoding of json [[RFC 7159](https://www.rfc-editor.org/rfc/rfc7159.html)]. |
||||||
|
Lightweight fork of [jsoniter](https://github.com/json-iterator/go). |
||||||
|
|
||||||
|
```console |
||||||
|
go get github.com/go-faster/jx |
||||||
|
``` |
||||||
|
|
||||||
|
* [Usage and examples](#usage) |
||||||
|
* [Roadmap](#roadmap) |
||||||
|
* [Non-goals](#non-goals) |
||||||
|
|
||||||
|
## Features |
||||||
|
* Directly encode and decode json values |
||||||
|
* No reflect or `interface{}` |
||||||
|
* Pools and direct buffer access for less (or none) allocations |
||||||
|
* Multi-pass decoding |
||||||
|
* Validation |
||||||
|
|
||||||
|
See [usage](#Usage) for examples. Mostly suitable for fast low-level json manipulation |
||||||
|
with high control. Used in [ogen](https://github.com/ogen-go/ogen) project for |
||||||
|
json (un)marshaling code generation based on json and OpenAPI schemas. |
||||||
|
|
||||||
|
For example, we have following OpenTelemetry log entry: |
||||||
|
|
||||||
|
```json |
||||||
|
{ |
||||||
|
"Timestamp": "1586960586000000000", |
||||||
|
"Attributes": { |
||||||
|
"http.status_code": 500, |
||||||
|
"http.url": "http://example.com", |
||||||
|
"my.custom.application.tag": "hello" |
||||||
|
}, |
||||||
|
"Resource": { |
||||||
|
"service.name": "donut_shop", |
||||||
|
"service.version": "2.0.0", |
||||||
|
"k8s.pod.uid": "1138528c-c36e-11e9-a1a7-42010a800198" |
||||||
|
}, |
||||||
|
"TraceId": "13e2a0921288b3ff80df0a0482d4fc46", |
||||||
|
"SpanId": "43222c2d51a7abe3", |
||||||
|
"SeverityText": "INFO", |
||||||
|
"SeverityNumber": 9, |
||||||
|
"Body": "20200415T072306-0700 INFO I like donuts" |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
Flexibility of `jx` enables highly efficient semantic-aware encoding and decoding, |
||||||
|
e.g. using `[16]byte` for `TraceId` with zero-allocation `hex` encoding in json: |
||||||
|
|
||||||
|
| Name | Speed | Allocations | |
||||||
|
|----------|-----------|-------------| |
||||||
|
| Decode | 970 MB/s | 0 allocs/op | |
||||||
|
| Validate | 1535 MB/s | 0 allocs/op | |
||||||
|
| Encode | 1104 MB/s | 0 allocs/op | |
||||||
|
| Write | 2146 MB/s | 0 allocs/op | |
||||||
|
|
||||||
|
See [otel_test.go](./otel_test.go) for example. |
||||||
|
|
||||||
|
## Why |
||||||
|
|
||||||
|
Most of [jsoniter](https://github.com/json-iterator/go) issues are caused by necessity |
||||||
|
to be drop-in replacement for standard `encoding/json`. Removing such constrains greatly |
||||||
|
simplified implementation and reduced scope, allowing to focus on json stream processing. |
||||||
|
|
||||||
|
* Commas are handled automatically while encoding |
||||||
|
* Raw json, Number and Base64 support |
||||||
|
* Reduced scope |
||||||
|
* No reflection |
||||||
|
* No `encoding/json` adapter |
||||||
|
* 3.5x less code (8.5K to 2.4K SLOC) |
||||||
|
* Fuzzing, improved test coverage |
||||||
|
* Drastically refactored and simplified |
||||||
|
* Explicit error returns |
||||||
|
* No `Config` or `API` |
||||||
|
|
||||||
|
|
||||||
|
## Usage |
||||||
|
|
||||||
|
* [Decoding](#decode) |
||||||
|
* [Encoding](#encode) |
||||||
|
* [Writer](#writer) |
||||||
|
* [Raw message](#raw) |
||||||
|
* [Number](#number) |
||||||
|
* [Base64](#base64) |
||||||
|
* [Validation](#validate) |
||||||
|
* [Multi pass decoding](#capture) |
||||||
|
|
||||||
|
### Decode |
||||||
|
|
||||||
|
Use [jx.Decoder](https://pkg.go.dev/github.com/go-faster/jx#Decoder). Zero value is valid, |
||||||
|
but constructors are available for convenience: |
||||||
|
* [jx.Decode(reader io.Reader, bufSize int)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for `io.Reader` |
||||||
|
* [jx.DecodeBytes([]byte)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for byte slices |
||||||
|
* [jx.DecodeStr(string)](https://pkg.go.dev/github.com/go-faster/jx#Decode) for strings |
||||||
|
|
||||||
|
To reuse decoders and their buffers, use [jx.GetDecoder](https://pkg.go.dev/github.com/go-faster/jx#GetDecoder) |
||||||
|
and [jx.PutDecoder](https://pkg.go.dev/github.com/go-faster/jx#PutDecoder) alongside with reset functions: |
||||||
|
* [jx.Decoder.Reset(io.Reader)](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Reset) to reset to new `io.Reader` |
||||||
|
* [jx.Decoder.ResetBytes([]byte)](https://pkg.go.dev/github.com/go-faster/jx#Decoder.ResetBytes) to decode another byte slice |
||||||
|
|
||||||
|
Decoder is reset on `PutDecoder`. |
||||||
|
|
||||||
|
```go |
||||||
|
d := jx.DecodeStr(`{"values":[4,8,15,16,23,42]}`) |
||||||
|
|
||||||
|
// Save all integers from "values" array to slice. |
||||||
|
var values []int |
||||||
|
|
||||||
|
// Iterate over each object field. |
||||||
|
if err := d.Obj(func(d *jx.Decoder, key string) error { |
||||||
|
switch key { |
||||||
|
case "values": |
||||||
|
// Iterate over each array element. |
||||||
|
return d.Arr(func(d *jx.Decoder) error { |
||||||
|
v, err := d.Int() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
values = append(values, v) |
||||||
|
return nil |
||||||
|
}) |
||||||
|
default: |
||||||
|
// Skip unknown fields if any. |
||||||
|
return d.Skip() |
||||||
|
} |
||||||
|
}); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(values) |
||||||
|
// Output: [4 8 15 16 23 42] |
||||||
|
``` |
||||||
|
|
||||||
|
### Encode |
||||||
|
Use [jx.Encoder](https://pkg.go.dev/github.com/go-faster/jx#Encoder). Zero value is valid, reuse with |
||||||
|
[jx.GetEncoder](https://pkg.go.dev/github.com/go-faster/jx#GetEncoder), |
||||||
|
[jx.PutEncoder](https://pkg.go.dev/github.com/go-faster/jx#PutEncoder) and |
||||||
|
[jx.Encoder.Reset()](https://pkg.go.dev/github.com/go-faster/jx#Encoder.Reset). Encoder is reset on `PutEncoder`. |
||||||
|
```go |
||||||
|
var e jx.Encoder |
||||||
|
e.ObjStart() // { |
||||||
|
e.FieldStart("values") // "values": |
||||||
|
e.ArrStart() // [ |
||||||
|
for _, v := range []int{4, 8, 15, 16, 23, 42} { |
||||||
|
e.Int(v) |
||||||
|
} |
||||||
|
e.ArrEnd() // ] |
||||||
|
e.ObjEnd() // } |
||||||
|
fmt.Println(e) |
||||||
|
fmt.Println("Buffer len:", len(e.Bytes())) |
||||||
|
// Output: {"values":[4,8,15,16,23,42]} |
||||||
|
// Buffer len: 28 |
||||||
|
``` |
||||||
|
|
||||||
|
### Writer |
||||||
|
|
||||||
|
Use [jx.Writer](https://pkg.go.dev/github.com/go-faster/jx#Writer) for low level json writing. |
||||||
|
|
||||||
|
No automatic commas or indentation for lowest possible overhead, useful for code generated json encoding. |
||||||
|
|
||||||
|
### Raw |
||||||
|
Use [jx.Decoder.Raw](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Raw) to read raw json values, similar to `json.RawMessage`. |
||||||
|
```go |
||||||
|
d := jx.DecodeStr(`{"foo": [1, 2, 3]}`) |
||||||
|
|
||||||
|
var raw jx.Raw |
||||||
|
if err := d.Obj(func(d *jx.Decoder, key string) error { |
||||||
|
v, err := d.Raw() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
raw = v |
||||||
|
return nil |
||||||
|
}); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(raw.Type(), raw) |
||||||
|
// Output: |
||||||
|
// array [1, 2, 3] |
||||||
|
``` |
||||||
|
|
||||||
|
### Number |
||||||
|
|
||||||
|
Use [jx.Decoder.Num](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Num) to read numbers, similar to `json.Number`. |
||||||
|
Also supports number strings, like `"12345"`, which is common compatible way to represent `uint64`. |
||||||
|
|
||||||
|
```go |
||||||
|
d := jx.DecodeStr(`{"foo": "10531.0"}`) |
||||||
|
|
||||||
|
var n jx.Num |
||||||
|
if err := d.Obj(func(d *jx.Decoder, key string) error { |
||||||
|
v, err := d.Num() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
n = v |
||||||
|
return nil |
||||||
|
}); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
|
||||||
|
fmt.Println(n) |
||||||
|
fmt.Println("positive:", n.Positive()) |
||||||
|
|
||||||
|
// Can decode floats with zero fractional part as integers: |
||||||
|
v, err := n.Int64() |
||||||
|
if err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
fmt.Println("int64:", v) |
||||||
|
// Output: |
||||||
|
// "10531.0" |
||||||
|
// positive: true |
||||||
|
// int64: 10531 |
||||||
|
``` |
||||||
|
|
||||||
|
### Base64 |
||||||
|
Use [jx.Encoder.Base64](https://pkg.go.dev/github.com/go-faster/jx#Encoder.Base64) and |
||||||
|
[jx.Decoder.Base64](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Base64) or |
||||||
|
[jx.Decoder.Base64Append](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Base64Append). |
||||||
|
|
||||||
|
Same as encoding/json, base64.StdEncoding or [[RFC 4648](https://www.rfc-editor.org/rfc/rfc4648.html)]. |
||||||
|
```go |
||||||
|
var e jx.Encoder |
||||||
|
e.Base64([]byte("Hello")) |
||||||
|
fmt.Println(e) |
||||||
|
|
||||||
|
data, _ := jx.DecodeBytes(e.Bytes()).Base64() |
||||||
|
fmt.Printf("%s", data) |
||||||
|
// Output: |
||||||
|
// "SGVsbG8=" |
||||||
|
// Hello |
||||||
|
``` |
||||||
|
|
||||||
|
### Validate |
||||||
|
|
||||||
|
Check that byte slice is valid json with [jx.Valid](https://pkg.go.dev/github.com/go-faster/jx#Valid): |
||||||
|
|
||||||
|
```go |
||||||
|
fmt.Println(jx.Valid([]byte(`{"field": "value"}`))) // true |
||||||
|
fmt.Println(jx.Valid([]byte(`"Hello, world!"`))) // true |
||||||
|
fmt.Println(jx.Valid([]byte(`["foo"}`))) // false |
||||||
|
``` |
||||||
|
|
||||||
|
### Capture |
||||||
|
The [jx.Decoder.Capture](https://pkg.go.dev/github.com/go-faster/jx#Decoder.Capture) method allows to unread everything is read in callback. |
||||||
|
Useful for multi-pass parsing: |
||||||
|
```go |
||||||
|
d := jx.DecodeStr(`["foo", "bar", "baz"]`) |
||||||
|
var elems int |
||||||
|
// NB: Currently Capture does not support io.Reader, only buffers. |
||||||
|
if err := d.Capture(func(d *jx.Decoder) error { |
||||||
|
// Everything decoded in this callback will be rolled back. |
||||||
|
return d.Arr(func(d *jx.Decoder) error { |
||||||
|
elems++ |
||||||
|
return d.Skip() |
||||||
|
}) |
||||||
|
}); err != nil { |
||||||
|
panic(err) |
||||||
|
} |
||||||
|
// Decoder is rolled back to state before "Capture" call. |
||||||
|
fmt.Println("Read", elems, "elements on first pass") |
||||||
|
fmt.Println("Next element is", d.Next(), "again") |
||||||
|
|
||||||
|
// Output: |
||||||
|
// Read 3 elements on first pass |
||||||
|
// Next element is array again |
||||||
|
``` |
||||||
|
|
||||||
|
### ObjBytes |
||||||
|
|
||||||
|
The `Decoder.ObjBytes` method tries not to allocate memory for keys, reusing existing buffer. |
||||||
|
```go |
||||||
|
d := DecodeStr(`{"id":1,"randomNumber":10}`) |
||||||
|
d.ObjBytes(func(d *Decoder, key []byte) error { |
||||||
|
switch string(key) { |
||||||
|
case "id": |
||||||
|
case "randomNumber": |
||||||
|
} |
||||||
|
return d.Skip() |
||||||
|
}) |
||||||
|
``` |
||||||
|
|
||||||
|
## Roadmap |
||||||
|
- [ ] Rework and export `Any` |
||||||
|
- [ ] Support `Raw` for io.Reader |
||||||
|
- [x] Support `Capture` for io.Reader |
||||||
|
- [ ] Improve Num |
||||||
|
- Better validation on decoding |
||||||
|
- Support BigFloat and BigInt |
||||||
|
- Support equivalence check, like `eq(1.0, 1) == true` |
||||||
|
- [ ] Add non-callback decoding of objects |
||||||
|
|
||||||
|
## Non-goals |
||||||
|
* Code generation for decoding or encoding |
||||||
|
* Replacement for `encoding/json` |
||||||
|
* Reflection or `interface{}` based encoding or decoding |
||||||
|
* Support for json path or similar |
||||||
|
|
||||||
|
This package should be kept as simple as possible and be used as |
||||||
|
low-level foundation for high-level projects like code generator. |
||||||
|
|
||||||
|
## License |
||||||
|
MIT, same as jsoniter |
@ -0,0 +1,143 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
// Type of json value.
|
||||||
|
type Type int |
||||||
|
|
||||||
|
func (t Type) String() string { |
||||||
|
switch t { |
||||||
|
case Invalid: |
||||||
|
return "invalid" |
||||||
|
case String: |
||||||
|
return "string" |
||||||
|
case Number: |
||||||
|
return "number" |
||||||
|
case Null: |
||||||
|
return "null" |
||||||
|
case Bool: |
||||||
|
return "bool" |
||||||
|
case Array: |
||||||
|
return "array" |
||||||
|
case Object: |
||||||
|
return "object" |
||||||
|
default: |
||||||
|
return "unknown" |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
// Invalid json value.
|
||||||
|
Invalid Type = iota |
||||||
|
// String json value, like "foo".
|
||||||
|
String |
||||||
|
// Number json value, like 100 or 1.01.
|
||||||
|
Number |
||||||
|
// Null json value.
|
||||||
|
Null |
||||||
|
// Bool json value, true or false.
|
||||||
|
Bool |
||||||
|
// Array json value, like [1, 2, 3].
|
||||||
|
Array |
||||||
|
// Object json value, like {"foo": 1}.
|
||||||
|
Object |
||||||
|
) |
||||||
|
|
||||||
|
var types []Type |
||||||
|
|
||||||
|
func init() { |
||||||
|
types = make([]Type, 256) |
||||||
|
for i := range types { |
||||||
|
types[i] = Invalid |
||||||
|
} |
||||||
|
types['"'] = String |
||||||
|
types['-'] = Number |
||||||
|
types['0'] = Number |
||||||
|
types['1'] = Number |
||||||
|
types['2'] = Number |
||||||
|
types['3'] = Number |
||||||
|
types['4'] = Number |
||||||
|
types['5'] = Number |
||||||
|
types['6'] = Number |
||||||
|
types['7'] = Number |
||||||
|
types['8'] = Number |
||||||
|
types['9'] = Number |
||||||
|
types['t'] = Bool |
||||||
|
types['f'] = Bool |
||||||
|
types['n'] = Null |
||||||
|
types['['] = Array |
||||||
|
types['{'] = Object |
||||||
|
} |
||||||
|
|
||||||
|
// Decoder decodes json.
|
||||||
|
//
|
||||||
|
// Can decode from io.Reader or byte slice directly.
|
||||||
|
type Decoder struct { |
||||||
|
reader io.Reader |
||||||
|
|
||||||
|
// buf is current buffer.
|
||||||
|
//
|
||||||
|
// Contains full json if reader is nil or used as a read buffer
|
||||||
|
// otherwise.
|
||||||
|
buf []byte |
||||||
|
head int // offset in buf to start of current json stream
|
||||||
|
tail int // offset in buf to end of current json stream
|
||||||
|
|
||||||
|
depth int |
||||||
|
} |
||||||
|
|
||||||
|
const defaultBuf = 512 |
||||||
|
|
||||||
|
// Decode creates a Decoder that reads json from io.Reader.
|
||||||
|
func Decode(reader io.Reader, bufSize int) *Decoder { |
||||||
|
if bufSize <= 0 { |
||||||
|
bufSize = defaultBuf |
||||||
|
} |
||||||
|
return &Decoder{ |
||||||
|
reader: reader, |
||||||
|
buf: make([]byte, bufSize), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DecodeBytes creates a Decoder that reads json from byte slice.
|
||||||
|
func DecodeBytes(input []byte) *Decoder { |
||||||
|
return &Decoder{ |
||||||
|
buf: input, |
||||||
|
tail: len(input), |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// DecodeStr creates a Decoder that reads string as json.
|
||||||
|
func DecodeStr(input string) *Decoder { |
||||||
|
return DecodeBytes([]byte(input)) |
||||||
|
} |
||||||
|
|
||||||
|
// Reset resets reader and underlying state, next reads will use provided io.Reader.
|
||||||
|
func (d *Decoder) Reset(reader io.Reader) { |
||||||
|
d.reader = reader |
||||||
|
d.head = 0 |
||||||
|
d.tail = 0 |
||||||
|
d.depth = 0 |
||||||
|
|
||||||
|
// Reads from reader need buffer.
|
||||||
|
if cap(d.buf) == 0 { |
||||||
|
// Allocate new buffer if none.
|
||||||
|
d.buf = make([]byte, defaultBuf) |
||||||
|
} |
||||||
|
if len(d.buf) == 0 { |
||||||
|
// Set buffer to full capacity if needed.
|
||||||
|
d.buf = d.buf[:cap(d.buf)] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// ResetBytes resets underlying state, next reads will use provided buffer.
|
||||||
|
func (d *Decoder) ResetBytes(input []byte) { |
||||||
|
d.reader = nil |
||||||
|
d.head = 0 |
||||||
|
d.tail = len(input) |
||||||
|
d.depth = 0 |
||||||
|
|
||||||
|
d.buf = input |
||||||
|
} |
@ -0,0 +1,80 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Elem skips to the start of next array element, returning true boolean
|
||||||
|
// if element exists.
|
||||||
|
//
|
||||||
|
// Can be called before or in Array.
|
||||||
|
func (d *Decoder) Elem() (ok bool, err error) { |
||||||
|
c, err := d.next() |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case '[': |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return false, errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
if c != ']' { |
||||||
|
d.unread() |
||||||
|
return true, nil |
||||||
|
} |
||||||
|
return false, nil |
||||||
|
case ']': |
||||||
|
return false, nil |
||||||
|
case ',': |
||||||
|
return true, nil |
||||||
|
default: |
||||||
|
return false, errors.Wrap(badToken(c), `"[" or "," or "]" expected`) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Arr decodes array and invokes callback on each array element.
|
||||||
|
func (d *Decoder) Arr(f func(d *Decoder) error) error { |
||||||
|
if err := d.consume('['); err != nil { |
||||||
|
return errors.Wrap(err, "start") |
||||||
|
} |
||||||
|
if f == nil { |
||||||
|
return d.skipArr() |
||||||
|
} |
||||||
|
if err := d.incDepth(); err != nil { |
||||||
|
return errors.Wrap(err, "inc") |
||||||
|
} |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if c == ']' { |
||||||
|
return d.decDepth() |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
if err := f(d); err != nil { |
||||||
|
return errors.Wrap(err, "callback") |
||||||
|
} |
||||||
|
|
||||||
|
c, err = d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
for c == ',' { |
||||||
|
// Skip whitespace before reading element.
|
||||||
|
if _, err := d.next(); err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
if err := f(d); err != nil { |
||||||
|
return errors.Wrap(err, "callback") |
||||||
|
} |
||||||
|
if c, err = d.next(); err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
} |
||||||
|
if c != ']' { |
||||||
|
return errors.Wrap(badToken(c), "end") |
||||||
|
} |
||||||
|
return d.decDepth() |
||||||
|
} |
@ -0,0 +1,46 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-faster/errors" |
||||||
|
"github.com/segmentio/asm/base64" |
||||||
|
) |
||||||
|
|
||||||
|
// Base64 decodes base64 encoded data from string.
|
||||||
|
//
|
||||||
|
// Same as encoding/json, base64.StdEncoding or RFC 4648.
|
||||||
|
func (d *Decoder) Base64() ([]byte, error) { |
||||||
|
if d.Next() == Null { |
||||||
|
if err := d.Null(); err != nil { |
||||||
|
return nil, errors.Wrap(err, "read null") |
||||||
|
} |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
return d.Base64Append([]byte{}) |
||||||
|
} |
||||||
|
|
||||||
|
// Base64Append appends base64 encoded data from string.
|
||||||
|
//
|
||||||
|
// Same as encoding/json, base64.StdEncoding or RFC 4648.
|
||||||
|
func (d *Decoder) Base64Append(b []byte) ([]byte, error) { |
||||||
|
if d.Next() == Null { |
||||||
|
if err := d.Null(); err != nil { |
||||||
|
return nil, errors.Wrap(err, "read null") |
||||||
|
} |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
buf, err := d.StrBytes() |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "bytes") |
||||||
|
} |
||||||
|
|
||||||
|
decodedLen := base64.StdEncoding.DecodedLen(len(buf)) |
||||||
|
start := len(b) |
||||||
|
b = append(b, make([]byte, decodedLen)...) |
||||||
|
|
||||||
|
n, err := base64.StdEncoding.Decode(b[start:], buf) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "decode") |
||||||
|
} |
||||||
|
|
||||||
|
return b[:start+n], nil |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Bool reads a json object as Bool
|
||||||
|
func (d *Decoder) Bool() (bool, error) { |
||||||
|
if err := d.skipSpace(); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
var buf [4]byte |
||||||
|
if err := d.readExact4(&buf); err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
|
||||||
|
switch string(buf[:]) { |
||||||
|
case "true": |
||||||
|
return true, nil |
||||||
|
case "fals": |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return false, err |
||||||
|
} |
||||||
|
if c != 'e' { |
||||||
|
return false, badToken(c) |
||||||
|
} |
||||||
|
return false, nil |
||||||
|
default: |
||||||
|
switch c := buf[0]; c { |
||||||
|
case 't': |
||||||
|
const encodedTrue = 't' | 'r'<<8 | 'u'<<16 | 'e'<<24 |
||||||
|
return false, findInvalidToken4(buf, encodedTrue) |
||||||
|
case 'f': |
||||||
|
const encodedAlse = 'a' | 'l'<<8 | 's'<<16 | 'e'<<24 |
||||||
|
return false, findInvalidToken4(buf, encodedAlse) |
||||||
|
default: |
||||||
|
return false, badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"io" |
||||||
|
) |
||||||
|
|
||||||
|
// Capture calls f and then rolls back to state before call.
|
||||||
|
func (d *Decoder) Capture(f func(d *Decoder) error) error { |
||||||
|
if f == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
if d.reader != nil { |
||||||
|
// TODO(tdakkota): May it be more efficient?
|
||||||
|
var buf bytes.Buffer |
||||||
|
reader := io.TeeReader(d.reader, &buf) |
||||||
|
defer func() { |
||||||
|
d.reader = io.MultiReader(&buf, d.reader) |
||||||
|
}() |
||||||
|
d.reader = reader |
||||||
|
} |
||||||
|
head, tail, depth := d.head, d.tail, d.depth |
||||||
|
err := f(d) |
||||||
|
d.head, d.tail, d.depth = head, tail, depth |
||||||
|
return err |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import "github.com/go-faster/errors" |
||||||
|
|
||||||
|
// limit maximum depth of nesting, as allowed by https://tools.ietf.org/html/rfc7159#section-9
|
||||||
|
const maxDepth = 10000 |
||||||
|
|
||||||
|
var errMaxDepth = errors.New("depth: maximum") |
||||||
|
|
||||||
|
func (d *Decoder) incDepth() error { |
||||||
|
d.depth++ |
||||||
|
if d.depth > maxDepth { |
||||||
|
return errMaxDepth |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
var errNegativeDepth = errors.New("depth: negative") |
||||||
|
|
||||||
|
func (d *Decoder) decDepth() error { |
||||||
|
d.depth-- |
||||||
|
if d.depth < 0 { |
||||||
|
return errNegativeDepth |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,360 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"bytes" |
||||||
|
"io" |
||||||
|
"math/big" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
var pow10 = []uint64{1, 10, 100, 1000, 10000, 100000, 1000000} |
||||||
|
|
||||||
|
var floatDigits []int8 |
||||||
|
|
||||||
|
const invalidCharForNumber = int8(-1) |
||||||
|
const endOfNumber = int8(-2) |
||||||
|
const dotInNumber = int8(-3) |
||||||
|
const maxFloat64 = 1<<63 - 1 |
||||||
|
|
||||||
|
func init() { |
||||||
|
floatDigits = make([]int8, 256) |
||||||
|
for i := 0; i < len(floatDigits); i++ { |
||||||
|
floatDigits[i] = invalidCharForNumber |
||||||
|
} |
||||||
|
for i := int8('0'); i <= int8('9'); i++ { |
||||||
|
floatDigits[i] = i - int8('0') |
||||||
|
} |
||||||
|
floatDigits[','] = endOfNumber |
||||||
|
floatDigits[']'] = endOfNumber |
||||||
|
floatDigits['}'] = endOfNumber |
||||||
|
floatDigits[' '] = endOfNumber |
||||||
|
floatDigits['\t'] = endOfNumber |
||||||
|
floatDigits['\n'] = endOfNumber |
||||||
|
floatDigits['.'] = dotInNumber |
||||||
|
} |
||||||
|
|
||||||
|
// BigFloat read big.Float
|
||||||
|
func (d *Decoder) BigFloat() (*big.Float, error) { |
||||||
|
str, err := d.numberAppend(nil) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "number") |
||||||
|
} |
||||||
|
prec := 64 |
||||||
|
if len(str) > prec { |
||||||
|
prec = len(str) |
||||||
|
} |
||||||
|
val, _, err := big.ParseFloat(string(str), 10, uint(prec), big.ToZero) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "float") |
||||||
|
} |
||||||
|
return val, nil |
||||||
|
} |
||||||
|
|
||||||
|
// BigInt read big.Int
|
||||||
|
func (d *Decoder) BigInt() (*big.Int, error) { |
||||||
|
str, err := d.numberAppend(nil) |
||||||
|
if err != nil { |
||||||
|
return nil, errors.Wrap(err, "number") |
||||||
|
} |
||||||
|
v := big.NewInt(0) |
||||||
|
var ok bool |
||||||
|
if v, ok = v.SetString(string(str), 10); !ok { |
||||||
|
return nil, errors.New("invalid") |
||||||
|
} |
||||||
|
return v, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Float32 reads float32 value.
|
||||||
|
func (d *Decoder) Float32() (float32, error) { |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
if c != '-' { |
||||||
|
d.unread() |
||||||
|
} |
||||||
|
v, err := d.positiveFloat32() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if c == '-' { |
||||||
|
v *= -1 |
||||||
|
} |
||||||
|
return v, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) positiveFloat32() (float32, error) { |
||||||
|
i := d.head |
||||||
|
// First char.
|
||||||
|
if i == d.tail { |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
c := d.buf[i] |
||||||
|
i++ |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case invalidCharForNumber: |
||||||
|
return d.f32Slow() |
||||||
|
case endOfNumber: |
||||||
|
return 0, errors.New("empty") |
||||||
|
case dotInNumber: |
||||||
|
return 0, errors.New("leading dot") |
||||||
|
case 0: |
||||||
|
if i == d.tail { |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
c = d.buf[i] |
||||||
|
switch c { |
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
return 0, errors.New("leading zero") |
||||||
|
} |
||||||
|
} |
||||||
|
value := uint64(ind) |
||||||
|
// Chars before dot.
|
||||||
|
NonDecimalLoop: |
||||||
|
for ; i < d.tail; i++ { |
||||||
|
c = d.buf[i] |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case invalidCharForNumber: |
||||||
|
return d.f32Slow() |
||||||
|
case endOfNumber: |
||||||
|
d.head = i |
||||||
|
return float32(value), nil |
||||||
|
case dotInNumber: |
||||||
|
break NonDecimalLoop |
||||||
|
} |
||||||
|
if value > uint64SafeToMultiple10 { |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
value = (value << 3) + (value << 1) + uint64(ind) // value = value * 10 + ind;
|
||||||
|
} |
||||||
|
// Chars after dot.
|
||||||
|
if c == '.' { |
||||||
|
i++ |
||||||
|
decimalPlaces := 0 |
||||||
|
if i == d.tail { |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
for ; i < d.tail; i++ { |
||||||
|
c = d.buf[i] |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case endOfNumber: |
||||||
|
if decimalPlaces > 0 && decimalPlaces < len(pow10) { |
||||||
|
d.head = i |
||||||
|
return float32(float64(value) / float64(pow10[decimalPlaces])), nil |
||||||
|
} |
||||||
|
// too many decimal places
|
||||||
|
return d.f32Slow() |
||||||
|
case invalidCharForNumber, dotInNumber: |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
decimalPlaces++ |
||||||
|
if value > uint64SafeToMultiple10 { |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
value = (value << 3) + (value << 1) + uint64(ind) |
||||||
|
} |
||||||
|
} |
||||||
|
return d.f32Slow() |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) number() []byte { |
||||||
|
start := d.head |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for i, c := range buf { |
||||||
|
switch c { |
||||||
|
case '+', '-', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
continue |
||||||
|
default: |
||||||
|
// End of number.
|
||||||
|
d.head += i |
||||||
|
return d.buf[start:d.head] |
||||||
|
} |
||||||
|
} |
||||||
|
// Buffer is number within head:tail.
|
||||||
|
d.head = d.tail |
||||||
|
return d.buf[start:d.tail] |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) numberAppend(b []byte) ([]byte, error) { |
||||||
|
for { |
||||||
|
b = append(b, d.number()...) |
||||||
|
if d.head != d.tail { |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
if err := d.read(); err != nil { |
||||||
|
if err == io.EOF { |
||||||
|
return b, nil |
||||||
|
} |
||||||
|
return b, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const ( |
||||||
|
size32 = 32 |
||||||
|
size64 = 64 |
||||||
|
) |
||||||
|
|
||||||
|
func (d *Decoder) f32Slow() (float32, error) { |
||||||
|
v, err := d.floatSlow(size32) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return float32(v), err |
||||||
|
} |
||||||
|
|
||||||
|
// Float64 read float64
|
||||||
|
func (d *Decoder) Float64() (float64, error) { |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case '-': |
||||||
|
v, err := d.positiveFloat64() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
return -v, err |
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
d.unread() |
||||||
|
return d.positiveFloat64() |
||||||
|
default: |
||||||
|
return 0, badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) positiveFloat64() (float64, error) { |
||||||
|
i := d.head |
||||||
|
// First char.
|
||||||
|
if i == d.tail { |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
c := d.buf[i] |
||||||
|
i++ |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case invalidCharForNumber: |
||||||
|
return d.float64Slow() |
||||||
|
case endOfNumber: |
||||||
|
return 0, errors.New("empty") |
||||||
|
case dotInNumber: |
||||||
|
return 0, errors.New("leading dot") |
||||||
|
case 0: |
||||||
|
if i == d.tail { |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
c = d.buf[i] |
||||||
|
switch c { |
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
return 0, errors.New("leading zero") |
||||||
|
} |
||||||
|
} |
||||||
|
value := uint64(ind) |
||||||
|
// Chars before dot.
|
||||||
|
NonDecimal: |
||||||
|
for ; i < d.tail; i++ { |
||||||
|
c = d.buf[i] |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case invalidCharForNumber: |
||||||
|
return d.float64Slow() |
||||||
|
case endOfNumber: |
||||||
|
d.head = i |
||||||
|
return float64(value), nil |
||||||
|
case dotInNumber: |
||||||
|
break NonDecimal |
||||||
|
} |
||||||
|
if value > uint64SafeToMultiple10 { |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
value = (value << 3) + (value << 1) + uint64(ind) // value = value * 10 + ind;
|
||||||
|
} |
||||||
|
// chars after dot
|
||||||
|
if c == '.' { |
||||||
|
i++ |
||||||
|
decimalPlaces := 0 |
||||||
|
if i == d.tail { |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
for ; i < d.tail; i++ { |
||||||
|
c = d.buf[i] |
||||||
|
ind := floatDigits[c] |
||||||
|
switch ind { |
||||||
|
case endOfNumber: |
||||||
|
if decimalPlaces > 0 && decimalPlaces < len(pow10) { |
||||||
|
d.head = i |
||||||
|
return float64(value) / float64(pow10[decimalPlaces]), nil |
||||||
|
} |
||||||
|
// too many decimal places
|
||||||
|
return d.float64Slow() |
||||||
|
case invalidCharForNumber, dotInNumber: |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
decimalPlaces++ |
||||||
|
// Not checking for uint64SafeToMultiple10 here because
|
||||||
|
// if condition is positive value multiplied by 10 is
|
||||||
|
// guaranteed to be bigger than maxFloat64.
|
||||||
|
value = (value << 3) + (value << 1) + uint64(ind) |
||||||
|
if value > maxFloat64 { |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return d.float64Slow() |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) floatSlow(size int) (float64, error) { |
||||||
|
var buf [32]byte |
||||||
|
|
||||||
|
str, err := d.numberAppend(buf[:0]) |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "number") |
||||||
|
} |
||||||
|
if err := validateFloat(str); err != nil { |
||||||
|
return 0, errors.Wrap(err, "invalid") |
||||||
|
} |
||||||
|
|
||||||
|
val, err := strconv.ParseFloat(string(str), size) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
|
||||||
|
return val, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) float64Slow() (float64, error) { return d.floatSlow(size64) } |
||||||
|
|
||||||
|
func validateFloat(str []byte) error { |
||||||
|
// strconv.ParseFloat is not validating `1.` or `1.e1`
|
||||||
|
if len(str) == 0 { |
||||||
|
return errors.New("empty") |
||||||
|
} |
||||||
|
if str[0] == '-' { |
||||||
|
return errors.New("double minus") |
||||||
|
} |
||||||
|
if len(str) >= 2 && str[0] == '0' { |
||||||
|
switch str[1] { |
||||||
|
case 'e', 'E', '.': |
||||||
|
default: |
||||||
|
return errors.New("leading zero") |
||||||
|
} |
||||||
|
} |
||||||
|
dotPos := bytes.IndexByte(str, '.') |
||||||
|
if dotPos != -1 { |
||||||
|
if dotPos == len(str)-1 { |
||||||
|
return errors.New("dot as last char") |
||||||
|
} |
||||||
|
switch str[dotPos+1] { |
||||||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
default: |
||||||
|
return errors.New("no digit after dot") |
||||||
|
} |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,303 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"math" |
||||||
|
"strconv" |
||||||
|
|
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
var intDigits []int8 |
||||||
|
|
||||||
|
const uint32SafeToMultiply10 = uint32(0xffffffff)/10 - 1 |
||||||
|
const uint64SafeToMultiple10 = uint64(0xffffffffffffffff)/10 - 1 |
||||||
|
|
||||||
|
func init() { |
||||||
|
intDigits = make([]int8, 256) |
||||||
|
for i := 0; i < len(intDigits); i++ { |
||||||
|
intDigits[i] = invalidCharForNumber |
||||||
|
} |
||||||
|
for i := int8('0'); i <= int8('9'); i++ { |
||||||
|
intDigits[i] = i - int8('0') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) uint(size int) (uint, error) { |
||||||
|
if size == 32 { |
||||||
|
v, err := d.UInt32() |
||||||
|
return uint(v), err |
||||||
|
} |
||||||
|
v, err := d.UInt64() |
||||||
|
return uint(v), err |
||||||
|
} |
||||||
|
|
||||||
|
// UInt read uint.
|
||||||
|
func (d *Decoder) UInt() (uint, error) { |
||||||
|
return d.uint(strconv.IntSize) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) int(size int) (int, error) { |
||||||
|
if size == 32 { |
||||||
|
v, err := d.Int32() |
||||||
|
return int(v), err |
||||||
|
} |
||||||
|
v, err := d.Int64() |
||||||
|
return int(v), err |
||||||
|
} |
||||||
|
|
||||||
|
// Int reads integer.
|
||||||
|
func (d *Decoder) Int() (int, error) { |
||||||
|
return d.int(strconv.IntSize) |
||||||
|
} |
||||||
|
|
||||||
|
// Int32 reads int32 value.
|
||||||
|
func (d *Decoder) Int32() (int32, error) { |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
if c == '-' { |
||||||
|
val, err := d.readUInt32() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if val > math.MaxInt32+1 { |
||||||
|
return 0, errors.New("overflow") |
||||||
|
} |
||||||
|
return -int32(val), nil |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
val, err := d.readUInt32() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if val > math.MaxInt32 { |
||||||
|
return 0, errors.New("overflow") |
||||||
|
} |
||||||
|
return int32(val), nil |
||||||
|
} |
||||||
|
|
||||||
|
// UInt32 read uint32
|
||||||
|
func (d *Decoder) UInt32() (uint32, error) { |
||||||
|
return d.readUInt32() |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) readUInt32() (uint32, error) { |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
ind := intDigits[c] |
||||||
|
if ind == 0 { |
||||||
|
return 0, nil |
||||||
|
} |
||||||
|
if ind == invalidCharForNumber { |
||||||
|
return 0, errors.Wrap(err, "bad token") |
||||||
|
} |
||||||
|
value := uint32(ind) |
||||||
|
if d.tail-d.head > 10 { |
||||||
|
i := d.head |
||||||
|
ind2 := intDigits[d.buf[i]] |
||||||
|
if ind2 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind3 := intDigits[d.buf[i]] |
||||||
|
if ind3 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*10 + uint32(ind2), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind4 := intDigits[d.buf[i]] |
||||||
|
if ind4 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*100 + uint32(ind2)*10 + uint32(ind3), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind5 := intDigits[d.buf[i]] |
||||||
|
if ind5 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*1000 + uint32(ind2)*100 + uint32(ind3)*10 + uint32(ind4), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind6 := intDigits[d.buf[i]] |
||||||
|
if ind6 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*10000 + uint32(ind2)*1000 + uint32(ind3)*100 + uint32(ind4)*10 + uint32(ind5), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind7 := intDigits[d.buf[i]] |
||||||
|
if ind7 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*100000 + uint32(ind2)*10000 + uint32(ind3)*1000 + uint32(ind4)*100 + uint32(ind5)*10 + uint32(ind6), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind8 := intDigits[d.buf[i]] |
||||||
|
if ind8 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*1000000 + uint32(ind2)*100000 + uint32(ind3)*10000 + uint32(ind4)*1000 + uint32(ind5)*100 + uint32(ind6)*10 + uint32(ind7), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind9 := intDigits[d.buf[i]] |
||||||
|
value = value*10000000 + uint32(ind2)*1000000 + uint32(ind3)*100000 + uint32(ind4)*10000 + uint32(ind5)*1000 + uint32(ind6)*100 + uint32(ind7)*10 + uint32(ind8) |
||||||
|
d.head = i |
||||||
|
if ind9 == invalidCharForNumber { |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
} |
||||||
|
for { |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for i, c := range buf { |
||||||
|
ind = intDigits[c] |
||||||
|
if ind == invalidCharForNumber { |
||||||
|
d.head += i |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
if value > uint32SafeToMultiply10 { |
||||||
|
value2 := (value << 3) + (value << 1) + uint32(ind) |
||||||
|
if value2 < value { |
||||||
|
return 0, errors.New("overflow") |
||||||
|
} |
||||||
|
value = value2 |
||||||
|
continue |
||||||
|
} |
||||||
|
value = (value << 3) + (value << 1) + uint32(ind) |
||||||
|
} |
||||||
|
err := d.read() |
||||||
|
if err == io.EOF { |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Int64 read int64
|
||||||
|
func (d *Decoder) Int64() (int64, error) { |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
if c == '-' { |
||||||
|
c, err := d.next() |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
val, err := d.readUInt64(c) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if val > math.MaxInt64+1 { |
||||||
|
return 0, errors.Errorf("%d overflows", val) |
||||||
|
} |
||||||
|
return -int64(val), nil |
||||||
|
} |
||||||
|
val, err := d.readUInt64(c) |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
if val > math.MaxInt64 { |
||||||
|
return 0, errors.Errorf("%d overflows", val) |
||||||
|
} |
||||||
|
return int64(val), nil |
||||||
|
} |
||||||
|
|
||||||
|
// UInt64 read uint64
|
||||||
|
func (d *Decoder) UInt64() (uint64, error) { |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "byte") |
||||||
|
} |
||||||
|
return d.readUInt64(c) |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) readUInt64(c byte) (uint64, error) { |
||||||
|
ind := intDigits[c] |
||||||
|
if ind == 0 { |
||||||
|
return 0, nil // single zero
|
||||||
|
} |
||||||
|
if ind == invalidCharForNumber { |
||||||
|
return 0, errors.Wrap(badToken(c), "invalid number") |
||||||
|
} |
||||||
|
value := uint64(ind) |
||||||
|
if d.tail-d.head > 10 { |
||||||
|
i := d.head |
||||||
|
ind2 := intDigits[d.buf[i]] |
||||||
|
if ind2 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind3 := intDigits[d.buf[i]] |
||||||
|
if ind3 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*10 + uint64(ind2), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind4 := intDigits[d.buf[i]] |
||||||
|
if ind4 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*100 + uint64(ind2)*10 + uint64(ind3), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind5 := intDigits[d.buf[i]] |
||||||
|
if ind5 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*1000 + uint64(ind2)*100 + uint64(ind3)*10 + uint64(ind4), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind6 := intDigits[d.buf[i]] |
||||||
|
if ind6 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*10000 + uint64(ind2)*1000 + uint64(ind3)*100 + uint64(ind4)*10 + uint64(ind5), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind7 := intDigits[d.buf[i]] |
||||||
|
if ind7 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*100000 + uint64(ind2)*10000 + uint64(ind3)*1000 + uint64(ind4)*100 + uint64(ind5)*10 + uint64(ind6), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind8 := intDigits[d.buf[i]] |
||||||
|
if ind8 == invalidCharForNumber { |
||||||
|
d.head = i |
||||||
|
return value*1000000 + uint64(ind2)*100000 + uint64(ind3)*10000 + uint64(ind4)*1000 + uint64(ind5)*100 + uint64(ind6)*10 + uint64(ind7), nil |
||||||
|
} |
||||||
|
i++ |
||||||
|
ind9 := intDigits[d.buf[i]] |
||||||
|
value = value*10000000 + uint64(ind2)*1000000 + uint64(ind3)*100000 + uint64(ind4)*10000 + uint64(ind5)*1000 + uint64(ind6)*100 + uint64(ind7)*10 + uint64(ind8) |
||||||
|
d.head = i |
||||||
|
if ind9 == invalidCharForNumber { |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
} |
||||||
|
for { |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for i, c := range buf { |
||||||
|
ind = intDigits[c] |
||||||
|
if ind == invalidCharForNumber { |
||||||
|
d.head += i |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
if value > uint64SafeToMultiple10 { |
||||||
|
value2 := (value << 3) + (value << 1) + uint64(ind) |
||||||
|
if value2 < value { |
||||||
|
return 0, errors.New("overflow") |
||||||
|
} |
||||||
|
value = value2 |
||||||
|
continue |
||||||
|
} |
||||||
|
value = (value << 3) + (value << 1) + uint64(ind) |
||||||
|
} |
||||||
|
err := d.read() |
||||||
|
if err == io.EOF { |
||||||
|
return value, nil |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return 0, errors.Wrap(err, "read") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Null reads a json object as null and
|
||||||
|
// returns whether it's a null or not.
|
||||||
|
func (d *Decoder) Null() error { |
||||||
|
if err := d.skipSpace(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
var buf [4]byte |
||||||
|
if err := d.readExact4(&buf); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
if string(buf[:]) != "null" { |
||||||
|
const encodedNull = 'n' | 'u'<<8 | 'l'<<16 | 'l'<<24 |
||||||
|
return findInvalidToken4(buf, encodedNull) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Num decodes number.
|
||||||
|
//
|
||||||
|
// Do not retain returned value, it references underlying buffer.
|
||||||
|
func (d *Decoder) Num() (Num, error) { |
||||||
|
return d.num(nil, false) |
||||||
|
} |
||||||
|
|
||||||
|
// NumAppend appends number.
|
||||||
|
func (d *Decoder) NumAppend(v Num) (Num, error) { |
||||||
|
return d.num(v, true) |
||||||
|
} |
||||||
|
|
||||||
|
// num decodes number.
|
||||||
|
func (d *Decoder) num(v Num, forceAppend bool) (Num, error) { |
||||||
|
var str bool |
||||||
|
switch d.Next() { |
||||||
|
case String: |
||||||
|
str = true |
||||||
|
case Number: // float or integer
|
||||||
|
default: |
||||||
|
return v, errors.Errorf("unexpected %s", d.Next()) |
||||||
|
} |
||||||
|
if d.reader == nil && !forceAppend { |
||||||
|
// Can use underlying buffer directly.
|
||||||
|
start := d.head |
||||||
|
d.head++ |
||||||
|
d.number() |
||||||
|
if str { |
||||||
|
if err := d.consume('"'); err != nil { |
||||||
|
return nil, errors.Wrap(err, "end of string") |
||||||
|
} |
||||||
|
} |
||||||
|
v = d.buf[start:d.head] |
||||||
|
} else { |
||||||
|
if str { |
||||||
|
d.head++ // '"'
|
||||||
|
v = append(v, '"') |
||||||
|
} |
||||||
|
buf, err := d.numberAppend(v) |
||||||
|
if err != nil { |
||||||
|
return v, errors.Wrap(err, "decode") |
||||||
|
} |
||||||
|
if str { |
||||||
|
if err := d.consume('"'); err != nil { |
||||||
|
return nil, errors.Wrap(err, "end of string") |
||||||
|
} |
||||||
|
buf = append(buf, '"') |
||||||
|
} |
||||||
|
v = buf |
||||||
|
} |
||||||
|
|
||||||
|
var dot bool |
||||||
|
for _, c := range v { |
||||||
|
if c != '.' { |
||||||
|
continue |
||||||
|
} |
||||||
|
if dot { |
||||||
|
return v, errors.New("multiple dots in number") |
||||||
|
} |
||||||
|
dot = true |
||||||
|
} |
||||||
|
|
||||||
|
// TODO(ernado): Additional validity checks
|
||||||
|
// Current invariants:
|
||||||
|
// 1) Zero or one dot
|
||||||
|
// 2) Only: +, -, ., e, E, 0-9
|
||||||
|
|
||||||
|
return v, nil |
||||||
|
} |
@ -0,0 +1,86 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// ObjBytes calls f for every key in object, using byte slice as key.
|
||||||
|
//
|
||||||
|
// The key value is valid only until f is not returned.
|
||||||
|
func (d *Decoder) ObjBytes(f func(d *Decoder, key []byte) error) error { |
||||||
|
if err := d.consume('{'); err != nil { |
||||||
|
return errors.Wrap(err, "start") |
||||||
|
} |
||||||
|
if f == nil { |
||||||
|
return d.skipObj() |
||||||
|
} |
||||||
|
if err := d.incDepth(); err != nil { |
||||||
|
return errors.Wrap(err, "inc") |
||||||
|
} |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
if c == '}' { |
||||||
|
return d.decDepth() |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
|
||||||
|
k, err := d.str(value{raw: true}) |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "str") |
||||||
|
} |
||||||
|
if err := d.consume(':'); err != nil { |
||||||
|
return errors.Wrap(err, "field") |
||||||
|
} |
||||||
|
// Skip whitespace.
|
||||||
|
if _, err = d.more(); err != nil { |
||||||
|
return errors.Wrap(err, "more") |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
if err := f(d, k.buf); err != nil { |
||||||
|
return errors.Wrap(err, "callback") |
||||||
|
} |
||||||
|
|
||||||
|
c, err = d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
for c == ',' { |
||||||
|
k, err := d.str(value{raw: true}) |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "str") |
||||||
|
} |
||||||
|
if err := d.consume(':'); err != nil { |
||||||
|
return errors.Wrap(err, "field") |
||||||
|
} |
||||||
|
// Check that value exists.
|
||||||
|
if _, err = d.more(); err != nil { |
||||||
|
return errors.Wrap(err, "more") |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
if err := f(d, k.buf); err != nil { |
||||||
|
return errors.Wrap(err, "callback") |
||||||
|
} |
||||||
|
if c, err = d.more(); err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
} |
||||||
|
if c != '}' { |
||||||
|
return errors.Wrap(badToken(c), "err") |
||||||
|
} |
||||||
|
return d.decDepth() |
||||||
|
} |
||||||
|
|
||||||
|
// Obj reads json object, calling f on each field.
|
||||||
|
//
|
||||||
|
// Use ObjBytes to reduce heap allocations for keys.
|
||||||
|
func (d *Decoder) Obj(f func(d *Decoder, key string) error) error { |
||||||
|
if f == nil { |
||||||
|
// Skipping object.
|
||||||
|
return d.ObjBytes(nil) |
||||||
|
} |
||||||
|
return d.ObjBytes(func(d *Decoder, key []byte) error { |
||||||
|
return f(d, string(key)) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import "github.com/go-faster/errors" |
||||||
|
|
||||||
|
// Raw is like Skip(), but saves and returns skipped value as raw json.
|
||||||
|
//
|
||||||
|
// Do not retain returned value, it references underlying buffer.
|
||||||
|
func (d *Decoder) Raw() (Raw, error) { |
||||||
|
if d.reader != nil { |
||||||
|
return nil, errors.New("not implemented for io.Reader") |
||||||
|
} |
||||||
|
|
||||||
|
start := d.head |
||||||
|
if err := d.Skip(); err != nil { |
||||||
|
return nil, errors.Wrap(err, "skip") |
||||||
|
} |
||||||
|
|
||||||
|
return d.buf[start:d.head], nil |
||||||
|
} |
||||||
|
|
||||||
|
// RawAppend is Raw that appends saved raw json value to buf.
|
||||||
|
func (d *Decoder) RawAppend(buf Raw) (Raw, error) { |
||||||
|
raw, err := d.Raw() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return append(buf, raw...), err |
||||||
|
} |
||||||
|
|
||||||
|
// Raw json value.
|
||||||
|
type Raw []byte |
||||||
|
|
||||||
|
// Type of Raw json value.
|
||||||
|
func (r Raw) Type() Type { |
||||||
|
d := Decoder{buf: r, tail: len(r)} |
||||||
|
return d.Next() |
||||||
|
} |
||||||
|
|
||||||
|
func (r Raw) String() string { return string(r) } |
@ -0,0 +1,146 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
"math/bits" |
||||||
|
) |
||||||
|
|
||||||
|
// Next gets Type of relatively next json element
|
||||||
|
func (d *Decoder) Next() Type { |
||||||
|
v, err := d.next() |
||||||
|
if err == nil { |
||||||
|
d.unread() |
||||||
|
} |
||||||
|
return types[v] |
||||||
|
} |
||||||
|
|
||||||
|
var spaceSet = [256]byte{ |
||||||
|
' ': 1, '\n': 1, '\t': 1, '\r': 1, |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) consume(c byte) (err error) { |
||||||
|
for { |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for i, got := range buf { |
||||||
|
switch spaceSet[got] { |
||||||
|
default: |
||||||
|
d.head += i + 1 |
||||||
|
if c != got { |
||||||
|
return badToken(got) |
||||||
|
} |
||||||
|
return nil |
||||||
|
case 1: |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
if err = d.read(); err != nil { |
||||||
|
if err == io.EOF { |
||||||
|
return io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// more is next but io.EOF is unexpected.
|
||||||
|
func (d *Decoder) more() (byte, error) { |
||||||
|
c, err := d.next() |
||||||
|
if err == io.EOF { |
||||||
|
err = io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return c, err |
||||||
|
} |
||||||
|
|
||||||
|
// next reads next non-whitespace token or error.
|
||||||
|
func (d *Decoder) next() (byte, error) { |
||||||
|
for { |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for i, c := range buf { |
||||||
|
switch spaceSet[c] { |
||||||
|
default: |
||||||
|
d.head += i + 1 |
||||||
|
return c, nil |
||||||
|
case 1: |
||||||
|
continue |
||||||
|
} |
||||||
|
} |
||||||
|
if err := d.read(); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) byte() (byte, error) { |
||||||
|
if d.head == d.tail { |
||||||
|
err := d.read() |
||||||
|
if err == io.EOF { |
||||||
|
err = io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
} |
||||||
|
c := d.buf[d.head] |
||||||
|
d.head++ |
||||||
|
return c, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) read() error { |
||||||
|
if d.reader == nil { |
||||||
|
d.head = d.tail |
||||||
|
return io.EOF |
||||||
|
} |
||||||
|
|
||||||
|
n, err := d.reader.Read(d.buf) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
d.head = 0 |
||||||
|
d.tail = n |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) readAtLeast(min int) error { |
||||||
|
if d.reader == nil { |
||||||
|
d.head = d.tail |
||||||
|
return io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
|
||||||
|
if need := min - len(d.buf); need > 0 { |
||||||
|
d.buf = append(d.buf, make([]byte, need)...) |
||||||
|
} |
||||||
|
n, err := io.ReadAtLeast(d.reader, d.buf, min) |
||||||
|
if err != nil { |
||||||
|
if err == io.EOF && n == 0 { |
||||||
|
return io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
d.head = 0 |
||||||
|
d.tail = n |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) unread() { d.head-- } |
||||||
|
|
||||||
|
func (d *Decoder) readExact4(b *[4]byte) error { |
||||||
|
if buf := d.buf[d.head:d.tail]; len(buf) >= len(b) { |
||||||
|
d.head += copy(b[:], buf[:4]) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
n := copy(b[:], d.buf[d.head:d.tail]) |
||||||
|
if err := d.readAtLeast(len(b) - n); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
d.head += copy(b[n:], d.buf[d.head:d.tail]) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func findInvalidToken4(buf [4]byte, mask uint32) error { |
||||||
|
c := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24 |
||||||
|
idx := bits.TrailingZeros32(c^mask) / 8 |
||||||
|
return badToken(buf[idx]) |
||||||
|
} |
@ -0,0 +1,472 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Skip skips a json object and positions to relatively the next json object.
|
||||||
|
func (d *Decoder) Skip() error { |
||||||
|
c, err := d.next() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case '"': |
||||||
|
if err := d.skipStr(); err != nil { |
||||||
|
return errors.Wrap(err, "str") |
||||||
|
} |
||||||
|
return nil |
||||||
|
case 'n': |
||||||
|
d.unread() |
||||||
|
return d.Null() |
||||||
|
case 't', 'f': |
||||||
|
d.unread() |
||||||
|
_, err := d.Bool() |
||||||
|
return err |
||||||
|
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': |
||||||
|
d.unread() |
||||||
|
return d.skipNumber() |
||||||
|
case '[': |
||||||
|
if err := d.skipArr(); err != nil { |
||||||
|
return errors.Wrap(err, "array") |
||||||
|
} |
||||||
|
return nil |
||||||
|
case '{': |
||||||
|
if err := d.skipObj(); err != nil { |
||||||
|
return errors.Wrap(err, "object") |
||||||
|
} |
||||||
|
return nil |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
skipNumberSet = [256]byte{ |
||||||
|
'0': 1, |
||||||
|
'1': 1, |
||||||
|
'2': 1, |
||||||
|
'3': 1, |
||||||
|
'4': 1, |
||||||
|
'5': 1, |
||||||
|
'6': 1, |
||||||
|
'7': 1, |
||||||
|
'8': 1, |
||||||
|
'9': 1, |
||||||
|
|
||||||
|
',': 2, |
||||||
|
']': 2, |
||||||
|
'}': 2, |
||||||
|
' ': 2, |
||||||
|
'\t': 2, |
||||||
|
'\n': 2, |
||||||
|
'\r': 2, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// skipNumber reads one JSON number.
|
||||||
|
//
|
||||||
|
// Assumes d.buf is not empty.
|
||||||
|
func (d *Decoder) skipNumber() error { |
||||||
|
const ( |
||||||
|
digitTag byte = 1 |
||||||
|
closerTag byte = 2 |
||||||
|
) |
||||||
|
c := d.buf[d.head] |
||||||
|
d.head++ |
||||||
|
switch c { |
||||||
|
case '-': |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// Character after '-' must be a digit.
|
||||||
|
if skipNumberSet[c] != digitTag { |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
if c != '0' { |
||||||
|
break |
||||||
|
} |
||||||
|
fallthrough |
||||||
|
case '0': |
||||||
|
// If buffer is empty, try to read more.
|
||||||
|
if d.head == d.tail { |
||||||
|
err := d.read() |
||||||
|
if err != nil { |
||||||
|
// There is no data anymore.
|
||||||
|
if err == io.EOF { |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
c = d.buf[d.head] |
||||||
|
if skipNumberSet[c] == closerTag { |
||||||
|
return nil |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case '.': |
||||||
|
goto stateDot |
||||||
|
case 'e', 'E': |
||||||
|
goto stateExp |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
for { |
||||||
|
for i, c := range d.buf[d.head:d.tail] { |
||||||
|
switch skipNumberSet[c] { |
||||||
|
case closerTag: |
||||||
|
d.head += i |
||||||
|
return nil |
||||||
|
case digitTag: |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
switch c { |
||||||
|
case '.': |
||||||
|
d.head += i |
||||||
|
goto stateDot |
||||||
|
case 'e', 'E': |
||||||
|
d.head += i |
||||||
|
goto stateExp |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := d.read(); err != nil { |
||||||
|
// There is no data anymore.
|
||||||
|
if err == io.EOF { |
||||||
|
d.head = d.tail |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
stateDot: |
||||||
|
d.head++ |
||||||
|
{ |
||||||
|
var last byte = '.' |
||||||
|
for { |
||||||
|
for i, c := range d.buf[d.head:d.tail] { |
||||||
|
switch skipNumberSet[c] { |
||||||
|
case closerTag: |
||||||
|
d.head += i |
||||||
|
// Check that dot is not last character.
|
||||||
|
if last == '.' { |
||||||
|
return io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return nil |
||||||
|
case digitTag: |
||||||
|
last = c |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
switch c { |
||||||
|
case 'e', 'E': |
||||||
|
if last == '.' { |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
d.head += i |
||||||
|
goto stateExp |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := d.read(); err != nil { |
||||||
|
// There is no data anymore.
|
||||||
|
if err == io.EOF { |
||||||
|
d.head = d.tail |
||||||
|
// Check that dot is not last character.
|
||||||
|
if last == '.' { |
||||||
|
return io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
stateExp: |
||||||
|
d.head++ |
||||||
|
// There must be a number or sign after e.
|
||||||
|
{ |
||||||
|
numOrSign, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if skipNumberSet[numOrSign] != digitTag { // If next character is not a digit, check for sign.
|
||||||
|
if numOrSign == '-' || numOrSign == '+' { |
||||||
|
num, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
// There must be a number after sign.
|
||||||
|
if skipNumberSet[num] != digitTag { |
||||||
|
return badToken(num) |
||||||
|
} |
||||||
|
} else { |
||||||
|
return badToken(numOrSign) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
for { |
||||||
|
for i, c := range d.buf[d.head:d.tail] { |
||||||
|
if skipNumberSet[c] == closerTag { |
||||||
|
d.head += i |
||||||
|
return nil |
||||||
|
} |
||||||
|
if skipNumberSet[c] == 0 { |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := d.read(); err != nil { |
||||||
|
// There is no data anymore.
|
||||||
|
if err == io.EOF { |
||||||
|
d.head = d.tail |
||||||
|
return nil |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ( |
||||||
|
escapedStrSet = [256]byte{ |
||||||
|
'"': '"', |
||||||
|
'\\': '\\', |
||||||
|
'/': '/', |
||||||
|
'b': '\b', |
||||||
|
'f': '\f', |
||||||
|
'n': '\n', |
||||||
|
'r': '\r', |
||||||
|
't': '\t', |
||||||
|
'u': 'u', |
||||||
|
} |
||||||
|
hexSet = [256]byte{ |
||||||
|
'0': 0x0 + 1, '1': 0x1 + 1, '2': 0x2 + 1, '3': 0x3 + 1, |
||||||
|
'4': 0x4 + 1, '5': 0x5 + 1, '6': 0x6 + 1, '7': 0x7 + 1, |
||||||
|
'8': 0x8 + 1, '9': 0x9 + 1, |
||||||
|
|
||||||
|
'A': 0xA + 1, 'B': 0xB + 1, 'C': 0xC + 1, 'D': 0xD + 1, |
||||||
|
'E': 0xE + 1, 'F': 0xF + 1, |
||||||
|
|
||||||
|
'a': 0xa + 1, 'b': 0xb + 1, 'c': 0xc + 1, 'd': 0xd + 1, |
||||||
|
'e': 0xe + 1, 'f': 0xf + 1, |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
// skipStr reads one JSON string.
|
||||||
|
//
|
||||||
|
// Assumes first quote was consumed.
|
||||||
|
func (d *Decoder) skipStr() error { |
||||||
|
var ( |
||||||
|
c byte |
||||||
|
i int |
||||||
|
) |
||||||
|
readStr: |
||||||
|
for { |
||||||
|
i = 0 |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for len(buf) >= 8 { |
||||||
|
c = buf[0] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[1] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[2] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[3] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[4] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[5] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[6] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[7] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
buf = buf[8:] |
||||||
|
} |
||||||
|
var n int |
||||||
|
for n, c = range buf { |
||||||
|
if safeSet[c] != 0 { |
||||||
|
i += n |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := d.read(); err != nil { |
||||||
|
if err == io.EOF { |
||||||
|
err = io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return err |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
readTok: |
||||||
|
; // Bug in cover tool, see https://github.com/golang/go/issues/28319.
|
||||||
|
switch { |
||||||
|
case c == '"': |
||||||
|
d.head += i + 1 |
||||||
|
return nil |
||||||
|
case c == '\\': |
||||||
|
d.head += i + 1 |
||||||
|
v, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
switch escapedStrSet[v] { |
||||||
|
case 'u': |
||||||
|
for i := 0; i < 4; i++ { |
||||||
|
h, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if hexSet[h] == 0 { |
||||||
|
return badToken(h) |
||||||
|
} |
||||||
|
} |
||||||
|
case 0: |
||||||
|
return badToken(v) |
||||||
|
} |
||||||
|
case c < ' ': |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
goto readStr |
||||||
|
} |
||||||
|
|
||||||
|
// skipObj reads JSON object.
|
||||||
|
//
|
||||||
|
// Assumes first bracket was consumed.
|
||||||
|
func (d *Decoder) skipObj() error { |
||||||
|
if err := d.incDepth(); err != nil { |
||||||
|
return errors.Wrap(err, "inc") |
||||||
|
} |
||||||
|
|
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case '}': |
||||||
|
return d.decDepth() |
||||||
|
case '"': |
||||||
|
d.unread() |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
|
||||||
|
for { |
||||||
|
if err := d.consume('"'); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
if err := d.skipStr(); err != nil { |
||||||
|
return errors.Wrap(err, "read field name") |
||||||
|
} |
||||||
|
if err := d.consume(':'); err != nil { |
||||||
|
return errors.Wrap(err, "field") |
||||||
|
} |
||||||
|
if err := d.Skip(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "read comma") |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case ',': |
||||||
|
continue |
||||||
|
case '}': |
||||||
|
return d.decDepth() |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// skipArr reads JSON array.
|
||||||
|
//
|
||||||
|
// Assumes first bracket was consumed.
|
||||||
|
func (d *Decoder) skipArr() error { |
||||||
|
if err := d.incDepth(); err != nil { |
||||||
|
return errors.Wrap(err, "inc") |
||||||
|
} |
||||||
|
|
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
if c == ']' { |
||||||
|
return d.decDepth() |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
|
||||||
|
for { |
||||||
|
if err := d.Skip(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
c, err := d.more() |
||||||
|
if err != nil { |
||||||
|
return errors.Wrap(err, "read comma") |
||||||
|
} |
||||||
|
switch c { |
||||||
|
case ',': |
||||||
|
continue |
||||||
|
case ']': |
||||||
|
return d.decDepth() |
||||||
|
default: |
||||||
|
return badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// skipSpace skips space characters.
|
||||||
|
//
|
||||||
|
// Returns io.ErrUnexpectedEOF if got io.EOF.
|
||||||
|
func (d *Decoder) skipSpace() error { |
||||||
|
// Skip space.
|
||||||
|
if _, err := d.more(); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
d.unread() |
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,324 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"unicode/utf16" |
||||||
|
"unicode/utf8" |
||||||
|
|
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// StrAppend reads string and appends it to byte slice.
|
||||||
|
func (d *Decoder) StrAppend(b []byte) ([]byte, error) { |
||||||
|
v := value{ |
||||||
|
buf: b, |
||||||
|
raw: false, |
||||||
|
} |
||||||
|
var err error |
||||||
|
if v, err = d.str(v); err != nil { |
||||||
|
return b, err |
||||||
|
} |
||||||
|
return v.buf, nil |
||||||
|
} |
||||||
|
|
||||||
|
type value struct { |
||||||
|
buf []byte |
||||||
|
raw bool // false forces buf reuse
|
||||||
|
} |
||||||
|
|
||||||
|
func (v value) rune(r rune) value { |
||||||
|
return value{ |
||||||
|
buf: appendRune(v.buf, r), |
||||||
|
raw: v.raw, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// badTokenErr means that Token was unexpected while decoding.
|
||||||
|
type badTokenErr struct { |
||||||
|
Token byte |
||||||
|
} |
||||||
|
|
||||||
|
func (e badTokenErr) Error() string { |
||||||
|
return fmt.Sprintf("unexpected byte %d '%s'", e.Token, []byte{e.Token}) |
||||||
|
} |
||||||
|
|
||||||
|
func badToken(c byte) error { |
||||||
|
return badTokenErr{Token: c} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) str(v value) (value, error) { |
||||||
|
if err := d.consume('"'); err != nil { |
||||||
|
return value{}, errors.Wrap(err, "start") |
||||||
|
} |
||||||
|
var ( |
||||||
|
c byte |
||||||
|
i int |
||||||
|
) |
||||||
|
for { |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for len(buf) >= 8 { |
||||||
|
c = buf[0] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[1] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[2] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[3] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[4] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[5] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[6] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[7] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
buf = buf[8:] |
||||||
|
} |
||||||
|
var n int |
||||||
|
for n, c = range buf { |
||||||
|
if safeSet[c] != 0 { |
||||||
|
i += n |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
} |
||||||
|
return d.strSlow(v) |
||||||
|
} |
||||||
|
readTok: |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
str := buf[:i] |
||||||
|
|
||||||
|
switch { |
||||||
|
case c == '"': |
||||||
|
// Skip string + last quote.
|
||||||
|
d.head += i + 1 |
||||||
|
if v.raw { |
||||||
|
return value{buf: str}, nil |
||||||
|
} |
||||||
|
return value{buf: append(v.buf, str...)}, nil |
||||||
|
case c == '\\': |
||||||
|
// Skip only string, keep quote in buffer.
|
||||||
|
d.head += i |
||||||
|
// We need a copy anyway, because string is escaped.
|
||||||
|
return d.strSlow(value{buf: append(v.buf, str...)}) |
||||||
|
default: |
||||||
|
return v, badToken(c) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) strSlow(v value) (value, error) { |
||||||
|
var ( |
||||||
|
c byte |
||||||
|
i int |
||||||
|
) |
||||||
|
readStr: |
||||||
|
for { |
||||||
|
i = 0 |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
for len(buf) >= 8 { |
||||||
|
c = buf[0] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[1] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[2] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[3] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[4] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[5] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[6] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
c = buf[7] |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
|
||||||
|
buf = buf[8:] |
||||||
|
} |
||||||
|
for _, c = range buf { |
||||||
|
if safeSet[c] != 0 { |
||||||
|
goto readTok |
||||||
|
} |
||||||
|
i++ |
||||||
|
} |
||||||
|
|
||||||
|
v.buf = append(v.buf, d.buf[d.head:d.head+i]...) |
||||||
|
if err := d.read(); err != nil { |
||||||
|
if err == io.EOF { |
||||||
|
return value{}, io.ErrUnexpectedEOF |
||||||
|
} |
||||||
|
return value{}, err |
||||||
|
} |
||||||
|
} |
||||||
|
readTok: |
||||||
|
buf := d.buf[d.head:d.tail] |
||||||
|
str := buf[:i] |
||||||
|
d.head += i + 1 |
||||||
|
|
||||||
|
switch { |
||||||
|
case c == '"': |
||||||
|
return value{buf: append(v.buf, str...)}, nil |
||||||
|
case c == '\\': |
||||||
|
v.buf = append(v.buf, str...) |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return value{}, errors.Wrap(err, "next") |
||||||
|
} |
||||||
|
v, err = d.escapedChar(v, c) |
||||||
|
if err != nil { |
||||||
|
return v, errors.Wrap(err, "escape") |
||||||
|
} |
||||||
|
default: |
||||||
|
return v, badToken(c) |
||||||
|
} |
||||||
|
goto readStr |
||||||
|
} |
||||||
|
|
||||||
|
// StrBytes returns string value as sub-slice of internal buffer.
|
||||||
|
//
|
||||||
|
// Bytes are valid only until next call to any Decoder method.
|
||||||
|
func (d *Decoder) StrBytes() ([]byte, error) { |
||||||
|
v, err := d.str(value{raw: true}) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return v.buf, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Str reads string.
|
||||||
|
func (d *Decoder) Str() (string, error) { |
||||||
|
s, err := d.StrBytes() |
||||||
|
if err != nil { |
||||||
|
return "", err |
||||||
|
} |
||||||
|
return string(s), nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) escapedChar(v value, c byte) (value, error) { |
||||||
|
switch val := escapedStrSet[c]; val { |
||||||
|
default: |
||||||
|
v.buf = append(v.buf, val) |
||||||
|
case 'u': |
||||||
|
r1, err := d.readU4() |
||||||
|
if err != nil { |
||||||
|
return value{}, errors.Wrap(err, "read u4") |
||||||
|
} |
||||||
|
if utf16.IsSurrogate(r1) { |
||||||
|
c, err := d.byte() |
||||||
|
if err != nil { |
||||||
|
return value{}, err |
||||||
|
} |
||||||
|
if c != '\\' { |
||||||
|
d.unread() |
||||||
|
return v.rune(r1), nil |
||||||
|
} |
||||||
|
c, err = d.byte() |
||||||
|
if err != nil { |
||||||
|
return value{}, err |
||||||
|
} |
||||||
|
if c != 'u' { |
||||||
|
return d.escapedChar(v.rune(r1), c) |
||||||
|
} |
||||||
|
r2, err := d.readU4() |
||||||
|
if err != nil { |
||||||
|
return value{}, err |
||||||
|
} |
||||||
|
combined := utf16.DecodeRune(r1, r2) |
||||||
|
if combined == '\uFFFD' { |
||||||
|
v = v.rune(r1).rune(r2) |
||||||
|
} else { |
||||||
|
v = v.rune(combined) |
||||||
|
} |
||||||
|
} else { |
||||||
|
v = v.rune(r1) |
||||||
|
} |
||||||
|
case 0: |
||||||
|
return v, errors.Wrap(badToken(c), "bad escape: %w") |
||||||
|
} |
||||||
|
return v, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (d *Decoder) readU4() (v rune, _ error) { |
||||||
|
var b [4]byte |
||||||
|
if err := d.readExact4(&b); err != nil { |
||||||
|
return 0, err |
||||||
|
} |
||||||
|
for _, c := range b { |
||||||
|
val := hexSet[c] |
||||||
|
if val == 0 { |
||||||
|
return 0, badToken(c) |
||||||
|
} |
||||||
|
v = v*16 + rune(val-1) |
||||||
|
} |
||||||
|
return v, nil |
||||||
|
} |
||||||
|
|
||||||
|
func appendRune(p []byte, r rune) []byte { |
||||||
|
buf := make([]byte, 4) |
||||||
|
n := utf8.EncodeRune(buf, r) |
||||||
|
return append(p, buf[:n]...) |
||||||
|
} |
@ -0,0 +1,22 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"io" |
||||||
|
|
||||||
|
"github.com/go-faster/errors" |
||||||
|
) |
||||||
|
|
||||||
|
// Validate consumes all input, validating that input is a json object
|
||||||
|
// without any trialing data.
|
||||||
|
func (d *Decoder) Validate() error { |
||||||
|
// First encountered value skip should consume all buffer.
|
||||||
|
if err := d.Skip(); err != nil { |
||||||
|
return errors.Wrap(err, "consume") |
||||||
|
} |
||||||
|
// Check for any trialing json.
|
||||||
|
if err := d.Skip(); err != io.EOF { |
||||||
|
return errors.Wrap(err, "unexpected trialing data") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
@ -0,0 +1,199 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
import "io" |
||||||
|
|
||||||
|
// Encoder encodes json to underlying buffer.
|
||||||
|
//
|
||||||
|
// Zero value is valid.
|
||||||
|
type Encoder struct { |
||||||
|
w Writer // underlying writer
|
||||||
|
indent int // count of spaces for single indentation level
|
||||||
|
|
||||||
|
// first handles state for comma and indentation writing.
|
||||||
|
//
|
||||||
|
// New Object or Array appends new level to this slice, and
|
||||||
|
// last element of this slice denotes whether first element was written.
|
||||||
|
//
|
||||||
|
// We write commas only before non-first element of Array or Object.
|
||||||
|
//
|
||||||
|
// See comma, begin, end and FieldStart for implementation details.
|
||||||
|
//
|
||||||
|
// Note: probably, this can be optimized as bit set to ease memory
|
||||||
|
// consumption.
|
||||||
|
//
|
||||||
|
// See https://yourbasic.org/algorithms/your-basic-int/#simple-sets
|
||||||
|
first []bool |
||||||
|
} |
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (e *Encoder) Write(p []byte) (n int, err error) { |
||||||
|
return e.w.Write(p) |
||||||
|
} |
||||||
|
|
||||||
|
// WriteTo implements io.WriterTo.
|
||||||
|
func (e *Encoder) WriteTo(w io.Writer) (n int64, err error) { |
||||||
|
return e.w.WriteTo(w) |
||||||
|
} |
||||||
|
|
||||||
|
// SetIdent sets length of single indentation step.
|
||||||
|
func (e *Encoder) SetIdent(n int) { |
||||||
|
e.indent = n |
||||||
|
} |
||||||
|
|
||||||
|
// String returns string of underlying buffer.
|
||||||
|
func (e Encoder) String() string { |
||||||
|
return string(e.Bytes()) |
||||||
|
} |
||||||
|
|
||||||
|
// Reset resets underlying buffer.
|
||||||
|
func (e *Encoder) Reset() { |
||||||
|
e.w.Buf = e.w.Buf[:0] |
||||||
|
e.first = e.first[:0] |
||||||
|
} |
||||||
|
|
||||||
|
// Bytes returns underlying buffer.
|
||||||
|
func (e Encoder) Bytes() []byte { return e.w.Buf } |
||||||
|
|
||||||
|
// SetBytes sets underlying buffer.
|
||||||
|
func (e *Encoder) SetBytes(buf []byte) { e.w.Buf = buf } |
||||||
|
|
||||||
|
// byte writes a single byte.
|
||||||
|
func (e *Encoder) byte(c byte) { |
||||||
|
e.w.Buf = append(e.w.Buf, c) |
||||||
|
} |
||||||
|
|
||||||
|
// RawStr writes string as raw json.
|
||||||
|
func (e *Encoder) RawStr(v string) { |
||||||
|
e.comma() |
||||||
|
e.w.RawStr(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Raw writes byte slice as raw json.
|
||||||
|
func (e *Encoder) Raw(b []byte) { |
||||||
|
e.comma() |
||||||
|
e.w.Raw(b) |
||||||
|
} |
||||||
|
|
||||||
|
// Null writes null.
|
||||||
|
func (e *Encoder) Null() { |
||||||
|
e.comma() |
||||||
|
e.w.Null() |
||||||
|
} |
||||||
|
|
||||||
|
// Bool encodes boolean.
|
||||||
|
func (e *Encoder) Bool(v bool) { |
||||||
|
e.comma() |
||||||
|
e.w.Bool(v) |
||||||
|
} |
||||||
|
|
||||||
|
// ObjStart writes object start, performing indentation if needed.
|
||||||
|
//
|
||||||
|
// Use Obj as convenience helper for writing objects.
|
||||||
|
func (e *Encoder) ObjStart() { |
||||||
|
e.comma() |
||||||
|
e.w.ObjStart() |
||||||
|
e.begin() |
||||||
|
e.writeIndent() |
||||||
|
} |
||||||
|
|
||||||
|
// FieldStart encodes field name and writes colon.
|
||||||
|
//
|
||||||
|
// For non-zero indentation also writes single space after colon.
|
||||||
|
//
|
||||||
|
// Use Field as convenience helper for encoding fields.
|
||||||
|
func (e *Encoder) FieldStart(field string) { |
||||||
|
e.comma() |
||||||
|
e.w.FieldStart(field) |
||||||
|
if e.indent > 0 { |
||||||
|
e.byte(' ') |
||||||
|
} |
||||||
|
if len(e.first) > 0 { |
||||||
|
e.first[e.current()] = true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Field encodes field start and then invokes callback.
|
||||||
|
//
|
||||||
|
// Has ~5ns overhead over FieldStart.
|
||||||
|
func (e *Encoder) Field(name string, f func(e *Encoder)) { |
||||||
|
e.FieldStart(name) |
||||||
|
f(e) |
||||||
|
} |
||||||
|
|
||||||
|
// ObjEnd writes end of object token, performing indentation if needed.
|
||||||
|
//
|
||||||
|
// Use Obj as convenience helper for writing objects.
|
||||||
|
func (e *Encoder) ObjEnd() { |
||||||
|
e.end() |
||||||
|
e.writeIndent() |
||||||
|
e.w.ObjEnd() |
||||||
|
} |
||||||
|
|
||||||
|
// ObjEmpty writes empty object.
|
||||||
|
func (e *Encoder) ObjEmpty() { |
||||||
|
e.comma() |
||||||
|
e.w.ObjStart() |
||||||
|
e.w.ObjEnd() |
||||||
|
} |
||||||
|
|
||||||
|
// Obj writes start of object, invokes callback and writes end of object.
|
||||||
|
//
|
||||||
|
// If callback is nil, writes empty object.
|
||||||
|
func (e *Encoder) Obj(f func(e *Encoder)) { |
||||||
|
if f == nil { |
||||||
|
e.ObjEmpty() |
||||||
|
return |
||||||
|
} |
||||||
|
e.ObjStart() |
||||||
|
f(e) |
||||||
|
e.ObjEnd() |
||||||
|
} |
||||||
|
|
||||||
|
// ArrStart writes start of array, performing indentation if needed.
|
||||||
|
//
|
||||||
|
// Use Arr as convenience helper for writing arrays.
|
||||||
|
func (e *Encoder) ArrStart() { |
||||||
|
e.comma() |
||||||
|
e.w.ArrStart() |
||||||
|
e.begin() |
||||||
|
e.writeIndent() |
||||||
|
} |
||||||
|
|
||||||
|
// ArrEmpty writes empty array.
|
||||||
|
func (e *Encoder) ArrEmpty() { |
||||||
|
e.comma() |
||||||
|
e.w.ArrStart() |
||||||
|
e.w.ArrEnd() |
||||||
|
} |
||||||
|
|
||||||
|
// ArrEnd writes end of array, performing indentation if needed.
|
||||||
|
//
|
||||||
|
// Use Arr as convenience helper for writing arrays.
|
||||||
|
func (e *Encoder) ArrEnd() { |
||||||
|
e.end() |
||||||
|
e.writeIndent() |
||||||
|
e.w.ArrEnd() |
||||||
|
} |
||||||
|
|
||||||
|
// Arr writes start of array, invokes callback and writes end of array.
|
||||||
|
//
|
||||||
|
// If callback is nil, writes empty array.
|
||||||
|
func (e *Encoder) Arr(f func(e *Encoder)) { |
||||||
|
if f == nil { |
||||||
|
e.ArrEmpty() |
||||||
|
return |
||||||
|
} |
||||||
|
e.ArrStart() |
||||||
|
f(e) |
||||||
|
e.ArrEnd() |
||||||
|
} |
||||||
|
|
||||||
|
func (e *Encoder) writeIndent() { |
||||||
|
if e.indent == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
e.byte('\n') |
||||||
|
for i := 0; i < len(e.first)*e.indent; i++ { |
||||||
|
e.w.Buf = append(e.w.Buf, ' ') |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Base64 encodes data as standard base64 encoded string.
|
||||||
|
//
|
||||||
|
// Same as encoding/json, base64.StdEncoding or RFC 4648.
|
||||||
|
func (e *Encoder) Base64(data []byte) { |
||||||
|
e.comma() |
||||||
|
e.w.Base64(data) |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// begin should be called before new Array or Object.
|
||||||
|
func (e *Encoder) begin() { |
||||||
|
e.first = append(e.first, true) |
||||||
|
} |
||||||
|
|
||||||
|
// end should be called after Array or Object.
|
||||||
|
func (e *Encoder) end() { |
||||||
|
if len(e.first) == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
e.first = e.first[:e.current()] |
||||||
|
} |
||||||
|
|
||||||
|
func (e *Encoder) current() int { return len(e.first) - 1 } |
||||||
|
|
||||||
|
// comma should be called before any new value.
|
||||||
|
func (e *Encoder) comma() { |
||||||
|
// Writing commas.
|
||||||
|
// 1. Before every field expect first.
|
||||||
|
// 2. Before every array element except first.
|
||||||
|
if len(e.first) == 0 { |
||||||
|
return |
||||||
|
} |
||||||
|
current := e.current() |
||||||
|
_ = e.first[current] |
||||||
|
if e.first[current] { |
||||||
|
e.first[current] = false |
||||||
|
return |
||||||
|
} |
||||||
|
e.byte(',') |
||||||
|
e.writeIndent() |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Float32 encodes float32.
|
||||||
|
//
|
||||||
|
// NB: Infinities and NaN are represented as null.
|
||||||
|
func (e *Encoder) Float32(v float32) { |
||||||
|
e.comma() |
||||||
|
e.w.Float32(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Float64 encodes float64.
|
||||||
|
//
|
||||||
|
// NB: Infinities and NaN are represented as null.
|
||||||
|
func (e *Encoder) Float64(v float64) { |
||||||
|
e.comma() |
||||||
|
e.w.Float64(v) |
||||||
|
} |
@ -0,0 +1,25 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Int encodes int.
|
||||||
|
func (e *Encoder) Int(v int) { |
||||||
|
e.comma() |
||||||
|
e.w.Int(v) |
||||||
|
} |
||||||
|
|
||||||
|
// UInt encodes uint.
|
||||||
|
func (e *Encoder) UInt(v uint) { |
||||||
|
e.comma() |
||||||
|
e.w.UInt(v) |
||||||
|
} |
||||||
|
|
||||||
|
// UInt8 encodes uint8.
|
||||||
|
func (e *Encoder) UInt8(v uint8) { |
||||||
|
e.comma() |
||||||
|
e.w.UInt8(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Int8 encodes int8.
|
||||||
|
func (e *Encoder) Int8(v int8) { |
||||||
|
e.comma() |
||||||
|
e.w.Int8(v) |
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// Num encodes number.
|
||||||
|
func (e *Encoder) Num(v Num) { |
||||||
|
e.comma() |
||||||
|
e.w.Num(v) |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package jx |
||||||
|
|
||||||
|
// StrEscape encodes string with html special characters escaping.
|
||||||
|
func (e *Encoder) StrEscape(v string) { |
||||||
|
e.comma() |
||||||
|
e.w.StrEscape(v) |
||||||
|
} |
||||||
|
|
||||||
|
// Str encodes string without html escaping.
|
||||||
|
//
|
||||||
|
// Use StrEscape to escape html, this is default for encoding/json and
|
||||||
|
// should be used by default for untrusted strings.
|
||||||
|
func (e *Encoder) Str(v string) { |
||||||
|
e.comma() |
||||||
|
e.w.Str(v) |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package jx |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
) |
||||||
|
|
||||||
|
func Fuzz(data []byte) int { |
||||||
|
got := Valid(data) |
||||||
|
exp := json.Valid(data) |
||||||
|
if !exp && got { |
||||||
|
fmt.Printf("jx: %v\nencoding/json:%v\n", got, exp) |
||||||
|
panic("mismatch") |
||||||
|
} |
||||||
|
return 1 |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue