diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 3c0189d..73f1d63 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -10,10 +10,10 @@ jobs: uses: actions/checkout@v2 with: ref: ${{ github.head_ref }} - - name: Set up Go 1.16.x + - name: Set up Go 1.22.x uses: actions/setup-go@v1 with: - go-version: 1.16.x + go-version: 1.22.x - uses: actions/cache@v1 with: path: ~/go/pkg/mod diff --git a/api/adapter.go b/api/adapter.go index 07de7b4..84ea000 100644 --- a/api/adapter.go +++ b/api/adapter.go @@ -2,8 +2,8 @@ package api import ( "context" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/logctx" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/logctx" ) // StdContext returns a standard context from an echo context. diff --git a/api/api.go b/api/api.go index d4304ab..131cd3d 100644 --- a/api/api.go +++ b/api/api.go @@ -1,25 +1,26 @@ /* -Package api is a standalone API package/pattern built on echo and logrus. +Package api is a standalone API package/pattern built on echo. It sets up /statusz and /healthz endpoints, and sets up logging middleware that takes care of the following important, and fundamentally (in Go) interconnected tasks: -- Extract (or add) a trace ID header to the request and response. -- The trace ID can be retrieved through api.TraceID(context) of the echo.Context for the request. -- Use that trace ID header as context for the logrus logger. -- Handle request logging (metadata about the request and response, - and log at the level appropriate for the status code). -- The request logger can be retrieved api.Logger(echo.Context). -- Recover from panics. -- Coerce all errors into api.Error types, and marshal them. -- Override echo's HTTPErrorHandler to pass through api.Error types. + - Extract (or add) a trace ID header to the request and response. + - The trace ID can be retrieved through api.TraceID(context) of the echo.Context for the request. + - Use that trace ID header as context for the logger. + - Handle request logging (metadata about the request and response, + and log at the level appropriate for the status code). + - The request logger can be retrieved api.Logger(echo.Context). + - Recover from panics. + - Coerce all errors into api.Error types, and marshal them. + - Override echo's HTTPErrorHandler to pass through api.Error types. */ package api import ( - "github.com/labstack/echo" - "github.com/labstack/echo/middleware" - "github.com/sirupsen/logrus" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/lithictech/go-aperitif/v2/logctx" + "log/slog" "net/http" "os" ) @@ -27,7 +28,7 @@ import ( type Config struct { // If not provided, create an echo.New. App *echo.Echo - Logger *logrus.Entry + Logger *slog.Logger LoggingMiddlwareConfig LoggingMiddlwareConfig // Origins for echo's CORS middleware. // If it and CorsConfig are empty, do not add the middleware. @@ -57,7 +58,7 @@ type Config struct { func New(cfg Config) *echo.Echo { if cfg.Logger == nil { - cfg.Logger = unconfiguredLogger() + cfg.Logger = logctx.UnconfiguredLogger() } if cfg.HealthHandler == nil { if cfg.HealthResponse == nil { diff --git a/api/api_test.go b/api/api_test.go index 6879105..4b643f4 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -2,17 +2,16 @@ package api_test import ( "errors" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/api" - "github.com/lithictech/go-aperitif/api/apiparams" - . "github.com/lithictech/go-aperitif/api/echoapitest" - . "github.com/lithictech/go-aperitif/apitest" - "github.com/lithictech/go-aperitif/logctx" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/api" + "github.com/lithictech/go-aperitif/v2/api/apiparams" + . "github.com/lithictech/go-aperitif/v2/api/echoapitest" + . "github.com/lithictech/go-aperitif/v2/apitest" + "github.com/lithictech/go-aperitif/v2/logctx" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/rgalanakis/golangal" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" + "log/slog" "net/http" "net/http/httptest" "testing" @@ -25,15 +24,14 @@ func TestAPI(t *testing.T) { var _ = Describe("API", func() { var e *echo.Echo - var logger *logrus.Logger - var logHook *test.Hook - var logEntry *logrus.Entry + + var logger *slog.Logger + var logHook *logctx.Hook BeforeEach(func() { - logger, logHook = test.NewNullLogger() - logEntry = logger.WithFields(nil) + logger, logHook = logctx.NewNullLogger() e = api.New(api.Config{ - Logger: logEntry, + Logger: logger, HealthResponse: map[string]interface{}{"o": "k"}, StatusResponse: map[string]interface{}{"it": "me"}, }) @@ -48,7 +46,7 @@ var _ = Describe("API", func() { It("can use custom health and status fields", func() { e = api.New(api.Config{ - Logger: logEntry, + Logger: logger, HealthHandler: func(c echo.Context) error { return c.String(200, "yo") }, @@ -117,58 +115,56 @@ var _ = Describe("API", func() { Describe("logging", func() { It("does not corrupt the input logger (by reassigning the closure)", func() { e.GET("/before-first-call", func(c echo.Context) error { - Expect(api.Logger(c).Data).ToNot(HaveKey("request_status")) + Expect(api.Logger(c).Handler().(*logctx.Hook).AttrMap()).ToNot(HaveKey("request_status")) return c.String(401, "ok") }) e.GET("/after-first-call", func(c echo.Context) error { - Expect(api.Logger(c).Data).ToNot(HaveKey("request_status")) + Expect(api.Logger(c).Handler().(*logctx.Hook).AttrMap()).ToNot(HaveKey("request_status")) return c.String(403, "ok") }) Expect(Serve(e, GetRequest("/before-first-call"))).To(HaveResponseCode(401)) Expect(Serve(e, GetRequest("/after-first-call"))).To(HaveResponseCode(403)) - Expect(logHook.Entries).To(HaveLen(2)) + Expect(logHook.Records()).To(HaveLen(2)) }) It("logs normal requests at info", func() { e.GET("/", func(c echo.Context) error { return c.String(200, "ok") }) Expect(Serve(e, GetRequest("/"))).To(HaveResponseCode(200)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Level).To(Equal(logrus.InfoLevel)) + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].Record.Level).To(Equal(slog.LevelInfo)) }) It("logs 500+ at error", func() { e.GET("/", func(c echo.Context) error { return c.String(500, "oh") }) Expect(Serve(e, GetRequest("/"))).To(HaveResponseCode(500)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Level).To(Equal(logrus.ErrorLevel)) + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].Record.Level).To(Equal(slog.LevelError)) }) It("logs 400 to 499 as warn", func() { e.GET("/", func(c echo.Context) error { return c.String(400, "client err") }) Expect(Serve(e, GetRequest("/"))).To(HaveResponseCode(400)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Level).To(Equal(logrus.WarnLevel)) + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].Record.Level).To(Equal(slog.LevelWarn)) }) It("logs status and health as debug", func() { - logger.SetLevel(logrus.DebugLevel) Expect(Serve(e, GetRequest("/healthz"))).To(HaveResponseCode(200)) Expect(Serve(e, GetRequest("/statusz"))).To(HaveResponseCode(200)) - Expect(logHook.Entries).To(HaveLen(2)) - Expect(logHook.Entries[0].Level).To(Equal(logrus.DebugLevel)) - Expect(logHook.Entries[1].Level).To(Equal(logrus.DebugLevel)) + Expect(logHook.Records()).To(HaveLen(2)) + Expect(logHook.Records()[0].Record.Level).To(Equal(slog.LevelDebug)) + Expect(logHook.Records()[1].Record.Level).To(Equal(slog.LevelDebug)) }) It("logs options as debug", func() { - logger.SetLevel(logrus.DebugLevel) Expect(Serve(e, NewRequest("OPTIONS", "/foo", nil))).To(HaveResponseCode(404)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Level).To(Equal(logrus.DebugLevel)) + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].Record.Level).To(Equal(slog.LevelDebug)) }) It("can log request and response headers", func() { e = api.New(api.Config{ - Logger: logEntry, + Logger: logger, LoggingMiddlwareConfig: api.LoggingMiddlwareConfig{ RequestHeaders: true, ResponseHeaders: true, @@ -179,8 +175,8 @@ var _ = Describe("API", func() { return c.String(200, "ok") }) Expect(Serve(e, GetRequest("/", SetReqHeader("ReqHead", "ReqHeadVal")))).To(HaveResponseCode(200)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Data).To(And( + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].AttrMap()).To(And( HaveKeyWithValue("request_header.Reqhead", "ReqHeadVal"), HaveKeyWithValue("response_header.Reshead", "ResHeadVal"), )) @@ -188,15 +184,15 @@ var _ = Describe("API", func() { It("can use custom DoLog, BeforeRequest, and AfterRequest hooks", func() { doLogCalled := false e = api.New(api.Config{ - Logger: logEntry, + Logger: logger, LoggingMiddlwareConfig: api.LoggingMiddlwareConfig{ - BeforeRequest: func(_ echo.Context, e *logrus.Entry) *logrus.Entry { - return e.WithField("before", 1) + BeforeRequest: func(_ echo.Context, e *slog.Logger) *slog.Logger { + return e.With("before", 1) }, - AfterRequest: func(_ echo.Context, e *logrus.Entry) *logrus.Entry { - return e.WithField("after", 2) + AfterRequest: func(_ echo.Context, e *slog.Logger) *slog.Logger { + return e.With("after", 2) }, - DoLog: func(c echo.Context, e *logrus.Entry) { + DoLog: func(c echo.Context, e *slog.Logger) { doLogCalled = true api.LoggingMiddlewareDefaultDoLog(c, e) }, @@ -207,9 +203,9 @@ var _ = Describe("API", func() { }) Expect(Serve(e, GetRequest("/"))).To(HaveResponseCode(400)) Expect(doLogCalled).To(BeTrue()) - Expect(logHook.Entries[len(logHook.Entries)-1].Data).To(And( - HaveKeyWithValue("before", 1), - HaveKeyWithValue("after", 2), + Expect(logHook.LastRecord().AttrMap()).To(And( + HaveKeyWithValue("before", BeEquivalentTo(1)), + HaveKeyWithValue("after", BeEquivalentTo(2)), )) }) }) @@ -284,7 +280,8 @@ var _ = Describe("API", func() { r, err := http.NewRequest("GET", "", nil) Expect(err).ToNot(HaveOccurred()) ctx := e.NewContext(r, httptest.NewRecorder()) - logger := logrus.New().WithField("a", 2) + logger, _ := logctx.NewNullLogger() + logger = logger.With("a", 2) api.SetLogger(ctx, logger) tid := api.TraceId(ctx) @@ -292,8 +289,8 @@ var _ = Describe("API", func() { tkey, tval := logctx.ActiveTraceId(c) Expect(tkey).To(Equal(logctx.RequestTraceIdKey)) Expect(tval).To(Equal(tid)) - Expect(logctx.Logger(c).Data).To(And( - HaveKeyWithValue("a", 2), + Expect(logctx.Logger(c).Handler().(*logctx.Hook).AttrMap()).To(And( + HaveKeyWithValue("a", BeEquivalentTo(2)), HaveKeyWithValue(BeEquivalentTo(logctx.RequestTraceIdKey), tid), )) }) @@ -320,17 +317,14 @@ var _ = Describe("API", func() { }) Describe("DebugMiddleware", func() { - BeforeEach(func() { - logger.SetLevel(logrus.DebugLevel) - }) It("noops if not enabled", func() { e.Use(api.DebugMiddleware(api.DebugMiddlewareConfig{Enabled: false, DumpResponseBody: true})) e.GET("/foo", func(c echo.Context) error { return c.String(200, "ok") }) Serve(e, NewRequest("POST", "/endpoint", nil)) - Expect(logHook.Entries).To(HaveLen(1)) - Expect(logHook.Entries[0].Message).To(Equal("request_finished")) + Expect(logHook.Records()).To(HaveLen(1)) + Expect(logHook.Records()[0].Record.Message).To(Equal("request_finished")) }) It("dumps what is enabled", func() { e.Use(api.DebugMiddleware(api.DebugMiddlewareConfig{Enabled: true, DumpResponseBody: true, DumpResponseHeaders: true})) @@ -338,9 +332,9 @@ var _ = Describe("API", func() { return c.String(200, "ok") }) Serve(e, NewRequest("GET", "/endpoint", nil)) - Expect(logHook.Entries).To(HaveLen(2)) - Expect(logHook.Entries[0].Message).To(Equal("request_debug")) - Expect(logHook.Entries[0].Data).To(And( + Expect(logHook.Records()).To(HaveLen(2)) + Expect(logHook.Records()[0].Record.Message).To(Equal("request_debug")) + Expect(logHook.Records()[0].AttrMap()).To(And( HaveKeyWithValue("debug_response_headers", HaveKey("Content-Type")), HaveKeyWithValue("debug_response_body", ContainSubstring("ok")), )) @@ -351,9 +345,9 @@ var _ = Describe("API", func() { return c.String(200, "ok") }) Serve(e, NewRequest("GET", "/endpoint", nil, SetReqHeader("Foo", "x"))) - Expect(logHook.Entries).To(HaveLen(2)) - Expect(logHook.Entries[0].Message).To(Equal("request_debug")) - Expect(logHook.Entries[0].Data).To(And( + Expect(logHook.Records()).To(HaveLen(2)) + Expect(logHook.Records()[0].Record.Message).To(Equal("request_debug")) + Expect(logHook.Records()[0].AttrMap()).To(And( HaveKeyWithValue("debug_request_headers", HaveKey("Foo")), HaveKeyWithValue("debug_response_headers", HaveKey("Content-Type")), HaveKeyWithValue("debug_request_body", ""), @@ -367,13 +361,13 @@ var _ = Describe("API", func() { }) Serve(e, NewRequest("GET", "/endpoint", nil, SetReqHeader("Foo", "x"))) Serve(e, NewRequest("GET", "/endpoint", nil, SetReqHeader("Foo", "x"))) - Expect(logHook.Entries).To(HaveLen(4)) - Expect(logHook.Entries[0].Message).To(Equal("request_debug")) - Expect(logHook.Entries[0].Data).ToNot(HaveKey("memory_sys")) - Expect(logHook.Entries[1].Message).To(Equal("request_finished")) - Expect(logHook.Entries[2].Message).To(Equal("request_debug")) - Expect(logHook.Entries[2].Data).To(HaveKey("memory_sys")) - Expect(logHook.Entries[3].Message).To(Equal("request_finished")) + Expect(logHook.Records()).To(HaveLen(4)) + Expect(logHook.Records()[0].Record.Message).To(Equal("request_debug")) + Expect(logHook.Records()[0].AttrMap()).ToNot(HaveKey("memory_sys")) + Expect(logHook.Records()[1].Record.Message).To(Equal("request_finished")) + Expect(logHook.Records()[2].Record.Message).To(Equal("request_debug")) + Expect(logHook.Records()[2].AttrMap()).To(HaveKey("memory_sys")) + Expect(logHook.Records()[3].Record.Message).To(Equal("request_finished")) }) }) }) diff --git a/api/apiparams/apiparams.go b/api/apiparams/apiparams.go index 415728c..da51234 100644 --- a/api/apiparams/apiparams.go +++ b/api/apiparams/apiparams.go @@ -2,7 +2,7 @@ package apiparams import ( "fmt" - "github.com/lithictech/go-aperitif/validator" + "github.com/lithictech/go-aperitif/v2/validator" "net/http" "reflect" "time" diff --git a/api/apiparams/apiparams_test.go b/api/apiparams/apiparams_test.go index d390298..1f08567 100644 --- a/api/apiparams/apiparams_test.go +++ b/api/apiparams/apiparams_test.go @@ -2,10 +2,10 @@ package apiparams_test import ( "fmt" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/api/apiparams" - . "github.com/lithictech/go-aperitif/api/echoapitest" - . "github.com/lithictech/go-aperitif/apitest" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/api/apiparams" + . "github.com/lithictech/go-aperitif/v2/api/echoapitest" + . "github.com/lithictech/go-aperitif/v2/apitest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/rgalanakis/golangal" diff --git a/api/apiparams/benchmark_test.go b/api/apiparams/benchmark_test.go index 2ee6d56..8caa3cd 100644 --- a/api/apiparams/benchmark_test.go +++ b/api/apiparams/benchmark_test.go @@ -2,8 +2,8 @@ package apiparams_test import ( "encoding/json" - "github.com/lithictech/go-aperitif/api/apiparams" - "github.com/lithictech/go-aperitif/convext" + "github.com/lithictech/go-aperitif/v2/api/apiparams" + "github.com/lithictech/go-aperitif/v2/convext" "net/http" "strings" "testing" diff --git a/api/apiparams/doc.go b/api/apiparams/doc.go index 41cef2f..a489645 100644 --- a/api/apiparams/doc.go +++ b/api/apiparams/doc.go @@ -34,24 +34,24 @@ For example, consider the following test: Note all the benefits: -- Data is pulled from path parameters, query parameters, any JSON body, - and defaults defined in struct tags. The variable names used for values - is specified via the appropriate struct tag. - See ParamSource for more details, but possible tags are "path", "query", "header", "form", and "json". - The "json" tag will bind from any source, not just a JSON request body. - This makes it clear at the endpoint and model definitions where data comes from and - how an endpoint is supposed to be called. -- Path and query param coercion is done from the basic JSON types, - depending on the struct field type (int/float, string, bool). -- Validation is done using the validator package. - Custom validators can be registered as we need to express more - sophisticated validations. - -Validations + - Data is pulled from path parameters, query parameters, any JSON body, + and defaults defined in struct tags. The variable names used for values + is specified via the appropriate struct tag. + See ParamSource for more details, but possible tags are "path", "query", "header", "form", and "json". + The "json" tag will bind from any source, not just a JSON request body. + This makes it clear at the endpoint and model definitions where data comes from and + how an endpoint is supposed to be called. + - Path and query param coercion is done from the basic JSON types, + depending on the struct field type (int/float, string, bool). + - Validation is done using the validator package. + Custom validators can be registered as we need to express more + sophisticated validations. + +# Validations See validator for a list of available validators and usage examples. -Adapters +# Adapters The only non-obvious prerequisite to using apiparams.BindAndValidate is to create a apiparams.Adapter for your HTTP framework of choice. @@ -125,7 +125,7 @@ chi pulls data out of there to figure out a URL Param, like when chi.URLParam is Note again that in general only one of these need to be defined and once per-project (or you can put them into a library, whatever floats your boat). -Errors +# Errors apiparams.BindAndValidate returns a apiparams.HTTPError. Nil result means no error. The HTTPError can be one of various error codes (415, 422, 400, 500) @@ -136,7 +136,7 @@ parseable-but-invalid value (like a too-high number), or malformed JSON. Callers should wrap the result in the appropriate error for their framework, or can write the Code and Message to the HTTP response. -Custom Types +# Custom Types Custom types can be used in an API by providing a CustomTypeDef and passing it to RegisterCustomType. A CustomTypeDef consists of a _defaulter_ and a _parser_. diff --git a/api/apiparams/paramfield.go b/api/apiparams/paramfield.go index ca1b0ab..1aacf41 100644 --- a/api/apiparams/paramfield.go +++ b/api/apiparams/paramfield.go @@ -7,7 +7,9 @@ import ( // ParamSource is a struct tag name that can define where a field is set by. // For example, a field of: -// Wibble string `path:"wibble"` +// +// Wibble string `path:"wibble"` +// // would be said to have a Source of "path". // In general, fields can only be set from their parameter source, // so that the Wibble field can only be set from the path and not a query parameter. @@ -36,7 +38,7 @@ var AllParamSources = []ParamSource{ // whether via query, path, header, or json/body parameters. // For a struct field of: // -// Field string `header:"x-my-field"` +// Field string `header:"x-my-field"` // // - Name is "x-my-field" // - Source is "header" @@ -51,7 +53,9 @@ type paramField struct { // that indicates how the parameter is supposed to be set: its Source (header, query, path, json) // the Name used to set the parameter, and a reference back to the parsed StructField. // This means parsing the struct field: -// Field string `query:"pretty"` +// +// Field string `query:"pretty"` +// // would return a paramField with a Source of "query" and Name of "pretty". // This also resolves json field naming rules (like `query:"-"` indicating not to set the field). // If no paramField can be parsed (it has no tags, or the tags indicate not to export the field), diff --git a/api/apiparams/reflector.go b/api/apiparams/reflector.go index 7667526..30f012b 100644 --- a/api/apiparams/reflector.go +++ b/api/apiparams/reflector.go @@ -114,9 +114,9 @@ func (f *fieldMapper) mapAndFlushRun() { // mapping the reflect.StructField to the name we should expect // it to be called in parameters. In other words, this struct: // -// type Params struct { -// Foo string `json:"foo"` -// } +// type Params struct { +// Foo string `json:"foo"` +// } // // would set a map of // {"foo": } for paramFieldsByJsonName and @@ -126,13 +126,13 @@ func (f *fieldMapper) mapAndFlushRun() { // // For nested params, the mapping is still flat. For example, this struct: // -// type Params struct { -// []struct { -// A string `json:"a"` -// } -// Nest struct { -// B int `json:"b"` -// } `json:"nest"` +// type Params struct { +// []struct { +// A string `json:"a"` +// } +// Nest struct { +// B int `json:"b"` +// } `json:"nest"` // // would set a map of // {"a": , "nest":, "b":} for paramFieldsByJsonName and @@ -143,8 +143,8 @@ func (f *fieldMapper) mapAndFlushRun() { // Since path/query params are a flat list of key-value pairs, we don't need // deep parameters from the struct. // - Mapping validation field errors (like "Foo" or "Nest[0].B" to JSON names. -// The only alternative is to map names back after the fact, -// or write yet-another-validator that is consistent with the way we parse names +// The only alternative is to map names back after the fact, +// or write yet-another-validator that is consistent with the way we parse names // from struct tags. // See the MapFieldNameToParamName method doc for more details on how this works. func (r reflector) parseStructTags(underlyingType reflect.Type) { @@ -183,8 +183,8 @@ func (r reflector) parseStructTags(underlyingType reflect.Type) { // or because a field is being set of a type that isn't supported. // For the latter case, imagine a struct with: // -// D time.Time `json:"d"` -// Foo MyFooType `json:"foo"` +// D time.Time `json:"d"` +// Foo MyFooType `json:"foo"` // // time.Time is a supported type, so would be fine, but MyFooType // is not a supported type so this code would panic. @@ -202,11 +202,11 @@ func (r reflector) setField(fieldDef reflect.StructField, field reflect.Value, v // parseValue parses a string value into a reflect.Value that can be set via reflection. // -// - t is the reflect.Type of the field that the value will be parsed into, -// such as a basic type like string or int, a slice type like []string or []int, or a struct type. -// - field is the reflect.Value of the existing struct field- -// this is only used for slice types, which need to append to the field. -// - value is the string value to parse. +// - t is the reflect.Type of the field that the value will be parsed into, +// such as a basic type like string or int, a slice type like []string or []int, or a struct type. +// - field is the reflect.Value of the existing struct field- +// this is only used for slice types, which need to append to the field. +// - value is the string value to parse. // // This is verbose, if generally straightforward. // If t is not a pointer type, the reflect.Value returned points to the new field value. diff --git a/api/auth0jwt/auth0jwt.go b/api/auth0jwt/auth0jwt.go deleted file mode 100644 index 1e19f1b..0000000 --- a/api/auth0jwt/auth0jwt.go +++ /dev/null @@ -1,139 +0,0 @@ -// Package auth0jwt is a modification of the Auth0 provided Go tutorial: -// https://auth0.com/docs/quickstart/backend/golang -// As you may guess that has several issues, but a lot of what's here has been taken verbatim. -package auth0jwt - -import ( - "crypto/subtle" - "encoding/json" - "errors" - "github.com/auth0/go-jwt-middleware" - "github.com/dgrijalva/jwt-go" - "github.com/labstack/echo" - "net/http" -) - -type Jwks struct { - Keys []JSONWebKeys `json:"keys"` -} - -type JSONWebKeys struct { - Kty string `json:"kty"` - Kid string `json:"kid"` - Use string `json:"use"` - N string `json:"n"` - E string `json:"e"` - X5c []string `json:"x5c"` -} - -type Config struct { - // Aud is used to veirfy the 'aud' claim. It's the identifier of the API in Auth0. - Aud string - // Iss is used to verify the 'iss' claim. - Iss string - // JwksPath is the path to the file like "https://my-application.auth0.com/.well-known/jwks.json". - // See https://auth0.com/docs/tokens/concepts/jwks - JwksPath string -} - -func NewMiddleware(cfg Config) echo.MiddlewareFunc { - mw := jwtmiddleware.New(jwtmiddleware.Options{ - ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { - checkAud := verifyArrayAudience(token.Claims.(jwt.MapClaims), cfg.Aud, true) - if !checkAud { - return token, echo.NewHTTPError(401, "invalid audience") - } - checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(cfg.Iss, true) - if !checkIss { - return token, echo.NewHTTPError(401, "invalid issuer") - } - - cert, err := getPemCert(cfg.JwksPath, token) - if err != nil { - return nil, err - } - - result, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(cert)) - return result, nil - }, - UserProperty: "user", - CredentialsOptional: false, - Debug: false, - }) - - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - req := c.Request() - // req gets stomped by CheckJWT, to have a new context - err := mw.CheckJWT(c.Response().Writer, req) - if err != nil { - return err - } - user := req.Context().Value(mw.Options.UserProperty) - if user == nil { - panic("why is 'user' nil in jwt context!?") - } - c.Set(mw.Options.UserProperty, user) - return next(c) - } - } -} - -func getPemCert(jwksPath string, token *jwt.Token) (string, error) { - cert := "" - resp, err := http.Get(jwksPath) - - if err != nil { - return cert, err - } - defer resp.Body.Close() - - var jwks = Jwks{} - err = json.NewDecoder(resp.Body).Decode(&jwks) - - if err != nil { - return cert, err - } - - for k := range jwks.Keys { - if token.Header["kid"] == jwks.Keys[k].Kid { - cert = "-----BEGIN CERTIFICATE-----\n" + jwks.Keys[k].X5c[0] + "\n-----END CERTIFICATE-----" - } - } - - if cert == "" { - err := errors.New("unable to find appropriate key") - return cert, err - } - - return cert, nil -} - -// Seehttps://github.com/dgrijalva/jwt-go/pull/308 -// These two methods are straight copy paste -func verifyArrayAudience(m jwt.MapClaims, cmp string, req bool) bool { - switch m["aud"].(type) { - case string: - aud := m["aud"].(string) - return verifyAudHelper(aud, cmp, req) - default: - auds := m["aud"].([]interface{}) - for _, aud := range auds { - if verifyAudHelper(aud.(string), cmp, req) { - return true - } - } - return false - } -} - -func verifyAudHelper(aud string, cmp string, required bool) bool { - if aud == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { - return true - } else { - return false - } -} diff --git a/api/auth0jwt/auth0jwt_test.go b/api/auth0jwt/auth0jwt_test.go deleted file mode 100644 index d5eca98..0000000 --- a/api/auth0jwt/auth0jwt_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package auth0jwt_test - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "testing" -) - -func TestAuth0Jwt(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "API Suite") -} - -var _ = Describe("auth0jwt", func() { - //var e *echo.Echo - - BeforeEach(func() { - //e = echo.New() - }) - - It("validates against iss", func() { - }) - It("validates against aud string", func() { - }) - It("validates against aud array", func() { - }) - It("adds the user to the echo context", func() { - }) - It("validates against the PEM cert", func() {}) -}) diff --git a/api/cache_control.go b/api/cache_control.go index cfe9068..1c502dd 100644 --- a/api/cache_control.go +++ b/api/cache_control.go @@ -1,7 +1,7 @@ package api import ( - "github.com/labstack/echo" + "github.com/labstack/echo/v4" ) func WithCacheControl(enabled bool, value string) echo.MiddlewareFunc { diff --git a/api/debug.go b/api/debug.go index 312517e..682c5b0 100644 --- a/api/debug.go +++ b/api/debug.go @@ -1,9 +1,9 @@ package api import ( - "github.com/labstack/echo" - "github.com/labstack/echo/middleware" - "github.com/lithictech/go-aperitif/logctx" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/lithictech/go-aperitif/v2/logctx" "net/http" "runtime" "sync/atomic" @@ -41,40 +41,40 @@ func DebugMiddleware(cfg DebugMiddlewareConfig) echo.MiddlewareFunc { atomic.AddUint64(&requestCounter, 1) log := logctx.Logger(StdContext(c)) if cfg.DumpRequestBody { - log = log.WithField("debug_request_body", string(reqBody)) + log = log.With("debug_request_body", string(reqBody)) } if cfg.DumpResponseBody { - log = log.WithField("debug_response_body", string(resBody)) + log = log.With("debug_response_body", string(resBody)) } if cfg.DumpRequestHeaders { - log = log.WithField("debug_request_headers", headerToMap(c.Request().Header)) + log = log.With("debug_request_headers", headerToMap(c.Request().Header)) } if cfg.DumpResponseHeaders { - log = log.WithField("debug_response_headers", headerToMap(c.Response().Header())) + log = log.With("debug_response_headers", headerToMap(c.Response().Header())) } if cfg.DumpMemoryEvery > 0 && (requestCounter%dumpEveryUint) == 0 { var ms runtime.MemStats runtime.ReadMemStats(&ms) - log = log.WithFields(map[string]interface{}{ - "memory_alloc": ms.Alloc, - "memory_total_alloc": ms.TotalAlloc, - "memory_sys": ms.Sys, - "memory_mallocs": ms.Mallocs, - "memory_frees": ms.Frees, - "memory_heap_alloc": ms.HeapAlloc, - "memory_heap_sys": ms.HeapSys, - "memory_heap_idle": ms.HeapIdle, - "memory_heap_inuse": ms.HeapInuse, - "memory_heap_released": ms.HeapReleased, - "memory_heap_objects": ms.HeapObjects, - "memory_stack_inuse": ms.StackInuse, - "memory_stack_sys": ms.StackSys, - "memory_other_sys": ms.OtherSys, - "memory_next_gc": ms.NextGC, - "memory_last_gc": ms.LastGC, - "memory_pause_total_ns": ms.PauseTotalNs, - "memory_num_gc": ms.NumGC, - }) + log = log.With( + "memory_alloc", ms.Alloc, + "memory_total_alloc", ms.TotalAlloc, + "memory_sys", ms.Sys, + "memory_mallocs", ms.Mallocs, + "memory_frees", ms.Frees, + "memory_heap_alloc", ms.HeapAlloc, + "memory_heap_sys", ms.HeapSys, + "memory_heap_idle", ms.HeapIdle, + "memory_heap_inuse", ms.HeapInuse, + "memory_heap_released", ms.HeapReleased, + "memory_heap_objects", ms.HeapObjects, + "memory_stack_inuse", ms.StackInuse, + "memory_stack_sys", ms.StackSys, + "memory_other_sys", ms.OtherSys, + "memory_next_gc", ms.NextGC, + "memory_last_gc", ms.LastGC, + "memory_pause_total_ns", ms.PauseTotalNs, + "memory_num_gc", ms.NumGC, + ) } log.Debug("request_debug") }) diff --git a/api/echoapitest/echoapitest.go b/api/echoapitest/echoapitest.go index 45d5ffb..18cab8e 100644 --- a/api/echoapitest/echoapitest.go +++ b/api/echoapitest/echoapitest.go @@ -1,7 +1,7 @@ package echoapitest import ( - "github.com/labstack/echo" + "github.com/labstack/echo/v4" "net/http" "net/http/httptest" ) diff --git a/api/logging.go b/api/logging.go index 65ac53b..dab2054 100644 --- a/api/logging.go +++ b/api/logging.go @@ -2,30 +2,26 @@ package api import ( "fmt" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/api/apiparams" - "github.com/lithictech/go-aperitif/logctx" - "github.com/sirupsen/logrus" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/api/apiparams" + "github.com/lithictech/go-aperitif/v2/logctx" + "log/slog" "net/http" "runtime" "strconv" "time" ) -func unconfiguredLogger() *logrus.Entry { - return logrus.New().WithField("unconfigured_logger", "true") -} - -func Logger(c echo.Context) *logrus.Entry { - logger, ok := c.Get(logctx.LoggerKey).(*logrus.Entry) +func Logger(c echo.Context) *slog.Logger { + logger, ok := c.Get(logctx.LoggerKey).(*slog.Logger) if !ok { - logger = unconfiguredLogger() + logger = logctx.UnconfiguredLogger() logger.Error("No logger configured for request!") } return logger } -func SetLogger(c echo.Context, logger *logrus.Entry) { +func SetLogger(c echo.Context, logger *slog.Logger) { c.Set(logctx.LoggerKey, logger) } @@ -37,20 +33,20 @@ type LoggingMiddlwareConfig struct { // If provided, the returned logger is stored in the context // which is eventually passed to the handler. // Use to add additional fields to the logger based on the request. - BeforeRequest func(echo.Context, *logrus.Entry) *logrus.Entry + BeforeRequest func(echo.Context, *slog.Logger) *slog.Logger // If provided, the returned logger is used for response logging. // Use to add additional fields to the logger based on the request or response. - AfterRequest func(echo.Context, *logrus.Entry) *logrus.Entry + AfterRequest func(echo.Context, *slog.Logger) *slog.Logger // The function that does the actual logging. // By default, it will log at a certain level based on the status code of the response. - DoLog func(echo.Context, *logrus.Entry) + DoLog func(echo.Context, *slog.Logger) } -func LoggingMiddleware(outerLogger *logrus.Entry) echo.MiddlewareFunc { +func LoggingMiddleware(outerLogger *slog.Logger) echo.MiddlewareFunc { return LoggingMiddlewareWithConfig(outerLogger, LoggingMiddlwareConfig{}) } -func LoggingMiddlewareWithConfig(outerLogger *logrus.Entry, cfg LoggingMiddlwareConfig) echo.MiddlewareFunc { +func LoggingMiddlewareWithConfig(outerLogger *slog.Logger, cfg LoggingMiddlwareConfig) echo.MiddlewareFunc { if cfg.DoLog == nil { cfg.DoLog = LoggingMiddlewareDefaultDoLog } @@ -67,24 +63,24 @@ func LoggingMiddlewareWithConfig(outerLogger *logrus.Entry, cfg LoggingMiddlware bytesIn = "0" } - logger := outerLogger.WithFields(logrus.Fields{ - "request_started_at": start.Format(time.RFC3339), - "request_remote_ip": c.RealIP(), - "request_method": req.Method, - "request_uri": req.RequestURI, - "request_protocol": req.Proto, - "request_host": req.Host, - "request_path": path, - "request_query": req.URL.RawQuery, - "request_referer": req.Referer(), - "request_user_agent": req.UserAgent(), - "request_bytes_in": bytesIn, - string(logctx.RequestTraceIdKey): TraceId(c), - }) + logger := outerLogger.With( + "request_started_at", start.Format(time.RFC3339), + "request_remote_ip", c.RealIP(), + "request_method", req.Method, + "request_uri", req.RequestURI, + "request_protocol", req.Proto, + "request_host", req.Host, + "request_path", path, + "request_query", req.URL.RawQuery, + "request_referer", req.Referer(), + "request_user_agent", req.UserAgent(), + "request_bytes_in", bytesIn, + string(logctx.RequestTraceIdKey), TraceId(c), + ) if cfg.RequestHeaders { for k, v := range req.Header { if len(v) > 0 && k != "Authorization" && k != "Cookie" { - logger = logger.WithField("request_header."+k, v[0]) + logger = logger.With("request_header."+k, v[0]) } } } @@ -103,21 +99,21 @@ func LoggingMiddlewareWithConfig(outerLogger *logrus.Entry, cfg LoggingMiddlware stop := time.Now() res := c.Response() - logger = Logger(c).WithFields(logrus.Fields{ - "request_finished_at": stop.Format(time.RFC3339), - "request_status": res.Status, - "request_latency_ms": int(stop.Sub(start)) / 1000 / 1000, - "request_bytes_out": strconv.FormatInt(res.Size, 10), - }) + logger = Logger(c).With( + "request_finished_at", stop.Format(time.RFC3339), + "request_status", res.Status, + "request_latency_ms", int(stop.Sub(start))/1000/1000, + "request_bytes_out", strconv.FormatInt(res.Size, 10), + ) if cfg.ResponseHeaders { for k, v := range res.Header() { if len(v) > 0 && k != "Set-Cookie" { - logger = logger.WithField("response_header."+k, v[0]) + logger = logger.With("response_header."+k, v[0]) } } } if err != nil { - logger = logger.WithField("request_error", err) + logger = logger.With("request_error", err) } if cfg.BeforeRequest != nil { logger = cfg.AfterRequest(c, logger) @@ -129,7 +125,7 @@ func LoggingMiddlewareWithConfig(outerLogger *logrus.Entry, cfg LoggingMiddlware } } -func LoggingMiddlewareDefaultDoLog(c echo.Context, logger *logrus.Entry) { +func LoggingMiddlewareDefaultDoLog(c echo.Context, logger *slog.Logger) { req := c.Request() res := c.Response() logMethod := logger.Info @@ -149,7 +145,7 @@ func LoggingMiddlewareDefaultDoLog(c echo.Context, logger *logrus.Entry) { // so that if it panics, we can recover from it and pass on a 500. // Use the "named return parameter can be set in defer" trick so we can // return the error we create from the panic. -func safeInvokeNext(logger *logrus.Entry, next echo.HandlerFunc, c echo.Context) (err error) { +func safeInvokeNext(logger *slog.Logger, next echo.HandlerFunc, c echo.Context) (err error) { defer func() { if r := recover(); r != nil { if e, ok := r.(error); ok { @@ -159,10 +155,10 @@ func safeInvokeNext(logger *logrus.Entry, next echo.HandlerFunc, c echo.Context) } stack := make([]byte, 4<<10) // 4kb length := runtime.Stack(stack, true) - logger.WithFields(logrus.Fields{ - "error": err, - "stack": string(stack[:length]), - }).Error("panic_recover") + logger.With( + "error", err, + "stack", string(stack[:length]), + ).Error("panic_recover") } }() err = next(c) @@ -189,12 +185,6 @@ func adaptToError(e error) error { return NewInternalError(e) } -// Deprecated: Use NewHTTPErrorHandler instead. -func HTTPErrorHandler(err error, c echo.Context) { - e := echo.New() - NewHTTPErrorHandler(e)(err, c) -} - func NewHTTPErrorHandler(e *echo.Echo) echo.HTTPErrorHandler { return func(err error, c echo.Context) { apiErr, ok := err.(Error) @@ -211,7 +201,7 @@ func NewHTTPErrorHandler(e *echo.Echo) echo.HTTPErrorHandler { err = c.JSON(apiErr.HTTPStatus, apiErr) } if err != nil { - Logger(c).WithField("error", err).Error("http_error_handler_error") + Logger(c).With("error", err).Error("http_error_handler_error") } } } diff --git a/api/preflight/preflight.go b/api/preflight/preflight.go index 4f174ac..fdf05f5 100644 --- a/api/preflight/preflight.go +++ b/api/preflight/preflight.go @@ -1,7 +1,7 @@ package preflight import ( - "github.com/labstack/echo" + "github.com/labstack/echo/v4" "github.com/pkg/errors" "time" ) diff --git a/api/preflight/preflight_test.go b/api/preflight/preflight_test.go index f3a8858..e973c09 100644 --- a/api/preflight/preflight_test.go +++ b/api/preflight/preflight_test.go @@ -2,11 +2,11 @@ package preflight_test import ( "errors" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/api" - . "github.com/lithictech/go-aperitif/api/echoapitest" - "github.com/lithictech/go-aperitif/api/preflight" - . "github.com/lithictech/go-aperitif/apitest" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/api" + . "github.com/lithictech/go-aperitif/v2/api/echoapitest" + "github.com/lithictech/go-aperitif/v2/api/preflight" + . "github.com/lithictech/go-aperitif/v2/apitest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/rgalanakis/golangal" diff --git a/api/spa/spa.go b/api/spa/spa.go index ede7b5e..b232f1d 100644 --- a/api/spa/spa.go +++ b/api/spa/spa.go @@ -6,7 +6,7 @@ package spa import ( - "github.com/labstack/echo" + "github.com/labstack/echo/v4" "log" "net/http" ) diff --git a/api/spa/spa_test.go b/api/spa/spa_test.go index db55a39..ffe6042 100644 --- a/api/spa/spa_test.go +++ b/api/spa/spa_test.go @@ -1,10 +1,10 @@ package spa_test import ( - "github.com/labstack/echo" - . "github.com/lithictech/go-aperitif/api/echoapitest" - "github.com/lithictech/go-aperitif/api/spa" - . "github.com/lithictech/go-aperitif/apitest" + "github.com/labstack/echo/v4" + . "github.com/lithictech/go-aperitif/v2/api/echoapitest" + "github.com/lithictech/go-aperitif/v2/api/spa" + . "github.com/lithictech/go-aperitif/v2/apitest" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/rgalanakis/golangal" diff --git a/api/sqlw/dblog.go b/api/sqlw/dblog.go deleted file mode 100644 index 99ff60d..0000000 --- a/api/sqlw/dblog.go +++ /dev/null @@ -1,92 +0,0 @@ -package sqlw - -import ( - "context" - "database/sql" - "github.com/lithictech/go-aperitif/logctx" - - "github.com/jmoiron/sqlx" - "github.com/sirupsen/logrus" -) - -// WithLogging adds logging around all calls. -func WithLogging(db Interface, defaultLogger *logrus.Entry) Interface { - if db == nil { - panic("must provide db") - } - if defaultLogger == nil { - panic("must provide logger") - } - return &dblogger{ - defaultLogger: defaultLogger, - db: db, - } -} - -type dblogger struct { - defaultLogger *logrus.Entry - db Interface -} - -func (p *dblogger) DBX() *sqlx.DB { - return p.db.DBX() -} - -func (p *dblogger) logger(ctx context.Context) *logrus.Entry { - if ctx == nil { - return p.defaultLogger - } - logger := logctx.LoggerOrNil(ctx) - if logger != nil { - return logger - } - return p.defaultLogger -} - -func (p *dblogger) log(ctx context.Context, cmd, q string, args []interface{}) { - logger := p.logger(ctx) - logger.WithFields(logrus.Fields{ - "sql_statement": q, - "sql_args": args, - }).Debug("sql_" + cmd) -} - -func (p *dblogger) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { - p.log(ctx, "exec", query, args) - return p.db.ExecContext(ctx, query, args...) -} - -func (p *dblogger) Exec(query string, args ...interface{}) (sql.Result, error) { - p.log(nil, "exec", query, args) - return p.db.Exec(query, args...) -} - -func (p *dblogger) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { - p.log(ctx, "query", query, args) - return p.db.QueryContext(ctx, query, args...) -} - -func (p *dblogger) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { - p.log(ctx, "queryx", query, args) - return p.db.QueryxContext(ctx, query, args...) -} - -func (p *dblogger) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row { - p.log(ctx, "queryxrow", query, args) - return p.db.QueryRowxContext(ctx, query, args...) -} - -func (p *dblogger) Query(query string, args ...interface{}) (*sql.Rows, error) { - p.log(nil, "query", query, args) - return p.db.Query(query, args...) -} - -func (p *dblogger) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) { - p.log(nil, "queryx", query, args) - return p.db.Queryx(query, args...) -} - -func (p *dblogger) QueryRowx(query string, args ...interface{}) *sqlx.Row { - p.log(nil, "queryxrow", query, args) - return p.db.QueryRowx(query, args...) -} diff --git a/api/sqlw/mockdb.go b/api/sqlw/mockdb.go deleted file mode 100644 index 6250f21..0000000 --- a/api/sqlw/mockdb.go +++ /dev/null @@ -1,88 +0,0 @@ -package sqlw - -import ( - "context" - "database/sql" - "github.com/jmoiron/sqlx" -) - -type Interceptor func(context.Context, string, []interface{}) error - -// WithInterceptor will call interceptor before each DB call. -// If interceptor returns an error, it will be returned. -// If the DB method does not return an error (like QueryRow), but Interceptor does, -// panic with the error. -// Usually this is used for mocking. -func WithInterceptor(db Interface, interceptor Interceptor) Interface { - return &dbintercept{ - Interceptor: interceptor, - DB: db, - } -} - -type dbintercept struct { - Interceptor Interceptor - DB Interface -} - -func (p *dbintercept) DBX() *sqlx.DB { - return p.DB.DBX() -} - -func (p *dbintercept) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { - if err := p.Interceptor(ctx, query, args); err != nil { - return nil, err - } - return p.DB.ExecContext(ctx, query, args...) -} - -func (p *dbintercept) Exec(query string, args ...interface{}) (sql.Result, error) { - if err := p.Interceptor(nil, query, args); err != nil { - return nil, err - } - return p.DB.Exec(query, args...) -} - -func (p *dbintercept) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { - if err := p.Interceptor(ctx, query, args); err != nil { - return nil, err - } - return p.DB.QueryContext(ctx, query, args...) -} - -func (p *dbintercept) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { - if err := p.Interceptor(ctx, query, args); err != nil { - return nil, err - } - return p.DB.QueryxContext(ctx, query, args...) -} - -func (p *dbintercept) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row { - if err := p.Interceptor(ctx, query, args); err != nil { - panic(err) - } - return p.DB.QueryRowxContext(ctx, query, args...) -} - -func (p *dbintercept) Query(query string, args ...interface{}) (*sql.Rows, error) { - if err := p.Interceptor(nil, query, args); err != nil { - return nil, err - } - return p.DB.Query(query, args...) -} - -func (p *dbintercept) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) { - if err := p.Interceptor(nil, query, args); err != nil { - return nil, err - } - return p.DB.Queryx(query, args...) -} - -func (p *dbintercept) QueryRowx(query string, args ...interface{}) *sqlx.Row { - if err := p.Interceptor(nil, query, args); err != nil { - panic(err) - } - return p.DB.QueryRowx(query, args...) -} - -var _ Interface = &dbintercept{} diff --git a/api/sqlw/sqlw.go b/api/sqlw/sqlw.go deleted file mode 100644 index b922102..0000000 --- a/api/sqlw/sqlw.go +++ /dev/null @@ -1,92 +0,0 @@ -package sqlw - -import ( - "context" - "database/sql" - "github.com/jmoiron/sqlx" -) - -// Interface is a common wrapper over sqlx so we can compose functionality. -type Interface interface { - sqlx.Queryer - sqlx.QueryerContext - sqlx.Execer - sqlx.ExecerContext - DBX() *sqlx.DB -} - -type AddRow func([]interface{}) - -func CopyFrom(ctx context.Context, db *sql.DB, copyIn string, rowAdder func(cb AddRow)) error { - txn, err := db.Begin() - if err != nil { - return err - } - stmt, err := txn.Prepare(copyIn) - if err != nil { - return err - } - rowAdder(func(i []interface{}) { - if _, e := stmt.ExecContext(ctx, i...); e != nil { - err = e - } - }) - if err != nil { - return err - } - if _, err = stmt.ExecContext(ctx); err != nil { - return err - } - if err := stmt.Close(); err != nil { - return err - } - if err := txn.Commit(); err != nil { - return err - } - return nil -} - -// Wrap wraps a real sqlx.DB connection into one that can be composed. -func Wrap(db *sqlx.DB) Interface { - return &sqlxWrapper{db: db} -} - -type sqlxWrapper struct { - db *sqlx.DB -} - -func (s *sqlxWrapper) Query(query string, args ...interface{}) (*sql.Rows, error) { - return s.db.Query(query, args...) -} - -func (s *sqlxWrapper) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) { - return s.db.Queryx(query, args...) -} - -func (s *sqlxWrapper) QueryRowx(query string, args ...interface{}) *sqlx.Row { - return s.db.QueryRowx(query, args...) -} - -func (s *sqlxWrapper) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) { - return s.db.QueryContext(ctx, query, args...) -} - -func (s *sqlxWrapper) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) { - return s.db.QueryxContext(ctx, query, args...) -} - -func (s *sqlxWrapper) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row { - return s.db.QueryRowxContext(ctx, query, args...) -} - -func (s *sqlxWrapper) Exec(query string, args ...interface{}) (sql.Result, error) { - return s.db.Exec(query, args...) -} - -func (s *sqlxWrapper) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) { - return s.db.ExecContext(ctx, query, args...) -} - -func (s *sqlxWrapper) DBX() *sqlx.DB { - return s.db -} diff --git a/api/trace_id.go b/api/trace_id.go index 59b0cab..78863e1 100644 --- a/api/trace_id.go +++ b/api/trace_id.go @@ -2,8 +2,8 @@ package api import ( "github.com/google/uuid" - "github.com/labstack/echo" - "github.com/lithictech/go-aperitif/logctx" + "github.com/labstack/echo/v4" + "github.com/lithictech/go-aperitif/v2/logctx" ) const TraceIdHeader = "Trace-Id" diff --git a/async/async.go b/async/async.go index 30e0f91..8bcf003 100644 --- a/async/async.go +++ b/async/async.go @@ -1,6 +1,6 @@ package async -import "github.com/lithictech/go-aperitif/mariobros" +import "github.com/lithictech/go-aperitif/v2/mariobros" type Goer func(name string, f func()) diff --git a/async/async_test.go b/async/async_test.go index a56d05e..22936f9 100644 --- a/async/async_test.go +++ b/async/async_test.go @@ -1,7 +1,7 @@ package async_test import ( - "github.com/lithictech/go-aperitif/async" + "github.com/lithictech/go-aperitif/v2/async" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "testing" diff --git a/go.mod b/go.mod index d799210..0eca24a 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,38 @@ -module github.com/lithictech/go-aperitif +module github.com/lithictech/go-aperitif/v2 -go 1.16 +go 1.22 require ( - github.com/auth0/go-jwt-middleware v0.0.0-20200507191422-d30d7b9ece63 - github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/google/uuid v1.1.1 - github.com/hashicorp/go-multierror v1.1.0 - github.com/jmoiron/sqlx v1.2.0 - github.com/labstack/echo v3.3.10+incompatible - github.com/labstack/gommon v0.3.0 // indirect - github.com/onsi/ginkgo/v2 v2.3.1 - github.com/onsi/gomega v1.22.0 + github.com/google/uuid v1.6.0 + github.com/hashicorp/go-multierror v1.1.1 + github.com/labstack/echo/v4 v4.12.0 + github.com/onsi/ginkgo/v2 v2.19.1 + github.com/onsi/gomega v1.34.1 + github.com/phsym/console-slog v0.3.1 github.com/pkg/errors v0.9.1 - github.com/rgalanakis/golangal v1.1.0 + github.com/rgalanakis/golangal v1.2.0 github.com/rgalanakis/validator v0.0.0-20180731224108-4a34a8927f7c - github.com/sirupsen/logrus v1.6.0 - github.com/valyala/fasttemplate v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 - google.golang.org/appengine v1.6.6 // indirect + golang.org/x/crypto v0.25.0 +) + +require ( + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.23.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1740b47..96eb469 100644 --- a/go.sum +++ b/go.sum @@ -1,207 +1,71 @@ -github.com/auth0/go-jwt-middleware v0.0.0-20200507191422-d30d7b9ece63 h1:LY/kRH+fCqA090FsM2VfZ+oocD99ogm3HrT1r0WDnCk= -github.com/auth0/go-jwt-middleware v0.0.0-20200507191422-d30d7b9ece63/go.mod h1:mF0ip7kTEFtnhBJbd/gJe62US3jykNN+dcZoZakJCCA= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= +github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= -github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= -github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg= -github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= -github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= -github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= -github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= -github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.3.1 h1:8SbseP7qM32WcvE6VaN6vfXxv698izmsJ1UQX9ve7T8= -github.com/onsi/ginkgo/v2 v2.3.1/go.mod h1:Sv4yQXwG5VmF7tm3Q5Z+RWUpPo24LF1mpnz2crUb8Ys= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= -github.com/onsi/gomega v1.22.0 h1:AIg2/OntwkBiCg5Tt1ayyiF1ArFrWFoCSMtMi/wdApk= -github.com/onsi/gomega v1.22.0/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= +github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/phsym/console-slog v0.3.1 h1:Fuzcrjr40xTc004S9Kni8XfNsk+qrptQmyR+wZw9/7A= +github.com/phsym/console-slog v0.3.1/go.mod h1:oJskjp/X6e6c0mGpfP8ELkfKUsrkDifYRAqJQgmdDS0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rgalanakis/golangal v1.1.0 h1:JMR5gBHMmcWQ86hYM8mRXp99AC1AeEdWKc4sNfjjA3A= -github.com/rgalanakis/golangal v1.1.0/go.mod h1:DT35MZom81QtvdrIerWZ0T3moT/npNBiJmW9cXcufMM= +github.com/rgalanakis/golangal v1.2.0 h1:dDrD6sJR2JbnNl0aDNvBHjzWN6r9lDTfv3XqKTNTMmg= +github.com/rgalanakis/golangal v1.2.0/go.mod h1:DT35MZom81QtvdrIerWZ0T3moT/npNBiJmW9cXcufMM= github.com/rgalanakis/validator v0.0.0-20180731224108-4a34a8927f7c h1:z1J+SUwpje5zEyPfHBIv6N++OxjETBV5wGBwkmO/Wzk= github.com/rgalanakis/validator v0.0.0-20180731224108-4a34a8927f7c/go.mod h1:qIw2dAd/4wMHHnINySB5+y9HPKQXUKmga0oZHSluPKE= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= -github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/jwtee/jwtee.go b/jwtee/jwtee.go deleted file mode 100644 index 025454d..0000000 --- a/jwtee/jwtee.go +++ /dev/null @@ -1,149 +0,0 @@ -// Package jwtee wraps github.com/dgrijalva/jwt-go -// with some tooling that makes it easier to use -// in most practical usage. -package jwtee - -import ( - "crypto/subtle" - "errors" - "github.com/dgrijalva/jwt-go" - "time" -) - -type Error struct { - msg string -} - -func (e Error) Error() string { - return e.msg -} - -type Jwtee struct { - Secret []byte - Aud string - Iss string - Alg jwt.SigningMethod -} - -type Input struct { - Secret string - Aud string - Iss string - Alg string -} - -func New(input Input) (Jwtee, error) { - j := Jwtee{ - Secret: []byte(input.Secret), - Aud: input.Aud, - Iss: input.Iss, - Alg: jwt.GetSigningMethod(input.Alg), - } - if len(j.Secret) == 0 { - return j, errors.New("secret is required") - } - if j.Aud == "" { - return j, errors.New("aud is required") - } - if j.Iss == "" { - return j, errors.New("iss is required") - } - if j.Alg == nil { - return j, errors.New("valid alg is required") - } - return j, nil -} - -func (j Jwtee) Parse(tokenString string) (*jwt.Token, error) { - return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - if token.Method != j.Alg { - return token, Error{msg: "invalid alg"} - } - checkAud := verifyArrayAudience(token.Claims.(jwt.MapClaims), j.Aud, true) - if !checkAud { - return token, Error{msg: "invalid aud"} - } - checkIss := token.Claims.(jwt.MapClaims).VerifyIssuer(j.Iss, true) - if !checkIss { - return token, Error{msg: "invalid iss"} - } - return j.Secret, nil - }) -} - -func (j Jwtee) ParseMapClaims(tokenString string) (jwt.MapClaims, error) { - tok, err := j.Parse(tokenString) - if tok == nil { - panic("token should never be nil") - } - return tok.Claims.(jwt.MapClaims), err -} - -func (j Jwtee) BuildTtl(ttl time.Duration, moreClaims map[string]interface{}) (string, error) { - tok := jwt.New(j.Alg) - mc := tok.Claims.(jwt.MapClaims) - mc["iss"] = j.Iss - mc["aud"] = j.Aud - mc["exp"] = jwt.TimeFunc().Add(ttl).Unix() - for k, v := range moreClaims { - mc[k] = v - } - return tok.SignedString(j.Secret) -} - -func (j Jwtee) Dup(input Input) Jwtee { - if len(input.Secret) > 0 { - j.Secret = []byte(input.Secret) - } - if input.Aud != "" { - j.Aud = input.Aud - } - if input.Iss != "" { - j.Iss = input.Iss - } - if input.Alg != "" { - j.Alg = jwt.GetSigningMethod(input.Alg) - } - return j -} - -// See https://github.com/dgrijalva/jwt-go/pull/308 -// These two methods are straight copy paste -func verifyArrayAudience(m jwt.MapClaims, cmp string, req bool) bool { - switch m["aud"].(type) { - case string: - aud := m["aud"].(string) - return verifyAudHelper(aud, cmp, req) - default: - auds := m["aud"].([]interface{}) - for _, aud := range auds { - if verifyAudHelper(aud.(string), cmp, req) { - return true - } - } - return false - } -} - -func verifyAudHelper(aud string, cmp string, required bool) bool { - if aud == "" { - return !required - } - if subtle.ConstantTimeCompare([]byte(aud), []byte(cmp)) != 0 { - return true - } else { - return false - } -} - -func StringClaim(claims jwt.MapClaims, key string) (string, bool) { - v, ok := claims[key] - if !ok { - return "", false - } - s, ok := v.(string) - if !ok { - return "", false - } - return s, len(s) > 0 -} diff --git a/jwtee/jwtee_test.go b/jwtee/jwtee_test.go deleted file mode 100644 index 3455b93..0000000 --- a/jwtee/jwtee_test.go +++ /dev/null @@ -1,197 +0,0 @@ -package jwtee_test - -import ( - "github.com/dgrijalva/jwt-go" - "github.com/lithictech/go-aperitif/jwtee" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - . "github.com/rgalanakis/golangal" - "testing" - "time" -) - -func TestJwtee(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "jwtee package Suite") -} - -var _ = Describe("jwtee", func() { - secret := "xyz" - aud := "hi" - iss := "there" - alg := "HS256" - - validInput := func() jwtee.Input { - return jwtee.Input{ - Secret: secret, - Aud: aud, - Iss: iss, - Alg: alg, - } - } - - newJwtee := func() jwtee.Jwtee { - j, err := jwtee.New(validInput()) - Expect(err).ToNot(HaveOccurred()) - return j - } - - It("requires secret, aud, iss, and alg", func() { - var err error - var jw jwtee.Jwtee - jw, err = jwtee.New(validInput()) - Expect(err).ToNot(HaveOccurred()) - Expect(jw).To(And( - MatchField("Aud", aud), - MatchField("Iss", iss), - MatchField("Alg", jwt.SigningMethodHS256), - )) - _, err = jwtee.New(jwtee.Input{ - Secret: "", - Aud: aud, - Iss: iss, - Alg: alg, - }) - Expect(err).To(MatchError(ContainSubstring("secret is required"))) - _, err = jwtee.New(jwtee.Input{ - Secret: secret, - Aud: "", - Iss: iss, - Alg: alg, - }) - Expect(err).To(MatchError(ContainSubstring("aud is required"))) - _, err = jwtee.New(jwtee.Input{ - Secret: secret, - Aud: aud, - Iss: "", - Alg: alg, - }) - Expect(err).To(MatchError(ContainSubstring("iss is required"))) - _, err = jwtee.New(jwtee.Input{ - Secret: secret, - Aud: aud, - Iss: iss, - Alg: "", - }) - Expect(err).To(MatchError(ContainSubstring("alg is required"))) - }) - It("can dup itself with non-empty input values", func() { - jw := newJwtee() - jw2 := jw.Dup(jwtee.Input{Aud: "another"}) - Expect(jw.Aud).To(Equal(aud)) - Expect(jw.Iss).To(Equal(iss)) - Expect(jw2.Aud).To(Equal("another")) - Expect(jw2.Iss).To(Equal(iss)) - }) - Describe("parsing", func() { - It("can verify with a string aud claim", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyfQ.kTgZa43Zq9LrjDAEerD8feT2_TrIhzCPO1UC4bBXzgQ` - cl, err := jw.ParseMapClaims(s) - Expect(err).ToNot(HaveOccurred()) - Expect(cl["aud"]).To(Equal("hi")) - }) - - It("can fail with an invalid aud claim", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ5byIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyfQ.BG7D0kCIcdgTfhOFNxArgubEL_2_WQmxE4vpnOv_AlU` - cl, err := jw.ParseMapClaims(s) - Expect(err).To(MatchError("invalid aud")) - Expect(cl["aud"]).To(Equal("yo")) - }) - It("can verify with an array aud claim", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaGkiLCJoZWxsbyJdLCJpc3MiOiJ0aGVyZSIsImlhdCI6MTUxNjIzOTAyMn0.37-1H6f20flFs2vjJ6u2nzh7BQ51kyQyELEX0y_xE3c` - cl, err := jw.ParseMapClaims(s) - Expect(err).ToNot(HaveOccurred()) - Expect(cl["aud"]).To(BeEquivalentTo([]interface{}{"hi", "hello"})) - }) - It("can fail if aud not in array aud claim", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsieW8iXSwiaXNzIjoidGhlcmUiLCJpYXQiOjE1MTYyMzkwMjJ9.u-WkwjTF4kxdGB2wtinAtC1usOnIqeTPnDKg2HQ2gJw` - cl, err := jw.ParseMapClaims(s) - Expect(err).To(MatchError("invalid aud")) - Expect(cl["aud"]).To(BeEquivalentTo([]interface{}{"yo"})) - }) - It("can verify against an issuer", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyfQ.kTgZa43Zq9LrjDAEerD8feT2_TrIhzCPO1UC4bBXzgQ` - cl, err := jw.ParseMapClaims(s) - Expect(err).ToNot(HaveOccurred()) - Expect(cl["iss"]).To(Equal("there")) - }) - It("can fail with an invalid issuer", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InlvbmRlciIsImlhdCI6MTUxNjIzOTAyMn0.Wo0zf5P9H4HAnOWTgdUKNN0W-jTTJot0lEl5kE1r3YY` - cl, err := jw.ParseMapClaims(s) - Expect(err).To(MatchError("invalid iss")) - Expect(cl["iss"]).To(Equal("yonder")) - }) - It("validates against the signing method", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyfQ.kTgZa43Zq9LrjDAEerD8feT2_TrIhzCPO1UC4bBXzgQ` - tok, err := jw.Parse(s) - Expect(err).ToNot(HaveOccurred()) - Expect(tok.Header["alg"]).To(Equal("HS256")) - }) - It("can fail with an unexpected signing method", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InlvbmRlciIsImlhdCI6MTUxNjIzOTAyMn0.7q_DMegJbTO9uxWPy7n2mfDrBgAO3xBSpVmGjqHG6-ubve8QH2Y1d2noYWMk-wjSwkfbVB1K98FCfVDvZxhfGA` - tok, err := jw.Parse(s) - Expect(err).To(MatchError("invalid alg")) - Expect(tok.Header["alg"]).To(Equal("HS512")) - }) - It("can verify an unexpired exp", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1OTc5ODkyMzM1MTR9.bpLset-GCKbOlish900xGamrRCqQzmX06A2e2BtAdJE` - cl, err := jw.ParseMapClaims(s) - Expect(err).ToNot(HaveOccurred()) - Expect(cl["exp"]).To(BeEquivalentTo(1597989233514)) - }) - It("fails expired exp", func() { - jw := newJwtee() - s := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImlzcyI6InRoZXJlIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjV9.XsqW1BORkEZBwYXzDVgPJmSV-6wDzkFaZ7NacIfDjNY` - cl, err := jw.ParseMapClaims(s) - Expect(err).To(MatchError("Token is expired")) - Expect(cl["exp"]).To(BeEquivalentTo(5)) - }) - }) - Describe("building", func() { - origTime := jwt.TimeFunc - AfterEach(func() { - jwt.TimeFunc = origTime - }) - It("builds a token with the default fields and additional fields", func() { - jwt.TimeFunc = func() time.Time { - return time.Unix(10000, 0) - } - jw := newJwtee() - js, err := jw.BuildTtl(654*time.Second, map[string]interface{}{"sub": 1234}) - Expect(err).ToNot(HaveOccurred()) - expected := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJoaSIsImV4cCI6MTA2NTQsImlzcyI6InRoZXJlIiwic3ViIjoxMjM0fQ.OgPwnSrNaEpCgSMcILAdATor2NGlupnt7ggbqr32NL0` - Expect(js).To(Equal(expected)) - }) - }) - Describe("StringClaim", func() { - It("extracts a non-empty string claim", func() { - c := jwt.MapClaims{"s": "", "s2": "a", "i": 1} - var s string - var ok bool - s, ok = jwtee.StringClaim(c, "s2") - Expect(ok).To(BeTrue()) - Expect(s).To(Equal("a")) - - s, ok = jwtee.StringClaim(c, "s") - Expect(ok).To(BeFalse()) - Expect(s).To(BeEmpty()) - - s, ok = jwtee.StringClaim(c, "x") - Expect(ok).To(BeFalse()) - Expect(s).To(BeEmpty()) - - s, ok = jwtee.StringClaim(c, "i") - Expect(ok).To(BeFalse()) - Expect(s).To(BeEmpty()) - }) - }) -}) diff --git a/kronos/kronos.go b/kronos/kronos.go index b1544e9..e9f1c0e 100644 --- a/kronos/kronos.go +++ b/kronos/kronos.go @@ -119,9 +119,9 @@ func RollMonth(t time.Time, months int) time.Time { // Get the new month after offsetting month m by offset. // -// offsetMonth(January, 1) => February -// offsetMonth(January, 13) => February -// offsetMonth(January, -1) => December +// offsetMonth(January, 1) => February +// offsetMonth(January, 13) => February +// offsetMonth(January, -1) => December func offsetMonth(m time.Month, offset int) time.Month { zeroBased := int(m - 1) offsetMonth := zeroBased + offset diff --git a/kronos/kronos_test.go b/kronos/kronos_test.go index 7e7ef50..d7d9c69 100644 --- a/kronos/kronos_test.go +++ b/kronos/kronos_test.go @@ -2,7 +2,7 @@ package kronos_test import ( "fmt" - "github.com/lithictech/go-aperitif/kronos" + "github.com/lithictech/go-aperitif/v2/kronos" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "math/rand" diff --git a/logctx/log_hook.go b/logctx/log_hook.go new file mode 100644 index 0000000..cbd7400 --- /dev/null +++ b/logctx/log_hook.go @@ -0,0 +1,108 @@ +package logctx + +import ( + "context" + "log/slog" + "sync" +) + +type HookRecord struct { + Record slog.Record + Attrs []slog.Attr + Group string +} + +func (r HookRecord) AttrMap() map[string]any { + return attrMap(r.Attrs) +} + +func NewHook() *Hook { + return &Hook{records: &hookRecords{r: make([]HookRecord, 0, 4)}} +} + +// Hook is a hook designed for dealing with logs in test scenarios. +type Hook struct { + records *hookRecords + attrs []slog.Attr + group string +} + +var _ slog.Handler = &Hook{} + +func (t *Hook) Enabled(context.Context, slog.Level) bool { + return true +} + +func (t *Hook) Handle(_ context.Context, r slog.Record) error { + t.records.Add(HookRecord{Record: r, Attrs: t.attrs, Group: t.group}) + return nil +} + +func (t *Hook) WithAttrs(attrs []slog.Attr) slog.Handler { + return &Hook{ + records: t.records, + attrs: append(t.attrs, attrs...), + group: t.group, + } +} + +func (t *Hook) WithGroup(group string) slog.Handler { + return &Hook{ + records: t.records, + attrs: t.attrs, + group: group, + } +} + +// LastRecord returns the last record that was logged or nil. +func (t *Hook) LastRecord() *HookRecord { + return t.records.LastRecord() +} + +// Records returns all records that were logged. +func (t *Hook) Records() []HookRecord { + return t.records.Records() +} + +func (t *Hook) AttrMap() map[string]any { + return attrMap(t.attrs) +} + +type hookRecords struct { + r []HookRecord + mux sync.RWMutex +} + +func (h *hookRecords) Add(r HookRecord) { + h.mux.Lock() + defer h.mux.Unlock() + h.r = append(h.r, r) +} + +func (h *hookRecords) Records() []HookRecord { + h.mux.RLock() + defer h.mux.RUnlock() + entries := make([]HookRecord, len(h.r)) + for i, rec := range h.r { + entries[i] = rec + } + return entries +} + +func (h *hookRecords) LastRecord() *HookRecord { + h.mux.RLock() + defer h.mux.RUnlock() + i := len(h.r) - 1 + if i < 0 { + return nil + } + return &h.r[i] +} + +func attrMap(attrs []slog.Attr) map[string]any { + result := make(map[string]any, len(attrs)) + for _, a := range attrs { + result[a.Key] = a.Value.Any() + } + return result +} diff --git a/logctx/logctx.go b/logctx/logctx.go index 545ac42..e42bfc7 100644 --- a/logctx/logctx.go +++ b/logctx/logctx.go @@ -3,9 +3,10 @@ package logctx import ( "context" "github.com/google/uuid" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" + "github.com/phsym/console-slog" "golang.org/x/crypto/ssh/terminal" + "io" + "log/slog" "os" ) @@ -25,13 +26,13 @@ const ProcessTraceIdKey TraceIdKey = "process_trace_id" // MissingTraceIdKey is the key that will be present to indicate tracing is misconfigured. const MissingTraceIdKey TraceIdKey = "missing_trace_id" -func unconfiguredLogger() *logrus.Entry { - return logrus.New().WithField("unconfigured_logger", "true") +func UnconfiguredLogger() *slog.Logger { + return slog.Default().With("unconfigured_logger", "true") } // WithLogger returns a new context that adds a logger which // can be retrieved with Logger(Context). -func WithLogger(c context.Context, logger *logrus.Entry) context.Context { +func WithLogger(c context.Context, logger *slog.Logger) context.Context { return context.WithValue(c, LoggerKey, logger) } @@ -43,7 +44,7 @@ func WithLogger(c context.Context, logger *logrus.Entry) context.Context { func WithTracingLogger(c context.Context) context.Context { logger := Logger(c) tkey, trace := ActiveTraceId(c) - logger = logger.WithField(string(tkey), trace) + logger = logger.With(string(tkey), trace) return context.WithValue(c, LoggerKey, logger) } @@ -51,16 +52,16 @@ func WithTraceId(c context.Context, key TraceIdKey) context.Context { return context.WithValue(c, key, uuid.New().String()) } -func LoggerOrNil(c context.Context) *logrus.Entry { - logger, _ := c.Value(LoggerKey).(*logrus.Entry) +func LoggerOrNil(c context.Context) *slog.Logger { + logger, _ := c.Value(LoggerKey).(*slog.Logger) return logger } -func Logger(c context.Context) *logrus.Entry { - if logger, ok := c.Value(LoggerKey).(*logrus.Entry); ok { +func Logger(c context.Context) *slog.Logger { + if logger, ok := c.Value(LoggerKey).(*slog.Logger); ok { return logger } - logger := unconfiguredLogger() + logger := UnconfiguredLogger() logger.Warn( "Logger called with no logger in context. " + "It should always be there to ensure consistent logs from a single logger") @@ -88,23 +89,15 @@ func ActiveTraceIdValue(c context.Context) string { return v } -func AddFieldsAndGet(c context.Context, fields map[string]interface{}) (context.Context, *logrus.Entry) { - logger := Logger(c) - logger = logger.WithFields(fields) - return WithLogger(c, logger), logger -} - -func AddFieldAndGet(c context.Context, key string, value interface{}) (context.Context, *logrus.Entry) { - return AddFieldsAndGet(c, map[string]interface{}{key: value}) -} - -func AddFields(c context.Context, fields map[string]interface{}) context.Context { - ctx, _ := AddFieldsAndGet(c, fields) +func AddTo(c context.Context, args ...any) context.Context { + ctx, _ := AddToR(c, args...) return ctx } -func AddField(c context.Context, key string, value interface{}) context.Context { - return AddFields(c, map[string]interface{}{key: value}) +func AddToR(c context.Context, args ...any) (context.Context, *slog.Logger) { + logger := Logger(c) + logger = logger.With(args...) + return WithLogger(c, logger), logger } type NewLoggerInput struct { @@ -113,56 +106,58 @@ type NewLoggerInput struct { File string BuildSha string BuildTime string - Fields logrus.Fields + Fields []any } -func NewLogger(cfg NewLoggerInput) (*logrus.Entry, error) { - logger := logrus.New() +func NewLogger(cfg NewLoggerInput) (*slog.Logger, error) { + // Set output to file or stdout/stderr (stderr for tty, stdout otherwise like for 12 factor apps) + var out io.Writer + if cfg.File != "" { + file, err := os.OpenFile(cfg.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + if err != nil { + return nil, err + } + out = file + } else if IsTty() { + out = os.Stderr + } else { + out = os.Stdout + } - // Parse and set level - lvl, err := logrus.ParseLevel(cfg.Level) + hopts := &slog.HandlerOptions{} + lvl, err := ParseLevel(cfg.Level) if err != nil { return nil, err } - logger.SetLevel(lvl) + hopts.Level = lvl - // Set format + var handler slog.Handler if cfg.Format == "json" { - logger.SetFormatter(&logrus.JSONFormatter{}) + handler = slog.NewJSONHandler(out, hopts) } else if cfg.Format == "text" { - logger.SetFormatter(&logrus.TextFormatter{}) + handler = slog.NewTextHandler(out, hopts) } else if cfg.File != "" { - logger.SetFormatter(&logrus.JSONFormatter{}) + handler = slog.NewJSONHandler(out, hopts) } else if IsTty() { - logger.SetFormatter(&logrus.TextFormatter{}) + handler = console.NewHandler(out, &console.HandlerOptions{ + AddSource: hopts.AddSource, + Level: hopts.Level, + }) } else { - logger.SetFormatter(&logrus.JSONFormatter{}) + handler = slog.NewJSONHandler(out, hopts) } - // Set output to file or stdout/stderr (stderr for tty, stdout otherwise like for 12 factor apps) - if cfg.File != "" { - file, err := os.OpenFile(cfg.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) - if err != nil { - return nil, err - } - logger.SetOutput(file) - } else if IsTty() { - logger.SetOutput(os.Stderr) - } else { - logger.SetOutput(os.Stdout) - } - - entry := logger.WithFields(nil) + logger := slog.New(handler) if len(cfg.Fields) > 0 { - entry = logger.WithFields(cfg.Fields) + logger = logger.With(cfg.Fields...) } if cfg.BuildSha != "" { - entry = entry.WithField("build_sha", cfg.BuildSha) + logger = logger.With("build_sha", cfg.BuildSha) } if cfg.BuildTime != "" { - entry = entry.WithField("build_time", cfg.BuildTime) + logger = logger.With("build_time", cfg.BuildTime) } - return entry, nil + return logger, nil } func IsTty() bool { @@ -171,12 +166,25 @@ func IsTty() bool { // WithNullLogger adds the logger from test.NewNullLogger into the given context // (default c to context.Background). Use the hook to get the log messages. -// See https://github.com/sirupsen/logrus#testing for testing with logrus. -func WithNullLogger(c context.Context) (context.Context, *test.Hook) { +// See https://github.com/sirupsen/logrus#testing for examples, +// though this doesn't use logrus the ideas still apply. +func WithNullLogger(c context.Context) (context.Context, *Hook) { if c == nil { c = context.Background() } - logger, hook := test.NewNullLogger() - c2 := WithLogger(c, logger.WithField("testlogger", true)) + logger, hook := NewNullLogger() + c2 := WithLogger(c, logger.With("testlogger", true)) return c2, hook } + +func ParseLevel(s string) (slog.Level, error) { + var level slog.Level + var err = level.UnmarshalText([]byte(s)) + return level, err +} + +func NewNullLogger() (*slog.Logger, *Hook) { + hook := NewHook() + logger := slog.New(hook) + return logger, hook +} diff --git a/logctx/logctx_test.go b/logctx/logctx_test.go index 9b107c1..0e9e493 100644 --- a/logctx/logctx_test.go +++ b/logctx/logctx_test.go @@ -2,10 +2,10 @@ package logctx_test import ( "context" - "github.com/lithictech/go-aperitif/logctx" + "github.com/lithictech/go-aperitif/v2/logctx" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/sirupsen/logrus" + "log/slog" "testing" ) @@ -15,37 +15,44 @@ func TestLogtools(t *testing.T) { } var _ = Describe("logtools", func() { - bg := context.Background() + var logger *slog.Logger + var hook *logctx.Hook + var ctx context.Context + + BeforeEach(func() { + logger, hook = logctx.NewNullLogger() + ctx = logctx.WithLogger(context.Background(), logger) + }) Describe("WithTraceId", func() { It("adds a new trace id", func() { - c := logctx.WithTraceId(bg, logctx.ProcessTraceIdKey) + c := logctx.WithTraceId(ctx, logctx.ProcessTraceIdKey) Expect(c.Value(logctx.ProcessTraceIdKey)).To(HaveLen(36)) }) }) Describe("ActiveTraceId", func() { It("returns a request trace id", func() { - c := context.WithValue(bg, logctx.RequestTraceIdKey, "abc") + c := context.WithValue(ctx, logctx.RequestTraceIdKey, "abc") key, val := logctx.ActiveTraceId(c) Expect(key).To(Equal(logctx.RequestTraceIdKey)) Expect(val).To(Equal("abc")) Expect(logctx.ActiveTraceIdValue(c)).To(Equal("abc")) }) It("returns a process trace id", func() { - c := context.WithValue(bg, logctx.ProcessTraceIdKey, "abc") + c := context.WithValue(ctx, logctx.ProcessTraceIdKey, "abc") key, val := logctx.ActiveTraceId(c) Expect(key).To(Equal(logctx.ProcessTraceIdKey)) Expect(val).To(Equal("abc")) }) It("returns a process trace id", func() { - c := context.WithValue(bg, logctx.JobTraceIdKey, "abc") + c := context.WithValue(ctx, logctx.JobTraceIdKey, "abc") key, val := logctx.ActiveTraceId(c) Expect(key).To(Equal(logctx.JobTraceIdKey)) Expect(val).To(Equal("abc")) }) It("prefers request->job->process trace id", func() { - c := bg + c := ctx c = context.WithValue(c, logctx.ProcessTraceIdKey, "proc") key, _ := logctx.ActiveTraceId(c) @@ -60,7 +67,7 @@ var _ = Describe("logtools", func() { Expect(key).To(Equal(logctx.RequestTraceIdKey)) }) It("defaults to a missing trace id", func() { - key, val := logctx.ActiveTraceId(bg) + key, val := logctx.ActiveTraceId(ctx) Expect(key).To(Equal(logctx.MissingTraceIdKey)) Expect(val).To(Equal("no-trace-id-in-context")) }) @@ -68,7 +75,6 @@ var _ = Describe("logtools", func() { Describe("WithLogger", func() { It("adds the logger", func() { - logger := &logrus.Entry{} c := logctx.WithLogger(context.Background(), logger) Expect(c.Value(logctx.LoggerKey)).To(BeAssignableToTypeOf(logger)) }) @@ -76,24 +82,26 @@ var _ = Describe("logtools", func() { Describe("WithTracingLogger", func() { It("adds a trace id to the logger", func() { - c := logctx.WithTracingLogger(logctx.WithTraceId(bg, logctx.RequestTraceIdKey)) - logger := c.Value(logctx.LoggerKey).(*logrus.Entry) - Expect(logger.Data).To(HaveKeyWithValue(BeEquivalentTo(logctx.RequestTraceIdKey), BeAssignableToTypeOf(""))) + c := logctx.WithTracingLogger(logctx.WithTraceId(ctx, logctx.RequestTraceIdKey)) + logctx.Logger(c).Info("hi") + Expect(hook.LastRecord().AttrMap()).To(HaveKeyWithValue(BeEquivalentTo(logctx.RequestTraceIdKey), BeAssignableToTypeOf(""))) }) }) - Describe("AddFields", func() { + Describe("AddTo", func() { It("returns a new context where the given fields have been added to the context logger", func() { - c := logctx.AddField(bg, "x", "y") - logger := logctx.Logger(c) - Expect(logger.Data).To(HaveKeyWithValue("x", "y")) + c := logctx.AddTo(ctx, "x", "y") + Expect(logctx.Logger(c).Handler().(*logctx.Hook).AttrMap()).To(HaveKeyWithValue("x", "y")) + logctx.Logger(c).Info("hi") + Expect(hook.LastRecord().AttrMap()).To(HaveKeyWithValue("x", "y")) }) }) - Describe("AddFieldsAndGet", func() { + Describe("AddToR", func() { It("returns the new context, and the logger that was added", func() { - c, logger := logctx.AddFieldAndGet(bg, "x", "y") - Expect(logger.Data).To(HaveKeyWithValue("x", "y")) + c, logger := logctx.AddToR(ctx, "x", "y") + logctx.Logger(c).Info("hi") + Expect(hook.LastRecord().AttrMap()).To(HaveKeyWithValue("x", "y")) Expect(logger).To(BeIdenticalTo(logctx.Logger(c))) }) }) @@ -102,8 +110,8 @@ var _ = Describe("logtools", func() { It("inserts the null logger", func() { c, hook := logctx.WithNullLogger(nil) logctx.Logger(c).Info("hi") - Expect(hook.Entries).To(HaveLen(1)) - Expect(hook.LastEntry().Message).To(Equal("hi")) + Expect(hook.Records()).To(HaveLen(1)) + Expect(hook.LastRecord().Record.Message).To(Equal("hi")) }) }) }) diff --git a/mariobros/mariobros.go b/mariobros/mariobros.go index f6f8cd2..14bdcb2 100644 --- a/mariobros/mariobros.go +++ b/mariobros/mariobros.go @@ -7,11 +7,11 @@ // the operation, like 'level1.area4.lavapit'. Then defer the callback returned from that function, // like: // -// go func() { -// mario := mariobros.Yo("level1.area4.lavapit") -// defer mario() -// // Do more stuff... -// } +// go func() { +// mario := mariobros.Yo("level1.area4.lavapit") +// defer mario() +// // Do more stuff... +// } // // Every 5 seconds (or whatever is configured), mariobros will report on the active goroutines. // @@ -22,7 +22,6 @@ // // If Mariobros is not active, calls to Yo noop and the timer that prints does not run. // It's important to call Mariobros.Start() early, or import mariobros/autoload. -// package mariobros import ( @@ -42,10 +41,9 @@ type Writer func(totalActive uint, activePerName map[string][]GoroutineId) // StreamWriter is used when you want to write mariobros output to a stream. // The output you get is like: // -// active goroutines (1): -// my.job: 1, 5 -// other.job: 6, 7 -// +// active goroutines (1): +// my.job: 1, 5 +// other.job: 6, 7 func StreamWriter(w io.Writer) Writer { return func(totalActive uint, activePerName map[string][]GoroutineId) { w := func(s string, a ...interface{}) { @@ -67,11 +65,11 @@ func StreamWriter(w io.Writer) Writer { // KeyValueWriter is helpful when you want to log a structured message. // For example: // -// mariobros.Start(mariobros.NewOptions(func(o *mariobros.Options) { -// o.Writer = mariobros.KeyValueWriter("mariobros_", func(m map[string]interface{}) { -// logger.WithFields(m).Info("mariobros") -// }) -// })) +// mariobros.Start(mariobros.NewOptions(func(o *mariobros.Options) { +// o.Writer = mariobros.KeyValueWriter("mariobros_", func(m map[string]interface{}) { +// logger.WithFields(m).Info("mariobros") +// }) +// })) func KeyValueWriter(keyPrefix string, write func(map[string]interface{})) Writer { return func(totalActive uint, activePerName map[string][]GoroutineId) { result := make(map[string]interface{}, len(activePerName)+1) diff --git a/parallel/parallel.go b/parallel/parallel.go index 47a48c0..fbdc8e9 100644 --- a/parallel/parallel.go +++ b/parallel/parallel.go @@ -3,7 +3,7 @@ package parallel import ( "errors" "github.com/hashicorp/go-multierror" - "github.com/lithictech/go-aperitif/mariobros" + "github.com/lithictech/go-aperitif/v2/mariobros" "sync" ) diff --git a/parallel/parallel_test.go b/parallel/parallel_test.go index 774c092..725bab9 100644 --- a/parallel/parallel_test.go +++ b/parallel/parallel_test.go @@ -1,7 +1,7 @@ package parallel_test import ( - "github.com/lithictech/go-aperitif/parallel" + "github.com/lithictech/go-aperitif/v2/parallel" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sync" diff --git a/quiz/quiz_test.go b/quiz/quiz_test.go index 4e7157d..3378ee7 100644 --- a/quiz/quiz_test.go +++ b/quiz/quiz_test.go @@ -1,7 +1,7 @@ package quiz_test import ( - "github.com/lithictech/go-aperitif/quiz" + "github.com/lithictech/go-aperitif/v2/quiz" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "testing" diff --git a/stopwatch/stopwatch.go b/stopwatch/stopwatch.go index f6639a2..4283a62 100644 --- a/stopwatch/stopwatch.go +++ b/stopwatch/stopwatch.go @@ -10,27 +10,28 @@ since they can have vastly different timings package stopwatch import ( - "github.com/sirupsen/logrus" + "context" + "log/slog" "time" ) type Stopwatch struct { start time.Time operation string - logger *logrus.Entry + logger *slog.Logger } type StartOpts struct { Key string - Level logrus.Level + Level slog.Level } -func StartWith(logger *logrus.Entry, operation string, opts StartOpts) *Stopwatch { +func StartWith(ctx context.Context, logger *slog.Logger, operation string, opts StartOpts) *Stopwatch { if opts.Key == "" { opts.Key = "_started" } if opts.Level == 0 { - opts.Level = logrus.DebugLevel + opts.Level = slog.LevelDebug } sw := &Stopwatch{ start: time.Now(), @@ -38,23 +39,23 @@ func StartWith(logger *logrus.Entry, operation string, opts StartOpts) *Stopwatc logger: logger, } - sw.logger.Log(opts.Level, operation+opts.Key) + sw.logger.Log(ctx, opts.Level, operation+opts.Key) return sw } -func Start(logger *logrus.Entry, operation string) *Stopwatch { - return StartWith(logger, operation, StartOpts{}) +func Start(ctx context.Context, logger *slog.Logger, operation string) *Stopwatch { + return StartWith(ctx, logger, operation, StartOpts{}) } type FinishOpts struct { - Logger *logrus.Entry + Logger *slog.Logger Key string ElapsedKey string Milliseconds bool - Level logrus.Level + Level slog.Level } -func (sw *Stopwatch) FinishWith(opts FinishOpts) { +func (sw *Stopwatch) FinishWith(ctx context.Context, opts FinishOpts) { if opts.Key == "" { opts.Key = "_finished" } @@ -62,27 +63,27 @@ func (sw *Stopwatch) FinishWith(opts FinishOpts) { opts.ElapsedKey = "elapsed" } if opts.Level == 0 { - opts.Level = logrus.InfoLevel + opts.Level = slog.LevelInfo } if opts.Logger == nil { opts.Logger = sw.logger } logger := opts.Logger if opts.Milliseconds { - logger = logger.WithField(opts.ElapsedKey, time.Since(sw.start).Milliseconds()) + logger = logger.With(opts.ElapsedKey, time.Since(sw.start).Milliseconds()) } else { - logger = logger.WithField(opts.ElapsedKey, time.Since(sw.start).Seconds()) + logger = logger.With(opts.ElapsedKey, time.Since(sw.start).Seconds()) } - logger.Log(opts.Level, sw.operation+opts.Key) + logger.Log(ctx, opts.Level, sw.operation+opts.Key) } -func (sw *Stopwatch) Finish() { - sw.FinishWith(FinishOpts{}) +func (sw *Stopwatch) Finish(ctx context.Context) { + sw.FinishWith(ctx, FinishOpts{}) } type LapOpts FinishOpts -func (sw *Stopwatch) LapWith(opts LapOpts) { +func (sw *Stopwatch) LapWith(ctx context.Context, opts LapOpts) { if opts.Key == "" { opts.Key = "_lap" } @@ -90,14 +91,14 @@ func (sw *Stopwatch) LapWith(opts LapOpts) { opts.ElapsedKey = "elapsed" } if opts.Level == 0 { - opts.Level = logrus.InfoLevel + opts.Level = slog.LevelInfo } if opts.Logger == nil { opts.Logger = sw.logger } - sw.FinishWith(FinishOpts(opts)) + sw.FinishWith(ctx, FinishOpts(opts)) } -func (sw *Stopwatch) Lap() { - sw.LapWith(LapOpts{}) +func (sw *Stopwatch) Lap(ctx context.Context) { + sw.LapWith(ctx, LapOpts{}) } diff --git a/stopwatch/stopwatch_test.go b/stopwatch/stopwatch_test.go index 5d3e892..466e732 100644 --- a/stopwatch/stopwatch_test.go +++ b/stopwatch/stopwatch_test.go @@ -1,11 +1,12 @@ package stopwatch_test import ( - "github.com/lithictech/go-aperitif/stopwatch" + "context" + "github.com/lithictech/go-aperitif/v2/logctx" + "github.com/lithictech/go-aperitif/v2/stopwatch" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/sirupsen/logrus" - "github.com/sirupsen/logrus/hooks/test" + "log/slog" "testing" ) @@ -15,67 +16,64 @@ func TestStopwatch(t *testing.T) { } var _ = Describe("Stopwatch", func() { - var logger *logrus.Logger - var entry *logrus.Entry - var hook *test.Hook + var logger *slog.Logger + var hook *logctx.Hook + var ctx context.Context BeforeEach(func() { - logger, hook = test.NewNullLogger() - logger.SetLevel(logrus.DebugLevel) - entry = logger.WithFields(nil) + logger, hook = logctx.NewNullLogger() + ctx = logctx.WithLogger(context.Background(), logger) }) + It("logs start and stop", func() { - sw := stopwatch.Start(entry, "test") - sw.Finish() - Expect(hook.Entries).To(HaveLen(2)) + sw := stopwatch.Start(ctx, logger, "test") + sw.Finish(ctx) + Expect(hook.Records()).To(HaveLen(2)) - Expect(hook.Entries[0].Level).To(Equal(logrus.DebugLevel)) - Expect(hook.Entries[0].Message).To(ContainSubstring("test_started")) + Expect(hook.Records()[0].Record.Level).To(Equal(slog.LevelDebug)) + Expect(hook.Records()[0].Record.Message).To(ContainSubstring("test_started")) - Expect(hook.Entries[1].Level).To(Equal(logrus.InfoLevel)) - Expect(hook.Entries[1].Message).To(ContainSubstring("test_finished")) + Expect(hook.Records()[1].Record.Level).To(Equal(slog.LevelInfo)) + Expect(hook.Records()[1].Record.Message).To(ContainSubstring("test_finished")) }) It("can custom start and stop", func() { - sw := stopwatch.StartWith(entry, "test", stopwatch.StartOpts{Level: logrus.WarnLevel, Key: "_begin"}) - sw.FinishWith(stopwatch.FinishOpts{Level: logrus.ErrorLevel, Key: "_end", ElapsedKey: "timing"}) - Expect(hook.Entries).To(HaveLen(2)) + sw := stopwatch.StartWith(ctx, logger, "test", stopwatch.StartOpts{Level: slog.LevelWarn, Key: "_begin"}) + sw.FinishWith(ctx, stopwatch.FinishOpts{Level: slog.LevelError, Key: "_end", ElapsedKey: "timing"}) + Expect(hook.Records()).To(HaveLen(2)) - Expect(hook.Entries[0].Level).To(Equal(logrus.WarnLevel)) - Expect(hook.Entries[0].Message).To(ContainSubstring("test_begin")) + Expect(hook.Records()[0].Record.Level).To(Equal(slog.LevelWarn)) + Expect(hook.Records()[0].Record.Message).To(ContainSubstring("test_begin")) - Expect(hook.Entries[1].Level).To(Equal(logrus.ErrorLevel)) - Expect(hook.Entries[1].Message).To(ContainSubstring("test_end")) - Expect(hook.Entries[1].Data).To(HaveKey("timing")) + Expect(hook.Records()[1].Record.Level).To(Equal(slog.LevelError)) + Expect(hook.Records()[1].Record.Message).To(ContainSubstring("test_end")) + Expect(hook.Records()[1].AttrMap()).To(HaveKey("timing")) }) It("can use a custom finish logger", func() { - startLogger, startHook := test.NewNullLogger() - startLogger.SetLevel(logrus.DebugLevel) - - finishLogger, finishHook := test.NewNullLogger() - finishLogger.SetLevel(logrus.DebugLevel) + startLogger, startHook := logctx.NewNullLogger() + finishLogger, finishHook := logctx.NewNullLogger() - sw := stopwatch.Start(startLogger.WithFields(nil), "test") + sw := stopwatch.Start(ctx, startLogger, "test") - sw.FinishWith(stopwatch.FinishOpts{Logger: finishLogger.WithFields(nil)}) + sw.FinishWith(ctx, stopwatch.FinishOpts{Logger: finishLogger}) - Expect(startHook.Entries).To(HaveLen(1)) - Expect(finishHook.Entries).To(HaveLen(1)) + Expect(startHook.Records()).To(HaveLen(1)) + Expect(finishHook.Records()).To(HaveLen(1)) }) It("can lap", func() { - sw := stopwatch.Start(entry, "test") - sw.Lap() - sw.LapWith(stopwatch.LapOpts{Key: "_split", Level: logrus.WarnLevel, ElapsedKey: "timing"}) - Expect(hook.Entries).To(HaveLen(3)) - - Expect(hook.Entries[1].Level).To(Equal(logrus.InfoLevel)) - Expect(hook.Entries[1].Message).To(ContainSubstring("test_lap")) - Expect(hook.Entries[1].Data).To(HaveKey("elapsed")) - - Expect(hook.Entries[2].Level).To(Equal(logrus.WarnLevel)) - Expect(hook.Entries[2].Message).To(ContainSubstring("test_split")) - Expect(hook.Entries[2].Data).To(HaveKey("timing")) + sw := stopwatch.Start(ctx, logger, "test") + sw.Lap(ctx) + sw.LapWith(ctx, stopwatch.LapOpts{Key: "_split", Level: slog.LevelWarn, ElapsedKey: "timing"}) + Expect(hook.Records()).To(HaveLen(3)) + + Expect(hook.Records()[1].Record.Level).To(Equal(slog.LevelInfo)) + Expect(hook.Records()[1].Record.Message).To(ContainSubstring("test_lap")) + Expect(hook.Records()[1].AttrMap()).To(HaveKey("elapsed")) + + Expect(hook.Records()[2].Record.Level).To(Equal(slog.LevelWarn)) + Expect(hook.Records()[2].Record.Message).To(ContainSubstring("test_split")) + Expect(hook.Records()[2].AttrMap()).To(HaveKey("timing")) }) }) diff --git a/stringutil/stringutil.go b/stringutil/stringutil.go deleted file mode 100644 index aedb198..0000000 --- a/stringutil/stringutil.go +++ /dev/null @@ -1,21 +0,0 @@ -package stringutil - -// Map applies f to each string in in. -func Map(in []string, f func(string) string) []string { - res := make([]string, 0, len(in)) - for _, s := range in { - res = append(res, f(s)) - } - return res -} - -// Contains returns true if in contains element, -// false if not. -func Contains(in []string, element string) bool { - for _, a := range in { - if a == element { - return true - } - } - return false -} diff --git a/stringutil/stringutil_test.go b/stringutil/stringutil_test.go deleted file mode 100644 index 61ab494..0000000 --- a/stringutil/stringutil_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package stringutil_test - -import ( - "github.com/lithictech/go-aperitif/stringutil" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "strings" - "testing" -) - -func TestStringUtil(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "stringutil Suite") -} - -var _ = Describe("Map", func() { - It("maps the input slice", func() { - s := []string{"a", "b"} - res := stringutil.Map(s, strings.ToUpper) - Expect(res).To(Equal([]string{"A", "B"})) - }) -}) - -var _ = Describe("Contains", func() { - It("is true if the slice contains the string", func() { - s := []string{"a", "b"} - Expect(stringutil.Contains(s, "a")).To(BeTrue()) - Expect(stringutil.Contains(s, "A")).To(BeFalse()) - }) -}) diff --git a/validator/doc.go b/validator/doc.go index 340d56a..77ae845 100644 --- a/validator/doc.go +++ b/validator/doc.go @@ -91,7 +91,7 @@ Available validators include: (validation will only be done if a value is provided). (Usage: comparenow=hour|gte comparenow=day|lt|opt) -Optional validations +# Optional validations Most validators support a way to specify they are optional. Usually that is something like providing "opt" as a value, like `intid=opt`, @@ -100,7 +100,7 @@ See example usages for details. Nil pointers are generally considered valid. See Pointers section for more details. -Pointers +# Pointers If validator is validating a pointer field, it will generally validate the underlying type the same as non-pointer fields. The only real difference is that a nil pointer will be considered valid, @@ -109,15 +109,14 @@ because pointer fields generally specify a value is optional. If a nil pointer isn't valid for a pointer field, you can use the "nonzero" validation. For example, a nil pointer is acceptable here, even though there is no trailing "|opt" flag: - type d struct { - D *time.Time `json:"d" validate:"comparenow=lte|day"` - } + type d struct { + D *time.Time `json:"d" validate:"comparenow=lte|day"` + } However, a nil pointer is not acceptable here, because of the "nonzero" validation: - type d struct { - D *time.Time `json:"d" validate:"comparenow=lte|day,nonzero"` - } - + type d struct { + D *time.Time `json:"d" validate:"comparenow=lte|day,nonzero"` + } */ package validator diff --git a/validator/stringutil.go b/validator/stringutil.go new file mode 100644 index 0000000..8ea7f8f --- /dev/null +++ b/validator/stringutil.go @@ -0,0 +1,18 @@ +package validator + +func mapString(in []string, f func(string) string) []string { + res := make([]string, 0, len(in)) + for _, s := range in { + res = append(res, f(s)) + } + return res +} + +func containsString(in []string, element string) bool { + for _, a := range in { + if a == element { + return true + } + } + return false +} diff --git a/validator/validator_test.go b/validator/validator_test.go index f79ba31..dad41f3 100644 --- a/validator/validator_test.go +++ b/validator/validator_test.go @@ -2,7 +2,7 @@ package validator_test import ( "errors" - "github.com/lithictech/go-aperitif/validator" + "github.com/lithictech/go-aperitif/v2/validator" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "testing" diff --git a/validator/validators.go b/validator/validators.go index 38ef98a..53817b3 100644 --- a/validator/validators.go +++ b/validator/validators.go @@ -2,8 +2,7 @@ package validator import ( "errors" - "github.com/lithictech/go-aperitif/kronos" - "github.com/lithictech/go-aperitif/stringutil" + "github.com/lithictech/go-aperitif/v2/kronos" "github.com/rgalanakis/validator" "net/url" "regexp" @@ -29,9 +28,10 @@ const optional = "opt" // Split the param string on |, // and return a type of (other args, if param ends in |opt, error in the case of empty args). // Examples: -// "a|b" -> (["a", "b"], false, nil) -// "a|opt" -> (["a"], true, nil) -// "|opt" -> ([], false, ) +// +// "a|b" -> (["a", "b"], false, nil) +// "a|opt" -> (["a"], true, nil) +// "|opt" -> ([], false, ) func splitOptionalVal(param string) ([]string, bool, error) { params := strings.Split(param, "|") if len(params) == 0 { @@ -91,7 +91,7 @@ func validateEnumImpl(v interface{}, param string, mapper func(string) string) e return err } if mapper != nil { - choices = stringutil.Map(choices, mapper) + choices = mapString(choices, mapper) } if s, ok := v.(string); ok { @@ -109,7 +109,7 @@ func validateEnumImpl(v interface{}, param string, mapper func(string) string) e return validator.ErrBadParameter } if mapper != nil { - ss = stringutil.Map(ss, mapper) + ss = mapString(ss, mapper) } return validateEnumImplSlice(ss, choices) } @@ -138,7 +138,7 @@ func validateEnumImplStr(s string, choices []string, optional bool) error { func validateEnumImplSlice(ss []string, choices []string) error { for _, s := range ss { - if !stringutil.Contains(choices, s) { + if !containsString(choices, s) { return newError("element not one of " + strings.Join(choices, "|")) } }