Skip to content

Commit

Permalink
all: add validate; imp timeutil.Duration
Browse files Browse the repository at this point in the history
  • Loading branch information
ainar-g committed Dec 4, 2024
1 parent 94260d8 commit e4a1b50
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 44 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ BRANCH = $${BRANCH:-$$(git rev-parse --abbrev-ref HEAD)}
GOAMD64 = v1
GOPROXY = https://proxy.golang.org|direct
GOTELEMETRY = off
GOTOOLCHAIN = go1.23.3
GOTOOLCHAIN = go1.23.4
RACE = 0
REVISION = $${REVISION:-$$(git rev-parse --short HEAD)}
VERSION = 0
Expand Down
2 changes: 1 addition & 1 deletion bamboo-specs/bamboo.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# exact patch version as opposed to a minor one to make sure that this exact
# version is actually used and not whatever the docker daemon on the CI has
# cached a few months ago.
'dockerGo': 'adguard/go-builder:1.23.3--1'
'dockerGo': 'adguard/go-builder:1.23.4--1'

'stages':
- 'Go Lint':
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module github.com/AdguardTeam/golibs

go 1.23.3
go 1.23.4

require (
github.com/getsentry/sentry-go v0.29.1
github.com/getsentry/sentry-go v0.30.0
github.com/stretchr/testify v1.10.0
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
golang.org/x/net v0.31.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getsentry/sentry-go v0.29.1 h1:DyZuChN8Hz3ARxGVV8ePaNXh1dQ7d76AiB117xcREwA=
github.com/getsentry/sentry-go v0.29.1/go.mod h1:x3AtIzN01d6SiWkderzaH28Tm0lgkafpJ5Bm3li39O0=
github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
Expand Down
6 changes: 3 additions & 3 deletions internal/tools/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/AdguardTeam/golibs/internal/tools

go 1.23.3
go 1.23.4

require (
github.com/fzipp/gocyclo v0.6.0
Expand Down Expand Up @@ -59,8 +59,8 @@ require (
golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.8.0 // indirect
google.golang.org/api v0.209.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/grpc v1.68.0 // indirect
google.golang.org/protobuf v1.35.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
8 changes: 4 additions & 4 deletions internal/tools/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a h1:OAiGFfOiA0v9MRYsSidp3ubZaBnteRUyn3xB2ZQ5G/E=
google.golang.org/genproto/googleapis/api v0.0.0-20241202173237-19429a94021a/go.mod h1:jehYqy3+AhJU9ve55aNOaSml7wUXjF9x6z2LcCfpAhY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
Expand Down
25 changes: 16 additions & 9 deletions netutil/httputil/logmw.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package httputil

import (
"context"
"log/slog"
"net/http"
"time"
Expand Down Expand Up @@ -65,15 +66,7 @@ func (mw *LogMiddleware) Wrap(h http.Handler) (wrapped http.Handler) {
rw.Reset(w)

l.Log(ctx, mw.lvl, "started")
defer func() {
// TODO(a.garipov): Augment our JSON handler to use
// [time.Duration.String] automatically?
if l.Enabled(ctx, mw.lvl) {
l.Log(ctx, mw.lvl, "finished", "code", rw.code, "elapsed", timeutil.Duration{
Duration: time.Since(startTime),
})
}
}()
defer func() { mw.printFinished(ctx, l, rw.code, time.Since(startTime)) }()

h.ServeHTTP(rw, nextReq)
rw.SetImplicitSuccess()
Expand All @@ -82,6 +75,20 @@ func (mw *LogMiddleware) Wrap(h http.Handler) (wrapped http.Handler) {
return http.HandlerFunc(f)
}

// printFinished is called at the end of handling of a query.
func (mw *LogMiddleware) printFinished(
ctx context.Context,
l *slog.Logger,
code int,
elapsed time.Duration,
) {
if !l.Enabled(ctx, mw.lvl) {
return
}

l.Log(ctx, mw.lvl, "finished", "code", code, "elapsed", timeutil.Duration(elapsed))
}

// attrsSlicePtr returns a pointer to an slice with the attributes from the request
// set. The callers should defer returning the slice back to the pool.
func (mw *LogMiddleware) attrsSlicePtr(r *http.Request) (attrsPtr *[]slog.Attr) {
Expand Down
32 changes: 16 additions & 16 deletions timeutil/duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,16 @@ import (
"encoding"
"fmt"
"time"

"github.com/AdguardTeam/golibs/errors"
)

// Day is the duration of one day.
const Day time.Duration = 24 * time.Hour

// Duration is a wrapper for time.Duration providing functionality for encoding.
type Duration struct {
time.Duration
}
type Duration time.Duration

// type check
var _ fmt.Stringer = Duration{}
var _ fmt.Stringer = Duration(0)

// String implements the [fmt.Stringer] interface for Duration. It wraps
// time.Duration.String method and additionally cuts off non-leading zero values
Expand All @@ -30,7 +26,8 @@ var _ fmt.Stringer = Duration{}
// Duration: "1h", time.Duration: "1h0m0s"
// Duration: "1h1m", time.Duration: "1h1m0s"
func (d Duration) String() (str string) {
str = d.Duration.String()
timeDur := time.Duration(d)
str = timeDur.String()

const (
tailMin = len(`0s`)
Expand All @@ -42,13 +39,13 @@ func (d Duration) String() (str string) {
minsInHour = time.Hour / time.Minute
)

switch rounded := d.Duration / time.Second; {
switch rounded := timeDur / time.Second; {
case
rounded == 0,
rounded*time.Second != d.Duration,
rounded*time.Second != timeDur,
rounded%60 != 0:
// Return the uncut value if it's either equal to zero or has
// fractions of a second or even whole seconds in it.
// Return the uncut value if it's either equal to zero or has fractions
// of a second or even whole seconds in it.
return str
case (rounded%secsInHour)/minsInHour != 0:
return str[:len(str)-tailMin]
Expand All @@ -58,7 +55,7 @@ func (d Duration) String() (str string) {
}

// type check
var _ encoding.TextMarshaler = Duration{}
var _ encoding.TextMarshaler = Duration(0)

// MarshalText implements the [encoding.TextMarshaler] interface for Duration.
func (d Duration) MarshalText() (text []byte, err error) {
Expand All @@ -71,11 +68,14 @@ var _ encoding.TextUnmarshaler = (*Duration)(nil)
// UnmarshalText implements the [encoding.TextUnmarshaler] interface for
// *Duration.
//
// TODO(e.burkov): Make it able to parse larger units like days.
// TODO(e.burkov): Make it able to parse larger units like days.
func (d *Duration) UnmarshalText(b []byte) (err error) {
defer func() { err = errors.Annotate(err, "unmarshaling duration: %w") }()
timeDur, err := time.ParseDuration(string(b))
if err != nil {
return fmt.Errorf("unmarshaling duration: %w", err)
}

d.Duration, err = time.ParseDuration(string(b))
*d = Duration(timeDur)

return err
return nil
}
10 changes: 4 additions & 6 deletions timeutil/duration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestDuration_String(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

d := timeutil.Duration{Duration: tc.val}
d := timeutil.Duration(tc.val)
assert.Equal(t, tc.name, d.String())
})
}
Expand All @@ -63,10 +63,8 @@ func TestDuration_String(t *testing.T) {
func TestDuration_encoding(t *testing.T) {
t.Parallel()

v := &timeutil.Duration{
Duration: time.Millisecond,
}
v := timeutil.Duration(time.Millisecond)

testutil.AssertMarshalText(t, "1ms", v)
testutil.AssertUnmarshalText(t, "1ms", v)
testutil.AssertMarshalText(t, "1ms", &v)
testutil.AssertUnmarshalText(t, "1ms", &v)
}
94 changes: 94 additions & 0 deletions validate/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Package validate contains functions for validating values.
//
// The common argument name can be the name of the JSON or YAML property, the
// name of a function argument, or anything similar.
//
// NOTE: More specific validations, like those of network addresses or URLs,
// should be put into related utility packages.
//
// TODO(a.garipov): Consider adding validate.KeyValues.
package validate

import (
"cmp"
"fmt"

"github.com/AdguardTeam/golibs/errors"
)

// Interface is the interface for configuration entities that can validate
// themselves.
type Interface interface {
// Validate returns an error if the entity isn't valid. Entities should not
// add a prefix; instead, the callers should add prefixes depending on the
// use.
Validate() (err error)
}

// Append validates values, wraps errors with the name and the index, appends
// them to errs, and returns the result.
func Append[T Interface](errs []error, name string, values []T) (res []error) {
res = errs
for i, v := range values {
// TODO(a.garipov): Consider flattening error slices.
err := v.Validate()
if err != nil {
res = append(res, fmt.Errorf("%s: at index %d: %w", name, i, err))
}
}

return res
}

// Slice validates values, wraps errors with the name and the index, and returns
// the result as a single joined error.
func Slice[T Interface](name string, values []T) (err error) {
return errors.Join(Append(nil, name, values)...)
}

// InRange returns an error of v is less than min or greater than max. The
// underlying error of err is [errors.ErrOutOfRange].
func InRange[T cmp.Ordered](name string, v, min, max T) (err error) {
const errRange = errors.ErrOutOfRange
if cmp.Compare(v, min) < 0 {
return fmt.Errorf("%s: %w: must be no less than %v, got %v", name, errRange, min, v)
} else if cmp.Compare(v, max) > 0 {
return fmt.Errorf("%s: %w: must be no greater than %v, got %v", name, errRange, max, v)
}

return nil
}

// NotNegative returns an error if v is less than the zero value of type T. The
// underlying error of err is [errors.ErrNegative].
func NotNegative[T cmp.Ordered](name string, v T) (err error) {
var zero T
if cmp.Compare(v, zero) < 0 {
return fmt.Errorf("%s: %w: %v", name, errors.ErrNegative, v)
}

return nil
}

// NotNil returns an error if v is nil. The underlying error of err is
// [errors.ErrNoValue].
//
// TODO(a.garipov): Find ways of extending to other nilable types.
func NotNil[T any](name string, v *T) (err error) {
if v == nil {
return fmt.Errorf("%s: %w", name, errors.ErrNoValue)
}

return nil
}

// Positive returns an error if v is less than or equal to the zero value of
// type T. The underlying error of err is [errors.ErrNotPositive].
func Positive[T cmp.Ordered](name string, v T) (err error) {
var zero T
if cmp.Compare(v, zero) <= 0 {
return fmt.Errorf("%s: %w: %v", name, errors.ErrNotPositive, v)
}

return nil
}
Loading

0 comments on commit e4a1b50

Please sign in to comment.