Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 4 additions & 10 deletions docs/codecs.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,17 @@ 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 (
"github.com/Suhaibinator/SRouter/pkg/codec"
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
Expand Down Expand Up @@ -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.
2 changes: 1 addition & 1 deletion docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
5 changes: 1 addition & 4 deletions examples/codec/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]{
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/Suhaibinator/SRouter

go 1.24.0
go 1.26.0
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep go.mod language version aligned with CI toolchain

Raising the module directive to go 1.26.0 breaks this repository’s own CI configuration, which is still pinned to Go 1.24 in .github/workflows/tests.yml (go-version: '1.24' in the test/lint/build/benchmark/examples jobs). With this commit, those jobs fail before running tests because go exits with go.mod requires go >= 1.26.0, so builds and checks are blocked until the workflow toolchain is upgraded in the same change.

Useful? React with 👍 / 👎.


require (
github.com/julienschmidt/httprouter v1.3.0
Expand All @@ -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
)

Expand All @@ -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
)

Expand Down
20 changes: 10 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
40 changes: 18 additions & 22 deletions pkg/codec/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,39 @@ 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)
},
}
}

// For testing purposes, we expose these variables so they can be overridden in tests
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()
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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.
28 changes: 15 additions & 13 deletions pkg/codec/proto_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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{})
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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.