diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e2925aa..e60a521 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.24'] + go-version: ['1.26.1'] os: [ubuntu-latest, macos-latest, windows-latest] fail-fast: true @@ -60,7 +60,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24' + go-version: '1.26.1' cache: true - name: Install golangci-lint @@ -74,7 +74,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - go-version: ['1.24'] + go-version: ['1.26.1'] os: [ubuntu-latest, macos-latest, windows-latest] steps: @@ -100,7 +100,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24' + go-version: '1.26.1' cache: true - name: Run benchmarks @@ -116,7 +116,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.24' + go-version: '1.26.1' cache: true - name: Build simple example diff --git a/README.md b/README.md index 8d42a1c..094c36f 100644 --- a/README.md +++ b/README.md @@ -1411,14 +1411,10 @@ codec.NewJSONCodec[T, U]() *codec.JSONCodec[T, U] ### ProtoCodec -Uses Protocol Buffers for marshaling and unmarshaling. Requires a factory function to create new request message instances without reflection. +Uses Protocol Buffers for marshaling and unmarshaling. It allocates fresh request messages without reflection. ```go -// Define a factory function for your specific proto message type (e.g., *MyProto) -myProtoFactory := func() *pb.MyProto { return &pb.MyProto{} } // Use generated type - -// Pass the factory to the constructor -codec.NewProtoCodec[T, U](myProtoFactory) *codec.ProtoCodec[T, U] // T must match factory return type +codec.NewProtoCodec[T, U]() *codec.ProtoCodec[T, U] ``` ### Codec Interface diff --git a/docs/codecs.md b/docs/codecs.md index 1c02982..b9fdd9e 100644 --- a/docs/codecs.md +++ b/docs/codecs.md @@ -60,7 +60,7 @@ route := router.RouteConfig[MyRequest, MyResponse]{ Handles Protocol Buffers encoding and decoding using Google's `protobuf` libraries (e.g., `google.golang.org/protobuf/proto`). -**Important:** Due to the nature of Protocol Buffers, the `ProtoCodec` requires a factory function (`codec.ProtoRequestFactory`) when being constructed. This function must return a new, zero-value instance of the specific *request* proto message type (`T`). This is needed internally to provide a concrete type for unmarshaling without relying on reflection. +**Important:** `ProtoCodec` infers the concrete request message type from `T`, so it can allocate fresh zero-value messages for unmarshaling without reflection. ```go import ( @@ -68,15 +68,9 @@ import ( pb "path/to/your/generated/proto/package" // Import your generated proto package ) -// Define the factory function for your request proto message type -// It must return the specific pointer type (e.g., *pb.MyRequestProto) -var myRequestFactory = func() *pb.MyRequestProto { - return &pb.MyRequestProto{} -} - -// Create a new Proto codec, providing the factory function +// Create a new Proto codec. // T is *pb.MyRequestProto, U is *pb.MyResponseProto (or appropriate response type) -protoCodec := codec.NewProtoCodec[*pb.MyRequestProto, *pb.MyResponseProto](myRequestFactory) +protoCodec := codec.NewProtoCodec[*pb.MyRequestProto, *pb.MyResponseProto]() // Use it in RouteConfig @@ -193,6 +187,6 @@ Remember to handle errors appropriately within your codec methods, potentially r - **`codec.Codec[T, U]`**: Interface defining methods `NewRequest() T`, `Decode(*http.Request) (T, error)`, `DecodeBytes([]byte) (T, error)`, and `Encode(http.ResponseWriter, U) error`. - **`codec.NewJSONCodec[T, U]() *codec.JSONCodec[T, U]`**: Constructor for the built-in JSON codec. -- **`codec.NewProtoCodec[T, U](factory codec.ProtoRequestFactory[T]) *codec.ProtoCodec[T, U]`**: Constructor for the built-in Protocol Buffers codec, requiring a factory function for the request type `T`. +- **`codec.NewProtoCodec[T, U]() *codec.ProtoCodec[T, U]`**: Constructor for the built-in Protocol Buffers codec. See the `examples/codec` directory for runnable examples using different codecs. diff --git a/docs/examples.md b/docs/examples.md index 2a59b4c..714db5e 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -26,7 +26,7 @@ Here's a brief overview of the available examples (refer to the source code and - **`/examples/trace-logging`**: Demonstrates enabling and using trace IDs for correlating logs within a request lifecycle. - **`/examples/cors-error-test`**: Demonstrates handling CORS preflight and error scenarios, including how SRouter writes CORS headers on error responses. - **`/examples/source-types`**: Shows how to use different `SourceType` options (Body, Base64QueryParameter, Base64PathParameter, etc.) for generic routes. -- **`/examples/codec`**: Illustrates using different codecs, particularly `JSONCodec` and `ProtoCodec` (including the required factory function for proto). +- **`/examples/codec`**: Illustrates using different codecs, particularly `JSONCodec` and `ProtoCodec`. - **`/examples/prometheus`**: Example of integrating SRouter's metrics system with Prometheus by providing a Prometheus-based implementation of the `metrics.MetricsRegistry` interface and showing how the application can expose the metrics via an HTTP handler. - **`/examples/custom-metrics`**: Demonstrates implementing a custom `metrics.MetricsRegistry` or `metrics.MetricsMiddleware`. - **`/examples/handler-error-middleware`**: Shows how middleware can access errors returned by generic handlers to make decisions (e.g., transaction rollback, custom error logging) using `scontext.GetHandlerErrorFromRequest`. diff --git a/examples/codec/main.go b/examples/codec/main.go index ead41ad..18f1c93 100644 --- a/examples/codec/main.go +++ b/examples/codec/main.go @@ -65,10 +65,7 @@ func main() { }, placeholderAuth, placeholderGetUserID) // Instantiate the ProtoCodec (can be done once outside the handler if reused) - // Create a factory function for User messages - userFactory := func() *pb.User { return &pb.User{} } - // Create a ProtoCodec for User messages, providing the factory - protoCodec := codec.NewProtoCodec[*pb.User, *pb.User](userFactory) + protoCodec := codec.NewProtoCodec[*pb.User, *pb.User]() // Define the generic route configuration routeCfg := router.RouteConfig[*pb.User, *pb.User]{ diff --git a/go.mod b/go.mod index 224ee55..6283146 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/Suhaibinator/SRouter -go 1.24.0 +go 1.26.0 require ( github.com/julienschmidt/httprouter v1.3.0 @@ -19,8 +19,8 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/text v0.32.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) @@ -30,10 +30,10 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.67.4 // indirect - github.com/prometheus/procfs v0.19.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.20.1 // indirect go.uber.org/ratelimit v0.3.1 - golang.org/x/sys v0.39.0 // indirect + golang.org/x/sys v0.42.0 // indirect google.golang.org/protobuf v1.36.11 ) diff --git a/go.sum b/go.sum index ca0d78e..c472c43 100644 --- a/go.sum +++ b/go.sum @@ -34,10 +34,10 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= -github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= -github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= -github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -52,12 +52,12 @@ go.uber.org/ratelimit v0.3.1 h1:K4qVE+byfv/B3tC+4nYWP7v/6SimcO7HzHekoMNBma0= go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJhRk= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/codec/proto.go b/pkg/codec/proto.go index 3e73c14..e247e94 100644 --- a/pkg/codec/proto.go +++ b/pkg/codec/proto.go @@ -8,31 +8,30 @@ import ( "google.golang.org/protobuf/proto" ) -// ProtoRequestFactory is a function type that creates new instances of protobuf request messages. -// It returns a pointer to a type implementing proto.Message. This factory pattern is used -// to avoid reflection when creating new message instances for decoding. -// T must be a pointer type (e.g., *MyRequest) that implements proto.Message. -type ProtoRequestFactory[T proto.Message] func() T - // ProtoCodec implements the Codec interface for Protocol Buffers. // It handles marshaling and unmarshaling of protobuf messages for use with generic routes. // Both T and U must be pointer types that implement proto.Message (e.g., *MyRequest, *MyResponse). type ProtoCodec[T proto.Message, U proto.Message] struct { - // Factory function to create new request objects without reflection. - newRequest ProtoRequestFactory[T] + newRequest func() T } -// NewProtoCodec creates a new ProtoCodec instance with the provided request factory. -// The factory function is used to create new instances of the request type without reflection. +// NewProtoCodec creates a new ProtoCodec instance for protobuf request/response types. +// It infers the underlying message type from T and allocates fresh zero-value messages +// without reflection by using Go's new(expr) support. // // Example: // -// codec := NewProtoCodec[*pb.CreateUserReq, *pb.CreateUserResp](func() *pb.CreateUserReq { -// return &pb.CreateUserReq{} -// }) -func NewProtoCodec[T proto.Message, U proto.Message](factory ProtoRequestFactory[T]) *ProtoCodec[T, U] { +// codec := NewProtoCodec[*pb.CreateUserReq, *pb.CreateUserResp]() +func NewProtoCodec[T interface { + proto.Message + *M +}, U proto.Message, M any]() *ProtoCodec[T, U] { + + var zero M return &ProtoCodec[T, U]{ - newRequest: factory, + newRequest: func() T { + return new(zero) + }, } } @@ -40,9 +39,8 @@ func NewProtoCodec[T proto.Message, U proto.Message](factory ProtoRequestFactory var protoUnmarshal = proto.Unmarshal var protoMarshal = proto.Marshal -// NewRequest creates a new instance of the request protobuf message using the factory. -// It implements the Codec interface. The factory pattern avoids the need for reflection -// when creating new message instances. +// NewRequest creates a new instance of the request protobuf message. +// It implements the Codec interface. func (c *ProtoCodec[T, U]) NewRequest() T { return c.newRequest() } @@ -52,7 +50,7 @@ func (c *ProtoCodec[T, U]) NewRequest() T { // body is closed after reading. Returns an error if the data is not valid protobuf // or doesn't match the expected message type. func (c *ProtoCodec[T, U]) Decode(r *http.Request) (T, error) { - msg := c.NewRequest() // Use the factory to create a new message instance + msg := c.NewRequest() body, err := io.ReadAll(r.Body) if err != nil { @@ -73,7 +71,7 @@ func (c *ProtoCodec[T, U]) Decode(r *http.Request) (T, error) { // data comes from sources other than the request body (e.g., base64-encoded // query parameters). Returns an error if the data is invalid. func (c *ProtoCodec[T, U]) DecodeBytes(data []byte) (T, error) { - msg := c.NewRequest() // Use the factory to create a new message instance + msg := c.NewRequest() // Unmarshal directly from the provided data if err := protoUnmarshal(data, msg); err != nil { @@ -96,5 +94,3 @@ func (c *ProtoCodec[T, U]) Encode(w http.ResponseWriter, resp U) error { _, err = w.Write(bytes) return err } - -// newMessage function is removed as it's replaced by the factory approach. diff --git a/pkg/codec/proto_test.go b/pkg/codec/proto_test.go index 08b80a9..41ecf91 100644 --- a/pkg/codec/proto_test.go +++ b/pkg/codec/proto_test.go @@ -21,26 +21,30 @@ func (m *TestProtoMessage) String() string { return string(m func (m *TestProtoMessage) ProtoMessage() {} func (m *TestProtoMessage) ProtoReflect() protoreflect.Message { return nil } -// testProtoFactory creates a new TestProtoMessage instance. -func testProtoFactory() *TestProtoMessage { - return &TestProtoMessage{} -} - // TestNewProtoCodec tests the NewProtoCodec function func TestNewProtoCodec(t *testing.T) { - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() if codec == nil { t.Error("Expected non-nil codec") } if codec == nil || codec.newRequest == nil { t.Error("Expected codec.newRequest to be set") } + + req := codec.NewRequest() + if req == nil { + t.Fatal("Expected NewRequest to allocate a message") + } + + if req == codec.NewRequest() { + t.Error("Expected NewRequest to return a distinct message instance") + } } // TestProtoCodecDecode tests the Decode method of ProtoCodec func TestProtoCodecDecode(t *testing.T) { // Create a codec - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() // Create a request with test data reqBody := []byte("test data") @@ -81,7 +85,7 @@ func TestProtoCodecDecode(t *testing.T) { // TestProtoCodecEncode tests the Encode method of ProtoCodec func TestProtoCodecEncode(t *testing.T) { // Create a codec - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() // Create a mock implementation of proto.Marshal originalMarshal := protoMarshal @@ -125,7 +129,7 @@ func TestProtoCodecEncode(t *testing.T) { // TestProtoCodecDecodeBytes tests the DecodeBytes method of ProtoCodec func TestProtoCodecDecodeBytes(t *testing.T) { // Create a codec - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() // Test data (simulating decoded bytes from Base64/Base62) testBytes := []byte("test byte data") @@ -177,7 +181,7 @@ func TestProtoCodecDecodeBytes(t *testing.T) { // TestProtoCodecDecodeErrors tests error handling in the Decode method of ProtoCodec func TestProtoCodecDecodeErrors(t *testing.T) { // Create a codec - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() // Test Decode with read error req := httptest.NewRequest("POST", "/test", &errorReader{}) @@ -209,7 +213,7 @@ func TestProtoCodecDecodeErrors(t *testing.T) { // TestProtoCodecEncodeErrors tests error handling in the Encode method of ProtoCodec func TestProtoCodecEncodeErrors(t *testing.T) { // Create a codec - codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage](testProtoFactory) + codec := NewProtoCodec[*TestProtoMessage, *TestProtoMessage]() // Test Encode with marshal error rr := httptest.NewRecorder() @@ -241,5 +245,3 @@ func TestProtoCodecEncodeErrors(t *testing.T) { t.Error("Expected error when writing response fails") } } - -// TestNewMessage is removed as the newMessage function no longer exists.