From 403c2a0155a79b63bc1f37d186422c5061a421ac Mon Sep 17 00:00:00 2001 From: Divya Darshana <98943137+coolwednesday@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:03:20 +0530 Subject: [PATCH 1/7] Initialising Tracing irrespective of configs (#1072) --- pkg/gofr/gofr.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/gofr/gofr.go b/pkg/gofr/gofr.go index 76fc2f708..ab4ae49c4 100644 --- a/pkg/gofr/gofr.go +++ b/pkg/gofr/gofr.go @@ -385,17 +385,6 @@ func (a *App) Migrate(migrationsMap map[int64]migration.Migrate) { } func (a *App) initTracer() { - traceExporter := a.Config.Get("TRACE_EXPORTER") - tracerURL := a.Config.Get("TRACER_URL") - - // deprecated : tracer_host and tracer_port are deprecated and will be removed in upcoming versions. - tracerHost := a.Config.Get("TRACER_HOST") - tracerPort := a.Config.GetOrDefault("TRACER_PORT", "9411") - - if !isValidConfig(a.Logger(), traceExporter, tracerURL, tracerHost, tracerPort) { - return - } - traceRatio, err := strconv.ParseFloat(a.Config.GetOrDefault("TRACER_RATIO", "1"), 64) if err != nil { a.container.Error(err) @@ -412,6 +401,17 @@ func (a *App) initTracer() { otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) otel.SetErrorHandler(&otelErrorHandler{logger: a.container.Logger}) + traceExporter := a.Config.Get("TRACE_EXPORTER") + tracerURL := a.Config.Get("TRACER_URL") + + // deprecated : tracer_host and tracer_port are deprecated and will be removed in upcoming versions. + tracerHost := a.Config.Get("TRACER_HOST") + tracerPort := a.Config.GetOrDefault("TRACER_PORT", "9411") + + if !isValidConfig(a.Logger(), traceExporter, tracerURL, tracerHost, tracerPort) { + return + } + exporter, err := a.getExporter(traceExporter, tracerHost, tracerPort, tracerURL) if err != nil { From 6b307b50959602ad297ac04e5925037097473588 Mon Sep 17 00:00:00 2001 From: Aryan Mehrotra <44036979+aryanmehrotra@users.noreply.github.com> Date: Fri, 4 Oct 2024 11:18:48 +0530 Subject: [PATCH 2/7] update go mod name for eventhub (#1075) --- pkg/gofr/datasource/pubsub/eventhub/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/datasource/pubsub/eventhub/go.mod b/pkg/gofr/datasource/pubsub/eventhub/go.mod index a13d76db3..ddf0a7b95 100644 --- a/pkg/gofr/datasource/pubsub/eventhub/go.mod +++ b/pkg/gofr/datasource/pubsub/eventhub/go.mod @@ -1,4 +1,4 @@ -module gofr.dev/pkg/gofr/datasource/pubsub/azeventhub +module gofr.dev/pkg/gofr/datasource/pubsub/eventhub go 1.22.3 From 47069d0bb6fd92774e302f5c84f3b924411a0f9e Mon Sep 17 00:00:00 2001 From: Umang Mundhra Date: Fri, 4 Oct 2024 14:51:16 +0530 Subject: [PATCH 3/7] Tracing support added for MongoDB database --- pkg/gofr/datasource/mongo/mongo.go | 117 ++++++++++++++++++------ pkg/gofr/datasource/mongo/mongo_test.go | 29 +++--- pkg/gofr/external_db.go | 5 + pkg/gofr/external_db_test.go | 1 + 4 files changed, 112 insertions(+), 40 deletions(-) diff --git a/pkg/gofr/datasource/mongo/mongo.go b/pkg/gofr/datasource/mongo/mongo.go index ecf084b94..08c6d9d82 100644 --- a/pkg/gofr/datasource/mongo/mongo.go +++ b/pkg/gofr/datasource/mongo/mongo.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "go.mongodb.org/mongo-driver/bson" @@ -49,7 +50,7 @@ i.e. by default observability features gets initialised when used with GoFr. // client := New(config) // client.UseLogger(loggerInstance) // client.UseMetrics(metricsInstance) -// client.Connect() +// client.Connect(). func New(c Config) *Client { return &Client{config: c} } @@ -101,28 +102,36 @@ func (c *Client) Connect() { // InsertOne inserts a single document into the specified collection. func (c *Client) InsertOne(ctx context.Context, collection string, document interface{}) (interface{}, error) { - defer c.sendOperationStats(&QueryLog{Query: "insertOne", Collection: collection, Filter: document}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "insertOne", collection) - return c.Database.Collection(collection).InsertOne(ctx, document) + result, err := c.Database.Collection(collection).InsertOne(tracerCtx, document) + + defer c.sendOperationStats(&QueryLog{Query: "insertOne", Collection: collection, Filter: document}, time.Now(), + "insert", span) + + return result, err } // InsertMany inserts multiple documents into the specified collection. func (c *Client) InsertMany(ctx context.Context, collection string, documents []interface{}) ([]interface{}, error) { - defer c.sendOperationStats(&QueryLog{Query: "insertMany", Collection: collection, Filter: documents}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "insertMany", collection) - res, err := c.Database.Collection(collection).InsertMany(ctx, documents) + res, err := c.Database.Collection(collection).InsertMany(tracerCtx, documents) if err != nil { return nil, err } + defer c.sendOperationStats(&QueryLog{Query: "insertMany", Collection: collection, Filter: documents}, time.Now(), + "insertMany", span) + return res.InsertedIDs, nil } // Find retrieves documents from the specified collection based on the provided filter and binds response to result. func (c *Client) Find(ctx context.Context, collection string, filter, results interface{}) error { - defer c.sendOperationStats(&QueryLog{Query: "find", Collection: collection, Filter: filter}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "find", collection) - cur, err := c.Database.Collection(collection).Find(ctx, filter) + cur, err := c.Database.Collection(collection).Find(tracerCtx, filter) if err != nil { return err } @@ -133,94 +142,129 @@ func (c *Client) Find(ctx context.Context, collection string, filter, results in return err } + defer c.sendOperationStats(&QueryLog{Query: "find", Collection: collection, Filter: filter}, time.Now(), "find", + span) + return nil } // FindOne retrieves a single document from the specified collection based on the provided filter and binds response to result. func (c *Client) FindOne(ctx context.Context, collection string, filter, result interface{}) error { - defer c.sendOperationStats(&QueryLog{Query: "findOne", Collection: collection, Filter: filter}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "findOne", collection) - b, err := c.Database.Collection(collection).FindOne(ctx, filter).Raw() + b, err := c.Database.Collection(collection).FindOne(tracerCtx, filter).Raw() if err != nil { return err } + defer c.sendOperationStats(&QueryLog{Query: "findOne", Collection: collection, Filter: filter}, time.Now(), + "findOne", span) + return bson.Unmarshal(b, result) } // UpdateByID updates a document in the specified collection by its ID. func (c *Client) UpdateByID(ctx context.Context, collection string, id, update interface{}) (int64, error) { - defer c.sendOperationStats(&QueryLog{Query: "updateByID", Collection: collection, ID: id, Update: update}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "updateByID", collection) + + res, err := c.Database.Collection(collection).UpdateByID(tracerCtx, id, update) - res, err := c.Database.Collection(collection).UpdateByID(ctx, id, update) + defer c.sendOperationStats(&QueryLog{Query: "updateByID", Collection: collection, ID: id, Update: update}, time.Now(), + "updateByID", span) return res.ModifiedCount, err } // UpdateOne updates a single document in the specified collection based on the provided filter. func (c *Client) UpdateOne(ctx context.Context, collection string, filter, update interface{}) error { - defer c.sendOperationStats(&QueryLog{Query: "updateOne", Collection: collection, Filter: filter, Update: update}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "updateOne", collection) + + _, err := c.Database.Collection(collection).UpdateOne(tracerCtx, filter, update) - _, err := c.Database.Collection(collection).UpdateOne(ctx, filter, update) + defer c.sendOperationStats(&QueryLog{Query: "updateOne", Collection: collection, Filter: filter, Update: update}, + time.Now(), "updateOne", span) return err } // UpdateMany updates multiple documents in the specified collection based on the provided filter. func (c *Client) UpdateMany(ctx context.Context, collection string, filter, update interface{}) (int64, error) { - defer c.sendOperationStats(&QueryLog{Query: "updateMany", Collection: collection, Filter: filter, Update: update}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "updateMany", collection) - res, err := c.Database.Collection(collection).UpdateMany(ctx, filter, update) + res, err := c.Database.Collection(collection).UpdateMany(tracerCtx, filter, update) + + defer c.sendOperationStats(&QueryLog{Query: "updateMany", Collection: collection, Filter: filter, Update: update}, time.Now(), + "updateMany", span) return res.ModifiedCount, err } // CountDocuments counts the number of documents in the specified collection based on the provided filter. func (c *Client) CountDocuments(ctx context.Context, collection string, filter interface{}) (int64, error) { - defer c.sendOperationStats(&QueryLog{Query: "countDocuments", Collection: collection, Filter: filter}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "countDocuments", collection) + + result, err := c.Database.Collection(collection).CountDocuments(tracerCtx, filter) + + defer c.sendOperationStats(&QueryLog{Query: "countDocuments", Collection: collection, Filter: filter}, time.Now(), + "countDocuments", span) - return c.Database.Collection(collection).CountDocuments(ctx, filter) + return result, err } // DeleteOne deletes a single document from the specified collection based on the provided filter. func (c *Client) DeleteOne(ctx context.Context, collection string, filter interface{}) (int64, error) { - defer c.sendOperationStats(&QueryLog{Query: "deleteOne", Collection: collection, Filter: filter}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "deleteOne", collection) - res, err := c.Database.Collection(collection).DeleteOne(ctx, filter) + res, err := c.Database.Collection(collection).DeleteOne(tracerCtx, filter) if err != nil { return 0, err } + defer c.sendOperationStats(&QueryLog{Query: "deleteOne", Collection: collection, Filter: filter}, time.Now(), + "deleteOne", span) + return res.DeletedCount, nil } // DeleteMany deletes multiple documents from the specified collection based on the provided filter. func (c *Client) DeleteMany(ctx context.Context, collection string, filter interface{}) (int64, error) { - defer c.sendOperationStats(&QueryLog{Query: "deleteMany", Collection: collection, Filter: filter}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "deleteMany", collection) - res, err := c.Database.Collection(collection).DeleteMany(ctx, filter) + res, err := c.Database.Collection(collection).DeleteMany(tracerCtx, filter) if err != nil { return 0, err } + defer c.sendOperationStats(&QueryLog{Query: "deleteMany", Collection: collection, Filter: filter}, time.Now(), + "deleteMany", span) + return res.DeletedCount, nil } // Drop drops the specified collection from the database. func (c *Client) Drop(ctx context.Context, collection string) error { - defer c.sendOperationStats(&QueryLog{Query: "drop", Collection: collection}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "drop", collection) + + err := c.Database.Collection(collection).Drop(tracerCtx) - return c.Database.Collection(collection).Drop(ctx) + defer c.sendOperationStats(&QueryLog{Query: "drop", Collection: collection}, time.Now(), "drop", span) + + return err } // CreateCollection creates the specified collection in the database. func (c *Client) CreateCollection(ctx context.Context, name string) error { - defer c.sendOperationStats(&QueryLog{Query: "createCollection", Collection: name}, time.Now()) + tracerCtx, span := c.addTrace(ctx, "createCollection", name) + + err := c.Database.CreateCollection(tracerCtx, name) + + defer c.sendOperationStats(&QueryLog{Query: "createCollection", Collection: name}, time.Now(), "createCollection", + span) - return c.Database.CreateCollection(ctx, name) + return err } -func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time) { +func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) { duration := time.Since(startTime).Milliseconds() ql.Duration = duration @@ -229,6 +273,11 @@ func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time) { c.metrics.RecordHistogram(context.Background(), "app_mongo_stats", float64(duration), "hostname", c.uri, "database", c.database, "type", ql.Query) + + if span != nil { + defer span.End() + span.SetAttributes(attribute.Int64(fmt.Sprintf("mongo.%v.duration", method), duration)) + } } type Health struct { @@ -258,7 +307,7 @@ func (c *Client) HealthCheck(ctx context.Context) (any, error) { } func (c *Client) StartSession() (interface{}, error) { - defer c.sendOperationStats(&QueryLog{Query: "startSession"}, time.Now()) + defer c.sendOperationStats(&QueryLog{Query: "startSession"}, time.Now(), "", nil) s, err := c.Client().StartSession() ses := &session{s} @@ -280,3 +329,17 @@ type Transaction interface { CommitTransaction(context.Context) error EndSession(context.Context) } + +func (c *Client) addTrace(ctx context.Context, method, collection string) (context.Context, trace.Span) { + if c.tracer != nil { + contextWithTrace, span := c.tracer.Start(ctx, fmt.Sprintf("mongodb-%v", method)) + + span.SetAttributes( + attribute.String("mongo.collection", collection), + ) + + return contextWithTrace, span + } + + return ctx, nil +} diff --git a/pkg/gofr/datasource/mongo/mongo_test.go b/pkg/gofr/datasource/mongo/mongo_test.go index 7be50ff04..2e5e41b69 100644 --- a/pkg/gofr/datasource/mongo/mongo_test.go +++ b/pkg/gofr/datasource/mongo/mongo_test.go @@ -11,6 +11,7 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/integration/mtest" + "go.opentelemetry.io/otel" "go.uber.org/mock/gomock" ) @@ -61,7 +62,7 @@ func Test_InsertCommands(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) @@ -79,7 +80,7 @@ func Test_InsertCommands(t *testing.T) { resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc) assert.NotNil(t, resp) - assert.Nil(t, err) + assert.NoError(t, err) }) mt.Run("insertOneError", func(mt *mtest.T) { @@ -137,7 +138,7 @@ func Test_CreateCollection(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) @@ -166,7 +167,7 @@ func Test_FindMultipleCommands(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) @@ -195,7 +196,7 @@ func Test_FindMultipleCommands(t *testing.T) { err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) - assert.Nil(t, err, "Unexpected error during Find operation") + assert.NoError(t, err, "Unexpected error during Find operation") }) mt.Run("FindCursorError", func(mt *mtest.T) { @@ -240,7 +241,7 @@ func Test_FindOneCommands(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(2) @@ -275,7 +276,7 @@ func Test_FindOneCommands(t *testing.T) { err := cl.FindOne(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments) assert.Equal(t, expectedUser.Name, foundDocuments.Name) - assert.Nil(t, err) + assert.NoError(t, err) }) mt.Run("FindOneError", func(mt *mtest.T) { @@ -307,7 +308,7 @@ func Test_UpdateCommands(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(3) @@ -358,7 +359,7 @@ func Test_CountDocuments(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) @@ -379,7 +380,8 @@ func Test_CountDocuments(t *testing.T) { _, err := indexView.CreateOne(context.Background(), mongo.IndexModel{ Keys: bson.D{{Key: "x", Value: 1}}, }) - require.Nil(mt, err, "CreateOne error for index: %v", err) + + assert.NoError(mt, err, "CreateOne error for index: %v", err) resp, err := cl.CountDocuments(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}}) @@ -398,7 +400,7 @@ func Test_DeleteCommands(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(4) @@ -466,7 +468,7 @@ func Test_Drop(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} metrics.EXPECT().RecordHistogram(context.Background(), "app_mongo_stats", gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()) @@ -495,7 +497,7 @@ func TestClient_StartSession(t *testing.T) { metrics := NewMockMetrics(ctrl) logger := NewMockLogger(ctrl) - cl := Client{metrics: metrics} + cl := Client{metrics: metrics, tracer: otel.GetTracerProvider().Tracer("gofr-mongo")} // Set up the mock expectation for the metrics recording metrics.EXPECT().RecordHistogram(gomock.Any(), "app_mongo_stats", gomock.Any(), "hostname", @@ -513,6 +515,7 @@ func TestClient_StartSession(t *testing.T) { // Call the StartSession method sess, err := cl.StartSession() + ses, ok := sess.(Transaction) if ok { err = ses.StartTransaction() diff --git a/pkg/gofr/external_db.go b/pkg/gofr/external_db.go index 5ef8fb4e0..0dbfc92d8 100644 --- a/pkg/gofr/external_db.go +++ b/pkg/gofr/external_db.go @@ -2,6 +2,7 @@ package gofr import ( "go.opentelemetry.io/otel" + "gofr.dev/pkg/gofr/container" "gofr.dev/pkg/gofr/datasource/file" ) @@ -11,6 +12,10 @@ func (a *App) AddMongo(db container.MongoProvider) { db.UseLogger(a.Logger()) db.UseMetrics(a.Metrics()) + tracer := otel.GetTracerProvider().Tracer("gofr-mongo") + + db.UseTracer(tracer) + db.Connect() a.container.Mongo = db diff --git a/pkg/gofr/external_db_test.go b/pkg/gofr/external_db_test.go index b2f2adb06..7018605f9 100644 --- a/pkg/gofr/external_db_test.go +++ b/pkg/gofr/external_db_test.go @@ -41,6 +41,7 @@ func TestApp_AddMongo(t *testing.T) { mock.EXPECT().UseLogger(app.Logger()) mock.EXPECT().UseMetrics(app.Metrics()) + mock.EXPECT().UseTracer(gomock.Any()) mock.EXPECT().Connect() app.AddMongo(mock) From c679acbc46a296bc792e0a9465f993eeb697a553 Mon Sep 17 00:00:00 2001 From: JonasBakys0 Date: Fri, 4 Oct 2024 12:47:31 +0300 Subject: [PATCH 4/7] doc: Update container package overview (#1065) --- pkg/gofr/container/container.go | 12 ++++++++++++ pkg/gofr/container/mock_datasources.go | 1 - pkg/gofr/container/mock_metrics.go | 1 - 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/pkg/gofr/container/container.go b/pkg/gofr/container/container.go index abd611a3b..01f961a41 100644 --- a/pkg/gofr/container/container.go +++ b/pkg/gofr/container/container.go @@ -1,3 +1,15 @@ +/* +Package container provides a centralized structure to manage common application-level concerns such as +logging, connection pools, and service management. This package is designed to facilitate the sharing and +management of these concerns across different parts of an application. + +Supported data sources: + - Databases (Cassandra, ClickHouse, MongoDB, DGraph, MySQL, PostgreSQL, SQLite) + - Key-value storages (Redis, BadgerDB) + - Pub/Sub systems (Azure Event Hub, Google as backend, Kafka, MQTT) + - Search engines (Solr) + - File systems (FTP, SFTP, S3) +*/ package container import ( diff --git a/pkg/gofr/container/mock_datasources.go b/pkg/gofr/container/mock_datasources.go index 468717651..cce0014c2 100644 --- a/pkg/gofr/container/mock_datasources.go +++ b/pkg/gofr/container/mock_datasources.go @@ -6,7 +6,6 @@ // mockgen -source=datasources.go -destination=mock_datasources.go -package=container // -// Package container is a generated GoMock package. package container import ( diff --git a/pkg/gofr/container/mock_metrics.go b/pkg/gofr/container/mock_metrics.go index 1357cd69a..4a15b1a06 100644 --- a/pkg/gofr/container/mock_metrics.go +++ b/pkg/gofr/container/mock_metrics.go @@ -6,7 +6,6 @@ // mockgen -source=metrics.go -destination=mock_metrics.go -package=container // -// Package container is a generated GoMock package. package container import ( From 91905428cedeb465fcfdf6831c7ac9b3b5b5e5c6 Mon Sep 17 00:00:00 2001 From: Chaitanya Gairola Date: Sat, 5 Oct 2024 09:40:00 +0530 Subject: [PATCH 5/7] Updates GoDocs for CLI Options and addRoute Function (#1076) --- pkg/gofr/cmd.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/gofr/cmd.go b/pkg/gofr/cmd.go index d27e4e429..5c48ca265 100644 --- a/pkg/gofr/cmd.go +++ b/pkg/gofr/cmd.go @@ -21,8 +21,10 @@ type route struct { help string } +// Options is a function type used to configure a route in the command handler. type Options func(c *route) +// ErrCommandNotFound is an empty struct used to represent a specific error when a command is not found. type ErrCommandNotFound struct{} func (ErrCommandNotFound) Error() string { @@ -121,6 +123,7 @@ func AddHelp(helperString string) Options { } } +// addRoute adds a new route to cmd's list of routes. func (cmd *cmd) addRoute(pattern string, handler Handler, options ...Options) { tempRoute := route{ pattern: pattern, From daf5a713d0ee55e18a98f37a1b3a39dc8ad7a81b Mon Sep 17 00:00:00 2001 From: Umang Mundhra Date: Mon, 7 Oct 2024 10:12:43 +0530 Subject: [PATCH 6/7] En/add support for encoded forms (#1073) --- docs/references/context/page.md | 6 ++--- pkg/gofr/http/multipart_file_bind.go | 14 +++++++++++ pkg/gofr/http/request.go | 36 +++++++++++++++++++++++----- pkg/gofr/http/request_test.go | 21 ++++++++++++++++ 4 files changed, 68 insertions(+), 9 deletions(-) diff --git a/docs/references/context/page.md b/docs/references/context/page.md index fb0da0925..ec34e03c3 100644 --- a/docs/references/context/page.md +++ b/docs/references/context/page.md @@ -51,9 +51,9 @@ parts of the request. // the Bind() method will map the incoming request to variable p ``` -- `Binding multipart-form data` - - To bind multipart-form data, you can use the Bind method similarly. The struct fields should be tagged appropriately - to map the form fields to the struct fields. +- `Binding multipart-form data / urlencoded form data ` + - To bind multipart-form data or url-encoded form, you can use the Bind method similarly. The struct fields should be tagged appropriately + to map the form fields to the struct fields. The supported content types are `multipart/form-data` and `application/x-www-form-urlencoded` ```go type Data struct { diff --git a/pkg/gofr/http/multipart_file_bind.go b/pkg/gofr/http/multipart_file_bind.go index a4fe481f6..571ceac1b 100644 --- a/pkg/gofr/http/multipart_file_bind.go +++ b/pkg/gofr/http/multipart_file_bind.go @@ -29,6 +29,16 @@ type formData struct { func (uf *formData) mapStruct(val reflect.Value, field *reflect.StructField) (bool, error) { vKind := val.Kind() + if field == nil { + // Check if val is not a struct + if vKind != reflect.Struct { + return false, nil // Return false if val is not a struct + } + + // If field is nil, iterate through all fields of the struct + return uf.iterateStructFields(val) + } + if vKind == reflect.Pointer { return uf.checkPointer(val, field) } @@ -79,6 +89,10 @@ func (uf *formData) checkPointer(val reflect.Value, field *reflect.StructField) } func (uf *formData) checkStruct(val reflect.Value) (bool, error) { + return uf.iterateStructFields(val) +} + +func (uf *formData) iterateStructFields(val reflect.Value) (bool, error) { var set bool tVal := val.Type() diff --git a/pkg/gofr/http/request.go b/pkg/gofr/http/request.go index e5aa8971c..2c493a7f6 100644 --- a/pkg/gofr/http/request.go +++ b/pkg/gofr/http/request.go @@ -68,6 +68,8 @@ func (r *Request) Bind(i interface{}) error { return json.Unmarshal(body, &i) case "multipart/form-data": return r.bindMultipart(i) + case "application/x-www-form-urlencoded": + return r.bindFormURLEncoded(i) } return nil @@ -109,6 +111,14 @@ func (r *Request) body() ([]byte, error) { } func (r *Request) bindMultipart(ptr any) error { + return r.bindForm(ptr, true) +} + +func (r *Request) bindFormURLEncoded(ptr any) error { + return r.bindForm(ptr, false) +} + +func (r *Request) bindForm(ptr any, isMultipart bool) error { ptrVal := reflect.ValueOf(ptr) if ptrVal.Kind() != reflect.Ptr { return errNonPointerBind @@ -116,19 +126,33 @@ func (r *Request) bindMultipart(ptr any) error { ptrVal = ptrVal.Elem() - if err := r.req.ParseMultipartForm(defaultMaxMemory); err != nil { - return err - } + var fd formData - fd := formData{files: r.req.MultipartForm.File, fields: r.req.MultipartForm.Value} + if isMultipart { + if err := r.req.ParseMultipartForm(defaultMaxMemory); err != nil { + return err + } - ok, err := fd.mapStruct(ptrVal, &reflect.StructField{}) + fd = formData{files: r.req.MultipartForm.File, fields: r.req.MultipartForm.Value} + } else { + if err := r.req.ParseForm(); err != nil { + return err + } + + fd = formData{fields: r.req.Form} + } + + ok, err := fd.mapStruct(ptrVal, nil) if err != nil { return err } if !ok { - return errNoFileFound + if isMultipart { + return errNoFileFound + } + + return errFieldsNotSet } return nil diff --git a/pkg/gofr/http/request_test.go b/pkg/gofr/http/request_test.go index 054fe7867..d69832c87 100644 --- a/pkg/gofr/http/request_test.go +++ b/pkg/gofr/http/request_test.go @@ -251,3 +251,24 @@ func Test_Params(t *testing.T) { assert.ElementsMatch(t, expectedTags, r.Params("tag"), "expected all values of 'tag' to match") assert.Empty(t, r.Params("nonexistent"), "expected empty slice for non-existent query param") } + +func TestBind_FormURLEncoded(t *testing.T) { + // Create a new HTTP request with form-encoded data + req := NewRequest(httptest.NewRequest(http.MethodPost, "/abc", strings.NewReader("Name=John&Age=30"))) + req.req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + x := struct { + Name string `form:"Name"` + Age int `form:"Age"` + }{} + + err := req.Bind(&x) + if err != nil { + t.Errorf("Bind error: %v", err) + } + + // Check the results + if x.Name != "John" || x.Age != 30 { + t.Errorf("Bind error. Got: %v", x) + } +} From 19d12414fabafad0fe1fd7286622d3250fd805a0 Mon Sep 17 00:00:00 2001 From: umang01-hash Date: Mon, 7 Oct 2024 11:28:58 +0530 Subject: [PATCH 7/7] update release version --- pkg/gofr/version/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/gofr/version/version.go b/pkg/gofr/version/version.go index e482dcfa3..fed3e0f0e 100644 --- a/pkg/gofr/version/version.go +++ b/pkg/gofr/version/version.go @@ -1,3 +1,3 @@ package version -const Framework = "v1.22.1" +const Framework = "v1.23.0"