Repository: gofr-dev/gofr
Branch: development
Commit: 9f54f6fd51f3
Files: 858
Total size: 7.6 MB
Directory structure:
gitextract_4eff3cym/
├── .codeclimate.yml
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ ├── feature_request.md
│ │ └── question.md
│ ├── dependabot.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── go.yml
│ ├── typos.yml
│ ├── website-prod.yml
│ └── website-stage.yml
├── .gitignore
├── .golangci.yml
├── .ls-lint.yml
├── .qlty/
│ ├── .gitignore
│ ├── configs/
│ │ ├── .hadolint.yaml
│ │ └── .yamllint.yaml
│ └── qlty.toml
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── SECURITY.md
├── docs/
│ ├── Dockerfile
│ ├── advanced-guide/
│ │ ├── authentication/
│ │ │ └── page.md
│ │ ├── building-cli-applications/
│ │ │ └── page.md
│ │ ├── circuit-breaker/
│ │ │ └── page.md
│ │ ├── custom-spans-in-tracing/
│ │ │ └── page.md
│ │ ├── dealing-with-sql/
│ │ │ └── page.md
│ │ ├── debugging/
│ │ │ └── page.md
│ │ ├── gofr-errors/
│ │ │ └── page.md
│ │ ├── graphql/
│ │ │ └── page.md
│ │ ├── grpc/
│ │ │ └── page.md
│ │ ├── grpc-streaming/
│ │ │ └── page.md
│ │ ├── handling-data-migrations/
│ │ │ └── page.md
│ │ ├── handling-file/
│ │ │ └── page.md
│ │ ├── http-communication/
│ │ │ └── page.md
│ │ ├── injecting-databases-drivers/
│ │ │ └── page.md
│ │ ├── key-value-store/
│ │ │ └── page.md
│ │ ├── middlewares/
│ │ │ └── page.md
│ │ ├── monitoring-service-health/
│ │ │ └── page.md
│ │ ├── overriding-default/
│ │ │ └── page.md
│ │ ├── publishing-custom-metrics/
│ │ │ └── page.md
│ │ ├── rbac/
│ │ │ └── page.md
│ │ ├── remote-log-level-change/
│ │ │ └── page.md
│ │ ├── serving-static-files/
│ │ │ └── page.md
│ │ ├── setting-custom-response-headers/
│ │ │ └── page.md
│ │ ├── startup-hooks/
│ │ │ └── page.md
│ │ ├── swagger-documentation/
│ │ │ └── page.md
│ │ ├── using-cron/
│ │ │ └── page.md
│ │ ├── using-publisher-subscriber/
│ │ │ └── page.md
│ │ └── websocket/
│ │ └── page.md
│ ├── datasources/
│ │ ├── arangodb/
│ │ │ └── page.md
│ │ ├── cassandra/
│ │ │ └── page.md
│ │ ├── clickhouse/
│ │ │ └── page.md
│ │ ├── cockroachdb/
│ │ │ └── page.md
│ │ ├── couchbase/
│ │ │ └── page.md
│ │ ├── dgraph/
│ │ │ └── page.md
│ │ ├── elasticsearch/
│ │ │ └── page.md
│ │ ├── getting-started/
│ │ │ └── page.md
│ │ ├── influxdb/
│ │ │ └── page.md
│ │ ├── migrations/
│ │ │ └── elasticsearch/
│ │ │ └── page.md
│ │ ├── mongodb/
│ │ │ └── page.md
│ │ ├── opentsdb/
│ │ │ └── page.md
│ │ ├── oracle/
│ │ │ └── page.md
│ │ ├── scylladb/
│ │ │ └── page.md
│ │ ├── solr/
│ │ │ └── page.md
│ │ └── surrealdb/
│ │ └── page.md
│ ├── events.json
│ ├── navigation.js
│ ├── page.md
│ ├── quick-start/
│ │ ├── add-rest-handlers/
│ │ │ └── page.md
│ │ ├── cli/
│ │ │ └── page.md
│ │ ├── configuration/
│ │ │ └── page.md
│ │ ├── connecting-mysql/
│ │ │ └── page.md
│ │ ├── connecting-redis/
│ │ │ └── page.md
│ │ ├── introduction/
│ │ │ └── page.md
│ │ └── observability/
│ │ └── page.md
│ ├── references/
│ │ ├── configs/
│ │ │ └── page.md
│ │ ├── context/
│ │ │ └── page.md
│ │ ├── gofrcli/
│ │ │ └── page.md
│ │ └── testing/
│ │ └── page.md
│ └── testimonials.json
├── examples/
│ ├── grpc/
│ │ ├── grpc-streaming-client/
│ │ │ ├── README.md
│ │ │ ├── client/
│ │ │ │ ├── chat.pb.go
│ │ │ │ ├── chat.proto
│ │ │ │ ├── chat_grpc.pb.go
│ │ │ │ ├── chatservice_client.go
│ │ │ │ └── health_client.go
│ │ │ └── main.go
│ │ ├── grpc-streaming-server/
│ │ │ ├── README.md
│ │ │ ├── main.go
│ │ │ ├── main_test.go
│ │ │ └── server/
│ │ │ ├── chat.pb.go
│ │ │ ├── chat.proto
│ │ │ ├── chat_grpc.pb.go
│ │ │ ├── chatservice_gofr.go
│ │ │ ├── chatservice_server.go
│ │ │ ├── health_gofr.go
│ │ │ └── request_gofr.go
│ │ ├── grpc-unary-client/
│ │ │ ├── README.md
│ │ │ ├── client/
│ │ │ │ ├── health_client.go
│ │ │ │ ├── health_test.go
│ │ │ │ ├── hello.pb.go
│ │ │ │ ├── hello.proto
│ │ │ │ ├── hello_client.go
│ │ │ │ ├── hello_grpc.pb.go
│ │ │ │ └── hello_test.go
│ │ │ ├── main.go
│ │ │ └── main_test.go
│ │ └── grpc-unary-server/
│ │ ├── README.md
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── server/
│ │ ├── health_gofr.go
│ │ ├── health_test.go
│ │ ├── hello.pb.go
│ │ ├── hello.proto
│ │ ├── hello_gofr.go
│ │ ├── hello_grpc.pb.go
│ │ ├── hello_server.go
│ │ ├── hello_server_test.go
│ │ ├── hello_test.go
│ │ ├── request_gofr.go
│ │ └── request_test.go
│ ├── http-server/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── docker/
│ │ │ ├── docker-compose.yaml
│ │ │ ├── prometheus/
│ │ │ │ └── prometheus.yml
│ │ │ └── provisioning/
│ │ │ ├── dashboards/
│ │ │ │ ├── dashboards.yaml
│ │ │ │ └── gofr-dashboard/
│ │ │ │ └── dashboards.json
│ │ │ └── datasources/
│ │ │ └── datasource.yaml
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── static/
│ │ └── openapi.json
│ ├── http-server-using-redis/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── main.go
│ │ └── main_test.go
│ ├── sample-cmd/
│ │ ├── README.md
│ │ ├── configs/
│ │ │ └── .test.env
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-add-filestore/
│ │ ├── README.md
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-add-rest-handlers/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── migrations/
│ │ ├── 1721816030_create_user_table.go
│ │ ├── 1721816030_create_user_table_test.go
│ │ ├── all.go
│ │ └── all_test.go
│ ├── using-cron-jobs/
│ │ ├── Readme.md
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-custom-metrics/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-file-bind/
│ │ ├── Dockerfile
│ │ ├── README.md
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-graphql/
│ │ ├── configs/
│ │ │ └── schema.graphqls
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── migrations/
│ │ ├── 20240205125300_create_users_table.go
│ │ └── all.go
│ ├── using-html-template/
│ │ ├── README.md
│ │ ├── main.go
│ │ ├── main_test.go
│ │ ├── static/
│ │ │ ├── 404.html
│ │ │ ├── index.html
│ │ │ └── style.css
│ │ └── templates/
│ │ └── todo.html
│ ├── using-http-auth-middleware/
│ │ ├── ReadMe.md
│ │ ├── main.go
│ │ └── main_test.go
│ ├── using-http-service/
│ │ ├── Dockerfile
│ │ ├── main.go
│ │ ├── main_test.go
│ │ └── readme.md
│ ├── using-migrations/
│ │ ├── Dockerfile
│ │ ├── go.sum
│ │ ├── main.go
│ │ ├── main_test.go
│ │ ├── migrations/
│ │ │ ├── 1722507126_create_employee_table.go
│ │ │ ├── 1722507126_create_employee_table_test.go
│ │ │ ├── 1722507180_redis_add_employee_name.go
│ │ │ ├── 1722507180_redis_add_employee_name_test.go
│ │ │ ├── all.go
│ │ │ └── all_test.go
│ │ └── readme.md
│ ├── using-publisher/
│ │ ├── Dockerfile
│ │ ├── main.go
│ │ ├── main_test.go
│ │ ├── migrations/
│ │ │ ├── 1721801313_create_topics.go
│ │ │ ├── 1721801313_create_topics_test.go
│ │ │ ├── all.go
│ │ │ └── all_test.go
│ │ └── readme.md
│ ├── using-subscriber/
│ │ ├── Dockerfile
│ │ ├── main.go
│ │ ├── main_test.go
│ │ ├── migrations/
│ │ │ ├── 1721800255_create_topics.go
│ │ │ ├── 1721800255_create_topics_test.go
│ │ │ ├── all.go
│ │ │ └── all_test.go
│ │ └── readme.md
│ └── using-web-socket/
│ ├── Readme.md
│ ├── main.go
│ └── main_test.go
├── go.mod
├── go.sum
├── go.work
├── go.work.sum
├── pkg/
│ └── gofr/
│ ├── auth.go
│ ├── cmd/
│ │ ├── request.go
│ │ ├── request_test.go
│ │ ├── responder.go
│ │ ├── responder_test.go
│ │ └── terminal/
│ │ ├── colors.go
│ │ ├── output.go
│ │ ├── output_test.go
│ │ ├── printers.go
│ │ ├── printers_test.go
│ │ ├── progress.go
│ │ ├── progress_test.go
│ │ ├── spinner.go
│ │ └── spinner_test.go
│ ├── cmd.go
│ ├── cmd_test.go
│ ├── config/
│ │ ├── config.go
│ │ ├── godotenv.go
│ │ ├── godotenv_test.go
│ │ ├── mock_config.go
│ │ └── mock_config_test.go
│ ├── constants.go
│ ├── container/
│ │ ├── container.go
│ │ ├── container_test.go
│ │ ├── datasources.go
│ │ ├── health.go
│ │ ├── health_test.go
│ │ ├── metrics.go
│ │ ├── mock_container.go
│ │ ├── mock_datasources.go
│ │ ├── mock_logger.go
│ │ ├── mock_metrics.go
│ │ ├── mockcontainer_test.go
│ │ └── sql_mock.go
│ ├── context.go
│ ├── context_test.go
│ ├── cron.go
│ ├── cron_scheduler.go
│ ├── cron_test.go
│ ├── crud_handlers.go
│ ├── crud_handlers_test.go
│ ├── crud_helpers.go
│ ├── datasource/
│ │ ├── README.md
│ │ ├── arangodb/
│ │ │ ├── arango.go
│ │ │ ├── arango_db.go
│ │ │ ├── arango_db_test.go
│ │ │ ├── arango_document.go
│ │ │ ├── arango_document_test.go
│ │ │ ├── arango_graph.go
│ │ │ ├── arango_graph_test.go
│ │ │ ├── arango_helper.go
│ │ │ ├── arango_helper_test.go
│ │ │ ├── arango_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_collection.go
│ │ │ ├── mock_database.go
│ │ │ ├── mock_graph.go
│ │ │ ├── mock_interfaces.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ └── mock_user.go
│ │ ├── cassandra/
│ │ │ ├── cassandra.go
│ │ │ ├── cassandra_batch.go
│ │ │ ├── cassandra_batch_test.go
│ │ │ ├── cassandra_test.go
│ │ │ ├── errors.go
│ │ │ ├── errors_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interfaces.go
│ │ │ ├── internal.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interfaces.go
│ │ │ ├── mock_logger.go
│ │ │ └── mock_metrics.go
│ │ ├── clickhouse/
│ │ │ ├── clickhouse.go
│ │ │ ├── clickhouse_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interface.go
│ │ │ ├── mock_logger.go
│ │ │ └── mock_metrics.go
│ │ ├── couchbase/
│ │ │ ├── couchbase.go
│ │ │ ├── couchbase_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interfaces.go
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interfaces.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ └── wrappers.go
│ │ ├── datasource.go
│ │ ├── dbresolver/
│ │ │ ├── circuit_breaker.go
│ │ │ ├── factory.go
│ │ │ ├── factory_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_db.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── mock_strategy.go
│ │ │ ├── options.go
│ │ │ ├── resolver.go
│ │ │ ├── resolver_test.go
│ │ │ ├── strategy.go
│ │ │ └── strategy_test.go
│ │ ├── dgraph/
│ │ │ ├── dgraph.go
│ │ │ ├── dgraph_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interfaces.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interfaces.go
│ │ │ ├── mock_logger.go
│ │ │ └── mock_metrics.go
│ │ ├── elasticsearch/
│ │ │ ├── documents.go
│ │ │ ├── elasticsearch.go
│ │ │ ├── elasticsearch_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_logger.go
│ │ │ └── mock_metrics.go
│ │ ├── errors.go
│ │ ├── errors_test.go
│ │ ├── file/
│ │ │ ├── azure/
│ │ │ │ ├── fs.go
│ │ │ │ ├── fs_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── storage_adapter.go
│ │ │ │ └── storage_adapter_test.go
│ │ │ ├── common_file.go
│ │ │ ├── common_file_test.go
│ │ │ ├── common_fs.go
│ │ │ ├── common_fs_signedurl_test.go
│ │ │ ├── common_fs_test.go
│ │ │ ├── ftp/
│ │ │ │ ├── fs.go
│ │ │ │ ├── fs_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── storage_adapter.go
│ │ │ │ └── storage_adapter_test.go
│ │ │ ├── gcs/
│ │ │ │ ├── fs.go
│ │ │ │ ├── fs_test.go
│ │ │ │ ├── gcs_cloud_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── storage_adapter.go
│ │ │ │ └── storage_adapter_test.go
│ │ │ ├── helper.go
│ │ │ ├── interface.go
│ │ │ ├── local_fs.go
│ │ │ ├── local_fs_test.go
│ │ │ ├── logger.go
│ │ │ ├── mock_interface.go
│ │ │ ├── observability.go
│ │ │ ├── row_reader.go
│ │ │ ├── row_reader_test.go
│ │ │ ├── s3/
│ │ │ │ ├── file.go
│ │ │ │ ├── file_parse.go
│ │ │ │ ├── file_test.go
│ │ │ │ ├── fs.go
│ │ │ │ ├── fs_dir.go
│ │ │ │ ├── fs_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── interface.go
│ │ │ │ ├── logger.go
│ │ │ │ ├── logger_test.go
│ │ │ │ └── mock_interface.go
│ │ │ └── sftp/
│ │ │ ├── file.go
│ │ │ ├── fs.go
│ │ │ ├── fs_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ └── mock_interface.go
│ │ ├── health.go
│ │ ├── influxdb/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── influxdb.go
│ │ │ ├── influxdb_test.go
│ │ │ ├── interface.go
│ │ │ ├── internal.go
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── metrics_logger.go
│ │ │ ├── mock_interface.go
│ │ │ └── mock_logger.go
│ │ ├── interface.go
│ │ ├── kv-store/
│ │ │ ├── badger/
│ │ │ │ ├── badger.go
│ │ │ │ ├── badger_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── logger.go
│ │ │ │ ├── logger_test.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_logger.go
│ │ │ │ └── mock_metrics.go
│ │ │ ├── dynamodb/
│ │ │ │ ├── dynamo.go
│ │ │ │ ├── dynamo_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── logger.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_dynamodb.go
│ │ │ │ ├── mock_logger.go
│ │ │ │ └── mock_metrics.go
│ │ │ └── nats/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interface.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── nats.go
│ │ │ └── nats_test.go
│ │ ├── logger.go
│ │ ├── mongo/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── mongo.go
│ │ │ └── mongo_test.go
│ │ ├── opentsdb/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── mock_interface.go
│ │ │ ├── observability.go
│ │ │ ├── opentsdb.go
│ │ │ ├── opentsdb_test.go
│ │ │ ├── preprocess.go
│ │ │ └── response.go
│ │ ├── oracle/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_interface.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── oracle.go
│ │ │ └── oracle_test.go
│ │ ├── pubsub/
│ │ │ ├── eventhub/
│ │ │ │ ├── eventhub.go
│ │ │ │ ├── eventhub_test.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── helper.go
│ │ │ │ ├── helper_test.go
│ │ │ │ ├── logger.go
│ │ │ │ ├── logger_test.go
│ │ │ │ ├── message.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_logger.go
│ │ │ │ └── mock_metrics.go
│ │ │ ├── google/
│ │ │ │ ├── google.go
│ │ │ │ ├── google_test.go
│ │ │ │ ├── health.go
│ │ │ │ ├── helper.go
│ │ │ │ ├── interfaces.go
│ │ │ │ ├── message.go
│ │ │ │ ├── message_test.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_interfaces.go
│ │ │ │ ├── mock_metrics.go
│ │ │ │ ├── tracing.go
│ │ │ │ └── tracing_test.go
│ │ │ ├── interface.go
│ │ │ ├── kafka/
│ │ │ │ ├── conn.go
│ │ │ │ ├── errors.go
│ │ │ │ ├── health.go
│ │ │ │ ├── health_test.go
│ │ │ │ ├── helper.go
│ │ │ │ ├── interfaces.go
│ │ │ │ ├── kafka.go
│ │ │ │ ├── kafka_sasl.go
│ │ │ │ ├── kafka_sasl_test.go
│ │ │ │ ├── kafka_test.go
│ │ │ │ ├── message.go
│ │ │ │ ├── message_test.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_interfaces.go
│ │ │ │ ├── mock_metrics.go
│ │ │ │ ├── tls.go
│ │ │ │ ├── tls_test.go
│ │ │ │ ├── tracing.go
│ │ │ │ └── tracing_test.go
│ │ │ ├── log.go
│ │ │ ├── message.go
│ │ │ ├── message_test.go
│ │ │ ├── mqtt/
│ │ │ │ ├── default_client.go
│ │ │ │ ├── helper.go
│ │ │ │ ├── interface.go
│ │ │ │ ├── message.go
│ │ │ │ ├── message_test.go
│ │ │ │ ├── mock_client.go
│ │ │ │ ├── mock_interfaces.go
│ │ │ │ ├── mock_token.go
│ │ │ │ ├── mqtt.go
│ │ │ │ └── mqtt_test.go
│ │ │ ├── nats/
│ │ │ │ ├── client.go
│ │ │ │ ├── client_helper.go
│ │ │ │ ├── client_test.go
│ │ │ │ ├── committer.go
│ │ │ │ ├── committer_test.go
│ │ │ │ ├── config.go
│ │ │ │ ├── connection_manager.go
│ │ │ │ ├── connection_manager_test.go
│ │ │ │ ├── connectors.go
│ │ │ │ ├── connectors_test.go
│ │ │ │ ├── errors.go
│ │ │ │ ├── go.mod
│ │ │ │ ├── go.sum
│ │ │ │ ├── health.go
│ │ │ │ ├── health_test.go
│ │ │ │ ├── interfaces.go
│ │ │ │ ├── message.go
│ │ │ │ ├── message_test.go
│ │ │ │ ├── metrics.go
│ │ │ │ ├── mock_client.go
│ │ │ │ ├── mock_jetstream.go
│ │ │ │ ├── mock_metrics.go
│ │ │ │ ├── mock_tracer.go
│ │ │ │ ├── pubsub_wrapper.go
│ │ │ │ ├── stream_manager.go
│ │ │ │ ├── stream_manager_test.go
│ │ │ │ ├── subscription_manager.go
│ │ │ │ ├── subscription_manager_test.go
│ │ │ │ ├── tracing.go
│ │ │ │ ├── tracing_test.go
│ │ │ │ └── wrapper_test.go
│ │ │ └── sqs/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── health.go
│ │ │ ├── health_test.go
│ │ │ ├── interfaces.go
│ │ │ ├── message.go
│ │ │ ├── message_test.go
│ │ │ ├── mock_client.go
│ │ │ ├── sqs.go
│ │ │ ├── sqs_test.go
│ │ │ ├── tracing.go
│ │ │ └── tracing_test.go
│ │ ├── redis/
│ │ │ ├── config.go
│ │ │ ├── config_test.go
│ │ │ ├── health.go
│ │ │ ├── health_test.go
│ │ │ ├── hook.go
│ │ │ ├── hook_test.go
│ │ │ ├── messages.go
│ │ │ ├── messages_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── pubsub.go
│ │ │ ├── pubsub_test.go
│ │ │ ├── redis.go
│ │ │ └── redis_test.go
│ │ ├── scylladb/
│ │ │ ├── errors.go
│ │ │ ├── errors_test.go
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── interface.go
│ │ │ ├── internal.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── mock_interface.go
│ │ │ ├── mock_logger.go
│ │ │ ├── scylladb.go
│ │ │ └── scylladb_test.go
│ │ ├── solr/
│ │ │ ├── go.mod
│ │ │ ├── go.sum
│ │ │ ├── logger.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_logger.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── solr.go
│ │ │ └── solr_test.go
│ │ ├── sql/
│ │ │ ├── bind.go
│ │ │ ├── bind_test.go
│ │ │ ├── db.go
│ │ │ ├── db_test.go
│ │ │ ├── health.go
│ │ │ ├── health_test.go
│ │ │ ├── metrics.go
│ │ │ ├── mock_metrics.go
│ │ │ ├── query_builder.go
│ │ │ ├── query_builder_test.go
│ │ │ ├── sql.go
│ │ │ ├── sql_mock.go
│ │ │ ├── sql_test.go
│ │ │ ├── supabase.go
│ │ │ └── supabase_test.go
│ │ └── surrealdb/
│ │ ├── go.mod
│ │ ├── go.sum
│ │ ├── interface.go
│ │ ├── mock_interface.go
│ │ ├── surrealdb.go
│ │ ├── surrealdb_test.go
│ │ ├── utils.go
│ │ └── wrapper.go
│ ├── default.go
│ ├── exporter.go
│ ├── exporter_test.go
│ ├── external_db.go
│ ├── external_db_test.go
│ ├── factory.go
│ ├── file/
│ │ ├── file.go
│ │ ├── file_test.go
│ │ ├── zip.go
│ │ └── zip_test.go
│ ├── gofr.go
│ ├── gofr_test.go
│ ├── graphql.go
│ ├── graphql_test.go
│ ├── grpc/
│ │ ├── log.go
│ │ ├── log_test.go
│ │ ├── middleware/
│ │ │ ├── apikey_auth.go
│ │ │ ├── auth_test.go
│ │ │ ├── basic_auth.go
│ │ │ ├── common.go
│ │ │ ├── oauth.go
│ │ │ └── oauth_integration_test.go
│ │ ├── rate_limiter.go
│ │ └── rate_limiter_test.go
│ ├── grpc.go
│ ├── grpc_test.go
│ ├── handler.go
│ ├── handler_test.go
│ ├── http/
│ │ ├── errors.go
│ │ ├── errors_test.go
│ │ ├── form_data_binder.go
│ │ ├── form_data_binder_test.go
│ │ ├── metrics.go
│ │ ├── middleware/
│ │ │ ├── apikey_auth.go
│ │ │ ├── apikey_auth_test.go
│ │ │ ├── auth.go
│ │ │ ├── auth_test.go
│ │ │ ├── basic_auth.go
│ │ │ ├── basic_auth_test.go
│ │ │ ├── config.go
│ │ │ ├── config_test.go
│ │ │ ├── cors.go
│ │ │ ├── cors_test.go
│ │ │ ├── errors.go
│ │ │ ├── errors_test.go
│ │ │ ├── logger.go
│ │ │ ├── logger_test.go
│ │ │ ├── metrics.go
│ │ │ ├── metrics_test.go
│ │ │ ├── oauth.go
│ │ │ ├── oauth_test.go
│ │ │ ├── rate_limiter.go
│ │ │ ├── rate_limiter_store.go
│ │ │ ├── rate_limiter_test.go
│ │ │ ├── tracer.go
│ │ │ ├── tracer_test.go
│ │ │ ├── validate.go
│ │ │ ├── validate_test.go
│ │ │ ├── web_socket.go
│ │ │ └── web_socket_test.go
│ │ ├── multipart_file_bind.go
│ │ ├── multipart_file_bind_test.go
│ │ ├── request.go
│ │ ├── request_test.go
│ │ ├── responder.go
│ │ ├── responder_test.go
│ │ ├── response/
│ │ │ ├── file.go
│ │ │ ├── raw.go
│ │ │ ├── redirect.go
│ │ │ ├── response.go
│ │ │ ├── template.go
│ │ │ └── xml.go
│ │ ├── router.go
│ │ └── router_test.go
│ ├── http_server.go
│ ├── http_server_test.go
│ ├── logging/
│ │ ├── ctx_logger.go
│ │ ├── ctx_logger_test.go
│ │ ├── level.go
│ │ ├── level_test.go
│ │ ├── logger.go
│ │ ├── logger_test.go
│ │ ├── mock_logger.go
│ │ ├── mock_logger_test.go
│ │ └── remotelogger/
│ │ ├── dynamic_level_logger.go
│ │ ├── dynamic_level_logger_test.go
│ │ └── mock_buffer_logger.go
│ ├── metrics/
│ │ ├── errors.go
│ │ ├── exporters/
│ │ │ ├── exporter.go
│ │ │ ├── telemetry.go
│ │ │ └── telemetry_test.go
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ ├── register.go
│ │ ├── register_test.go
│ │ ├── store.go
│ │ └── store_test.go
│ ├── metrics_server.go
│ ├── migration/
│ │ ├── arango.go
│ │ ├── arango_test.go
│ │ ├── cassandra.go
│ │ ├── cassandra_test.go
│ │ ├── clickhouse.go
│ │ ├── clickhouse_test.go
│ │ ├── datasource.go
│ │ ├── datasource_test.go
│ │ ├── dgraph.go
│ │ ├── dgraph_test.go
│ │ ├── elasticsearch.go
│ │ ├── elasticsearch_test.go
│ │ ├── interface.go
│ │ ├── logger.go
│ │ ├── migration.go
│ │ ├── migration_test.go
│ │ ├── mock_interface.go
│ │ ├── mongo.go
│ │ ├── mongo_test.go
│ │ ├── opentsdb.go
│ │ ├── opentsdb_test.go
│ │ ├── oracle.go
│ │ ├── oracle_test.go
│ │ ├── pubsub.go
│ │ ├── pubsub_test.go
│ │ ├── redis.go
│ │ ├── redis_test.go
│ │ ├── scylla_db.go
│ │ ├── scylla_db_test.go
│ │ ├── sql.go
│ │ ├── sql_test.go
│ │ ├── surreal_db.go
│ │ └── surreal_db_test.go
│ ├── otel.go
│ ├── otel_test.go
│ ├── rbac/
│ │ ├── config.go
│ │ ├── config_test.go
│ │ ├── endpoint_matcher.go
│ │ ├── endpoint_matcher_test.go
│ │ ├── middleware.go
│ │ └── middleware_test.go
│ ├── rbac.go
│ ├── rbac_test.go
│ ├── request.go
│ ├── responder.go
│ ├── rest.go
│ ├── run.go
│ ├── service/
│ │ ├── apikey_auth.go
│ │ ├── apikey_auth_test.go
│ │ ├── auth.go
│ │ ├── auth_test.go
│ │ ├── basic_auth.go
│ │ ├── basic_auth_test.go
│ │ ├── circuit_breaker.go
│ │ ├── circuit_breaker_test.go
│ │ ├── connection_pool.go
│ │ ├── connection_pool_test.go
│ │ ├── custom_header.go
│ │ ├── custom_header_test.go
│ │ ├── errors.go
│ │ ├── errors_test.go
│ │ ├── health.go
│ │ ├── health_config.go
│ │ ├── health_config_test.go
│ │ ├── health_test.go
│ │ ├── logger.go
│ │ ├── logger_test.go
│ │ ├── metrics.go
│ │ ├── mock_http_service.go
│ │ ├── mock_metrics.go
│ │ ├── mock_oauth_server.go
│ │ ├── new.go
│ │ ├── new_test.go
│ │ ├── oauth.go
│ │ ├── oauth_test.go
│ │ ├── options.go
│ │ ├── rate_limiter.go
│ │ ├── rate_limiter_config.go
│ │ ├── rate_limiter_config_test.go
│ │ ├── rate_limiter_store.go
│ │ ├── rate_limiter_store_test.go
│ │ ├── rate_limiter_test.go
│ │ ├── response.go
│ │ ├── response_test.go
│ │ ├── retry.go
│ │ └── retry_test.go
│ ├── shutdown.go
│ ├── shutdown_test.go
│ ├── static/
│ │ ├── files.go
│ │ ├── index.css
│ │ ├── index.html
│ │ ├── oauth2-redirect.html
│ │ ├── swagger-ui-bundle.js
│ │ ├── swagger-ui-standalone-preset.js
│ │ ├── swagger-ui.css
│ │ └── swagger-ui.js
│ ├── subscriber.go
│ ├── subscriber_test.go
│ ├── swagger.go
│ ├── swagger_test.go
│ ├── telemetry.go
│ ├── testutil/
│ │ ├── error.go
│ │ ├── error_test.go
│ │ ├── os.go
│ │ ├── os_test.go
│ │ ├── port.go
│ │ └── port_test.go
│ ├── version/
│ │ └── version.go
│ ├── websocket/
│ │ ├── interfaces.go
│ │ ├── mock_interfaces.go
│ │ ├── options.go
│ │ ├── websocket.go
│ │ └── websocket_test.go
│ ├── websocket.go
│ └── websocket_test.go
└── typos.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .codeclimate.yml
================================================
version: "2"
checks:
argument-count:
enabled: true
config:
threshold: 6
complex-logic:
enabled: true
config:
threshold: 10
file-lines:
enabled: true
config:
threshold: 300
method-complexity:
enabled: true
config:
threshold: 12
method-count:
enabled: true
config:
threshold: 25
method-lines:
enabled: true
config:
threshold: 100
nested-control-flow:
enabled: true
config:
threshold: 4
return-statements:
enabled: true
config:
threshold: 8
similar-code:
enabled: false
identical-code:
enabled: false
# plugins:
# eslint:
# enabled: true
# channel: "eslint-6"
exclude_patterns:
- "**/mock_*"
- "**/*_test.go"
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: triage
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior, if applicable:
1. The code is
```go
```
2. The error is
```
```
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Environments (please complete the following information):**
- OS: [e.g. Linux]
- gofr version [e.g. v1.5.0]
- go version [e.g. 1.21]
**More description**
Add any other context about the problem here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: triage
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question on using gofr
title: ''
labels: ''
assignees: ''
---
================================================
FILE: .github/dependabot.yml
================================================
---
version: 2
updates:
- package-ecosystem: "gomod"
open-pull-requests-limit: 10 # avoid spam, if no one reacts
directories:
- "/"
- "/examples/*"
- "/pkg/gofr/datasource/*"
- "/pkg/gofr/datasource/file/*"
- "/pkg/gofr/datasource/kv-store/*"
- "/pkg/gofr/datasource/pubsub/*"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
open-pull-requests-limit: 10 # avoid spam, if no one reacts
directory: "/"
schedule:
# Check for updates to GitHub Actions every week
interval: "weekly"
groups:
actions:
update-types:
- "minor"
- "patch"
================================================
FILE: .github/pull_request_template.md
================================================
## Pull Request Template
**Description:**
- Provide a concise explanation of the changes made.
- Mention the issue number(s) this PR addresses (if applicable).
- Highlight the motivation behind the changes and the expected benefits.
**Breaking Changes (if applicable):**
- List any breaking changes introduced by this PR.
- Explain the rationale behind these changes and how they will impact users.
**Additional Information:**
- Mention any relevant dependencies or external libraries used.
- Include screenshots or code snippets (if necessary) to clarify the changes.
**Checklist:**
- [ ] I have formatted my code using `goimport` and `golangci-lint`.
- [ ] All new code is covered by unit tests.
- [ ] This PR does not decrease the overall code coverage.
- [ ] I have reviewed the code comments and documentation for clarity.
**Thank you for your contribution!**
================================================
FILE: .github/workflows/go.yml
================================================
---
name: Workflow-Pipeline
permissions:
contents: read
# Define when this workflow should run
on:
# Run on push events to main or development branches
push:
branches:
- main
- development
paths-ignore:
- 'docs/**' # Ignore changes to docs folder
- '**/*.md'
# Run on pull requests to main or development branches
pull_request:
branches:
- main
- development
paths-ignore:
- 'docs/**' # Ignore changes to docs folder
- '**/*.md'
# Define the jobs that this workflow will run
jobs:
# Job for testing the examples directory
Example-Unit-Testing:
name: Example Unit Testing (v${{ matrix.go-version }})🛠
runs-on: ubuntu-latest
# Define a matrix strategy to test against multiple Go versions
strategy:
matrix:
go-version: ['1.25','1.24', '1.23']
# Continue with other jobs if one version fails
fail-fast: false
# Define service containers that tests depend on
services:
# Kafka service
kafka:
image: bitnamilegacy/kafka:3.4.1
ports:
- "9092:9092"
env:
KAFKA_ENABLE_KRAFT: yes
KAFKA_CFG_PROCESS_ROLES: broker,controller
KAFKA_CFG_CONTROLLER_LISTENER_NAMES: CONTROLLER
KAFKA_CFG_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
KAFKA_CFG_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092
KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE: true
KAFKA_BROKER_ID: 1
KAFKA_CFG_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9093
ALLOW_PLAINTEXT_LISTENER: yes
KAFKA_CFG_NODE_ID: 1
# Redis service
redis:
image: redis:7.0.5
ports:
- "2002:6379"
options: "--entrypoint redis-server"
# MySQL service
mysql:
image: mysql:8.2.0
ports:
- "2001:3306"
env:
MYSQL_ROOT_PASSWORD: "password"
MYSQL_DATABASE: "test"
# Steps to execute for this job
steps:
- name: Checkout code into go module directory
uses: actions/checkout@v6
with:
fetch-depth: 0 # Full git history for accurate testing
# Set up the Go environment with the specified version
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
id: Go
- name: Get dependencies
run: |
go mod download
- name: Start Zipkin
run: docker run -d -p 2005:9411 openzipkin/zipkin:latest
# Run tests with automatic retry on failures
- name: Test with Retry Logic
id: test
uses: nick-fields/retry@v3
with:
timeout_minutes: 5 # Maximum time for the tests to run
max_attempts: 2 # Retry up to 2 times if tests fail
command: |
export APP_ENV=test
# Run tests for the examples directory with coverage
go test ./examples/... -v -short -covermode=atomic -coverprofile packageWithpbgo.cov -coverpkg=./examples/...
# Filter out auto-generated files by protobuf and gofr framework from coverage report
grep -vE '(/client/|grpc-.+-client/main\.go|_client\.go|_gofr\.go|_grpc\.pb\.go|\.pb\.go|\.proto|health_.*\.go)' packageWithpbgo.cov > profile.cov
# Display coverage statistics
go tool cover -func profile.cov
# Upload coverage report for the 1.24 Go version only
- name: Upload Test Coverage
if: ${{ matrix.go-version == '1.24'}}
uses: actions/upload-artifact@v7
with:
name: Example-Test-Report
path: profile.cov
# Job for testing the pkg directory
PKG-Unit-Testing:
name: PKG Unit Testing (v${{ matrix.go-version }})🛠
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.25','1.24', '1.23']
fail-fast: false
steps:
- name: Checkout code into go module directory
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
id: Go
- name: Get dependencies
run: |
go mod download
# Run pkg tests with automatic retry logic
- name: Test with Retry Logic
id: test
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 2
retry_on: error
command: |
export APP_ENV=test
# Run tests for root gofr package
go test -v -short -covermode=atomic \
-coverpkg=./pkg/gofr -coverprofile=gofr_only.cov ./pkg/gofr
exit_code=$?
if [ $exit_code -eq 2 ]; then
echo "::error::Panic detected in root gofr package tests"
exit 2
elif [ $exit_code -ne 0 ]; then
echo "::error::Root gofr package tests failed"
exit $exit_code
fi
# Run tests for sub-packages
go test -v -covermode=atomic \
-coverpkg=./pkg/gofr -coverprofile=submodules.cov ./pkg/gofr/...
exit_code=$?
if [ $exit_code -eq 2 ]; then
echo "::error::Panic detected in gofr sub-packages tests"
exit 2
elif [ $exit_code -ne 0 ]; then
echo "::error::Gofr sub-packages tests failed"
exit $exit_code
fi
# Combine coverage profiles
echo "mode: atomic" > profile.cov
grep -h -v "mode:" gofr_only.cov submodules.cov | grep -v '/mock_' >> profile.cov
# Show coverage summary
go tool cover -func profile.cov
# Upload coverage report for the 1.24 Go version only
- name: Upload Test Coverage
if: ${{ matrix.go-version == '1.24'}}
uses: actions/upload-artifact@v7
with:
name: PKG-Coverage-Report
path: profile.cov
# Job for analyzing and reporting code coverage
parse_coverage:
name: Code Coverage
runs-on: ubuntu-latest
# This job runs after both Example and PKG testing are complete
needs: [ Example-Unit-Testing,PKG-Unit-Testing ]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
# Download coverage reports from previous jobs
- name: Download Coverage Report
uses: actions/download-artifact@v8
with:
path: artifacts
# Merge the coverage reports from Example and PKG tests
- name: Merge Coverage Files
working-directory: artifacts
run: |
echo "mode: atomic" > merged_profile.cov
grep -h -v "mode:" ./Example-Test-Report/profile.cov ./PKG-Coverage-Report/profile.cov >> merged_profile.cov
# Calculate and output the total code coverage percentage
- name: Parse code-coverage value
working-directory: artifacts
run: |
codeCoverage=$(go tool cover -func=merged_profile.cov | grep total | awk '{print $3}')
codeCoverage=${codeCoverage%?}
echo "CODE_COVERAGE=$codeCoverage" >> $GITHUB_ENV
echo "✅ Total Code Coverage: $codeCoverage%"
# - name: Check if code-coverage is greater than threshold
# run: |
# codeCoverage=${{ env.CODE_COVERAGE }}
# codeCoverage=${codeCoverage%??}
# if [[ $codeCoverage -lt 92 ]]; then echo "code coverage cannot be less than 92%, currently its ${{ env.CODE_COVERAGE }}%" && exit 1; fi;
# Job for testing submodules inside the pkg directory
Submodule-Unit-Testing:
name: Submodule Unit Testing (v${{ matrix.go-version }})🛠
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.25','1.24', '1.23']
fail-fast: false
steps:
- name: Checkout code into go module directory
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
id: Go
# Find all submodules (directories with go.mod files) in the pkg directory
- name: Detect Submodules
id: detect_submodules
run: |
# Find all directories containing a go.mod file within 'pkg'
SUBMODULES=$(find pkg -name "go.mod" -exec dirname {} \; | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "submodules=$SUBMODULES" >> $GITHUB_OUTPUT
# Test all submodules in parallel with retry logic
- name: Test Submodules with Retry and Parallelism
id: test_submodules
uses: nick-fields/retry@v3
with:
timeout_minutes: 5
max_attempts: 2
command: |
export APP_ENV=test
# Create a directory for coverage reports
mkdir -p coverage_reports
# Get the list of submodules
SUBMODULES='${{ steps.detect_submodules.outputs.submodules }}'
# Process each submodule in parallel with a maximum of 4 parallel jobs
echo $SUBMODULES | jq -c '.[]' | xargs -I{} -P 4 bash -c '
module={}
echo "Testing module: $module"
cd $module
# Extract module name (replace / with _)
module_name=$(echo $module | tr "/" "_")
# Download dependencies for the submodule
go mod download
go mod tidy
# Run tests with a focus on failed tests first
go test ./... -v -short -coverprofile=${module_name}.cov -coverpkg=./...
# Copy coverage file to the coverage_reports directory
cp ${module_name}.cov ../../../coverage_reports/
cd -
'
# Upload submodule coverage reports as an artifact
- name: Upload Coverage Reports
uses: actions/upload-artifact@v7
with:
name: submodule-coverage-reports
path: coverage_reports/*.cov
# Job for uploading coverage to external services (qlty.sh)
upload_coverage:
name: Upload Coverage📊
runs-on: ubuntu-latest
env:
QLTY_TOKEN: ${{ secrets.QLTY_TOKEN }}
QLTY_COVERAGE_TOKEN: ${{ secrets.QLTY_TOKEN }}
# This job only needs example and pkg test results, not submodules
needs: [Example-Unit-Testing, PKG-Unit-Testing]
# Only run this job on pushes to the development branch
if: ${{ github.repository == 'gofr-dev/gofr' && github.event_name == 'push' && github.ref == 'refs/heads/development'}}
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
- name: Install qlty CLI
run: |
curl https://qlty.sh | sh
echo "$HOME/.qlty/bin" >> $GITHUB_PATH
# Download coverage artifacts
- name: Download Coverage Report
uses: actions/download-artifact@v8
with:
path: artifacts
# Merge coverage from example and pkg tests only
- name: Merge Coverage Files
working-directory: artifacts
run: |
echo "mode: atomic" > merged_profile.cov
grep -h -v "mode:" ./Example-Test-Report/profile.cov ./PKG-Coverage-Report/profile.cov >> merged_profile.cov
# Generate and print total coverage percentage
echo "Total Coverage:"
go tool cover -func=merged_profile.cov | tail -n 1
shell: bash
# Upload merged coverage to CodeClimate for analysis
- name: Upload
working-directory: artifacts
run: qlty coverage publish merged_profile.cov --format=coverprofile --strip-prefix="gofr.dev/" --add-prefix="${GITHUB_WORKSPACE}/"
env:
QLTY_TOKEN: ${{ secrets.QLTY_TOKEN }}
# Job for code quality checks
code_quality:
name: Code Quality🎖️
runs-on: ubuntu-latest
outputs:
modules: ${{ steps.changed-submodules.outputs.modules }}
has_modules: ${{ steps.changed-submodules.outputs.has_modules }}
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v6
with:
fetch-depth: 0 # Full history needed for proper diff analysis
- name: Set up Go environment
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: false
- name: Get dependencies
run: go mod download
# Use the official golangci-lint action for the root module
# This action automatically detects changed files and only reports new issues
- name: Lint Root Module
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0
only-new-issues: true
args: --timeout=5m
# Detect changed files to determine which submodules need linting
# This implements a changed-files based approach as suggested by the maintainer
- name: Get Changed Files
id: changed-files
uses: tj-actions/changed-files@v47
with:
files: |
pkg/**/*.go
pkg/**/go.mod
pkg/**/go.sum
# Find all submodules that have changes
- name: Find Changed Submodules
id: changed-submodules
run: |
# Check if any files changed
if [ "${{ steps.changed-files.outputs.any_changed }}" != "true" ]; then
echo "✅ No changes in pkg/ directory"
echo "modules=[]" >> $GITHUB_OUTPUT
echo "has_modules=false" >> $GITHUB_OUTPUT
exit 0
fi
changed_files="${{ steps.changed-files.outputs.all_changed_files }}"
changed_modules=""
echo "📝 Changed files detected:"
echo "$changed_files" | tr ' ' '\n'
echo ""
# Extract unique submodule directories from changed files
for file in $changed_files; do
# Find the nearest parent directory containing go.mod
dir=$(dirname "$file")
while [ "$dir" != "." ] && [ "$dir" != "/" ]; do
if [ -f "$dir/go.mod" ] && [[ "$dir" == pkg/* ]]; then
# Check if this module is not already in the list
if [[ ! "$changed_modules" =~ (^|[[:space:]])"$dir"($|[[:space:]]) ]]; then
changed_modules="$changed_modules$dir "
fi
break
fi
dir=$(dirname "$dir")
done
done
changed_modules=$(echo "$changed_modules" | xargs -n1 | sort -u)
if [ -n "$changed_modules" ]; then
echo "📦 Submodules with changes:"
echo "$changed_modules"
# Convert to JSON array for matrix usage
modules_json=$(echo "$changed_modules" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "modules=$modules_json" >> $GITHUB_OUTPUT
echo "has_modules=true" >> $GITHUB_OUTPUT
else
echo "✅ No submodule changes detected"
echo "modules=[]" >> $GITHUB_OUTPUT
echo "has_modules=false" >> $GITHUB_OUTPUT
fi
# Separate job to lint changed submodules using matrix strategy
# This allows us to use the official golangci-lint action for each submodule
lint_changed_submodules:
name: Lint Submodules🔍
runs-on: ubuntu-latest
needs: code_quality
if: needs.code_quality.outputs.has_modules == 'true'
strategy:
matrix:
module: ${{ fromJson(needs.code_quality.outputs.modules) }}
fail-fast: false
steps:
- name: Check out code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Go environment
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: false
- name: Download dependencies for ${{ matrix.module }}
working-directory: ${{ matrix.module }}
run: go mod download
# Use the official golangci-lint action for this submodule
- name: Lint ${{ matrix.module }}
uses: golangci/golangci-lint-action@v9
with:
version: v2.4.0
working-directory: ${{ matrix.module }}
only-new-issues: true
args: --timeout=9m
# Job for checking filename conventions
linting_party:
name: Linting Party🥳
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
- name: Set up Go environment
uses: actions/setup-go@v5
with:
go-version: 1.25
# Check file naming conventions using ls-lint
- name: Check for file names errors
uses: ls-lint/action@v2.3.1
with:
config: .ls-lint.yml
================================================
FILE: .github/workflows/typos.yml
================================================
name: Typos Check
on:
push:
pull_request:
jobs:
typos:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: typos-action
uses: crate-ci/typos@v1.44.0
================================================
FILE: .github/workflows/website-prod.yml
================================================
name: Build and Deploy
permissions:
contents: read
on:
push:
tags:
- "v*.*.*"
env:
APP_NAME: gofr-website
WEBSITE_REGISTRY: ghcr.io
GAR_PROJECT: raramuri-tech
GAR_REGISTRY: kops-dev
CLUSTER_NAME: raramuri-tech
CLUSTER_PROJECT: raramuri-tech
NAMESPACE: gofr-dev
NAMESPACE_STAGE: gofr-dev-stg
jobs:
dockerize:
if: ${{ github.repository == 'gofr-dev/gofr' }}
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
outputs:
image: ${{ steps.output-image.outputs.image }}
name: 🐳 Dockerize
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GAR
uses: docker/login-action@v4
with:
registry: us-central1-docker.pkg.dev
username: _json_key
password: ${{ secrets.GOFR_WEBSITE_GOFR_DEV_DEPLOYMENT_KEY }}
- name: Log in to the GitHub Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.WEBSITE_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download UI Image
run: |
docker pull ${{ env.WEBSITE_REGISTRY }}/gofr-dev/website:latest
- name: Determine Image Tag
id: determine-tag
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG=${GITHUB_REF#refs/tags/}
else
TAG=${{ github.sha }}
fi
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Build and Push Image
uses: docker/build-push-action@v7
with:
push: true
context: ./
file: ./docs/Dockerfile
tags: us-central1-docker.pkg.dev/${{ env.GAR_PROJECT }}/${{ env.GAR_REGISTRY }}/${{ env.APP_NAME }}:${{ env.TAG }}
- id: output-image
run: echo "image=`echo us-central1-docker.pkg.dev/${{ env.GAR_PROJECT }}/${{ env.GAR_REGISTRY }}/${{ env.APP_NAME }}:${{ env.TAG }}`" >> "$GITHUB_OUTPUT"
deployment:
runs-on: ubuntu-latest
name: 🚀 Deploy-Prod
needs: dockerize
container:
image: ghcr.io/zopsmart/gha-images:deployments-0.1.3
options: --rm
env:
image: ${{needs.dockerize.outputs.image}}
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Authorize to GCP service account
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GOFR_WEBSITE_GOFR_DEV_DEPLOYMENT_KEY }}
- name: Set GCloud Project and Fetch Cluster Credentials
run: gcloud container clusters get-credentials ${{ env.CLUSTER_NAME }} --region=us-central1 --project=${{ env.CLUSTER_PROJECT }}
- name: Update Deployment Image
run: kubectl set image deployment/${{ env.APP_NAME }} ${{ env.APP_NAME }}=${{ env.image }} --namespace ${{ env.NAMESPACE }}
================================================
FILE: .github/workflows/website-stage.yml
================================================
name: Build and Deploy Website To Stage
permissions:
contents: read
on:
push:
branches:
- development
env:
APP_NAME: gofr-website
WEBSITE_REGISTRY: ghcr.io
GAR_PROJECT: raramuri-tech
GAR_REGISTRY: kops-dev
CLUSTER_NAME: raramuri-tech
CLUSTER_PROJECT: raramuri-tech
NAMESPACE: gofr-dev
NAMESPACE_STAGE: gofr-dev-stg
jobs:
dockerize:
if: ${{ github.repository == 'gofr-dev/gofr' }}
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
outputs:
image: ${{ steps.output-image.outputs.image }}
name: 🐳 Dockerize
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to GAR
uses: docker/login-action@v4
with:
registry: us-central1-docker.pkg.dev
username: _json_key
password: ${{ secrets.GOFR_WEBSITE_GOFR_DEV_STG_DEPLOYMENT_KEY }}
- name: Log in to the GitHub Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.WEBSITE_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download UI Image
run: |
docker pull ${{ env.WEBSITE_REGISTRY }}/gofr-dev/website:latest
- name: Determine Image Tag
id: determine-tag
run: |
if [[ "${{ github.ref }}" == refs/tags/* ]]; then
TAG=${GITHUB_REF#refs/tags/}
else
TAG=${{ github.sha }}
fi
echo "TAG=$TAG" >> $GITHUB_ENV
- name: Build and Push Image
uses: docker/build-push-action@v7
with:
push: true
context: ./
file: ./docs/Dockerfile
tags: us-central1-docker.pkg.dev/${{ env.GAR_PROJECT }}/${{ env.GAR_REGISTRY }}/${{ env.APP_NAME }}:${{ env.TAG }}
- id: output-image
run: echo "image=`echo us-central1-docker.pkg.dev/${{ env.GAR_PROJECT }}/${{ env.GAR_REGISTRY }}/${{ env.APP_NAME }}:${{ env.TAG }}`" >> "$GITHUB_OUTPUT"
deployment_stage:
runs-on: ubuntu-latest
name: 🚀 Deploy-Stage
needs: dockerize
container:
image: ghcr.io/zopsmart/gha-images:deployments-0.1.3
options: --rm
env:
image: ${{needs.dockerize.outputs.image}}
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Authorize to GCP service account
uses: google-github-actions/auth@v3
with:
credentials_json: ${{ secrets.GOFR_WEBSITE_GOFR_DEV_STG_DEPLOYMENT_KEY }}
- name: Set GCloud Project and Fetch Cluster Credentials
run: gcloud container clusters get-credentials ${{ env.CLUSTER_NAME }} --region=us-central1 --project=${{ env.CLUSTER_PROJECT }}
- name: Update Deployment Image
run: kubectl set image deployment/${{ env.APP_NAME }} ${{ env.APP_NAME }}=${{ env.image }} --namespace ${{ env.NAMESPACE_STAGE }}
================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.txt
# Dependency directories (remove the comment below to include it)
vendor/
.idea
*.impl
# IDE Cache
.vscode
.DS_Store
================================================
FILE: .golangci.yml
================================================
version: "2"
linters:
default: none
enable:
- asciicheck
- bodyclose
- canonicalheader
- copyloopvar
- dogsled
- dupl
- err113
- errcheck
- errorlint
- exhaustive
- funlen
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godot
- goprintffuncname
- gosec
- govet
- ineffassign
- lll
- mirror
- misspell
- mnd
- nakedret
- nestif
- noctx
- nolintlint
- prealloc
- revive
- rowserrcheck
- staticcheck
- testifylint
- thelper
- unconvert
- unparam
- unused
- usestdlibvars
- usetesting
- whitespace
- wsl_v5
# don't enable:
# - godox # Disabling because we need TODO lines at this stage of project.
# - testpackage # We also need to do unit test for unexported functions. And adding _internal in all files is cumbersome.
settings:
dupl:
threshold: 100
exhaustive:
default-signifies-exhaustive: false
funlen:
lines: 100
statements: 50
goconst:
min-len: 2
min-occurrences: 2
gocritic:
disabled-checks:
- dupImport # https://github.com/go-critic/go-critic/issues/845
- ifElseChain
- octalLiteral
- whyNoLint
- wrapperFunc
enabled-tags:
- diagnostic
- experimental
- opinionated
- performance
- style
gocyclo:
min-complexity: 10
govet:
enable:
- shadow
settings:
printf:
funcs:
- (gofr.dev/pkg/gofr/Logger).Logf
- (gofr.dev/pkg/gofr/Logger).Errorf
lll:
line-length: 140
misspell:
locale: US
mnd:
checks:
- argument
- case
- condition
- return
nolintlint:
require-explanation: true # require an explanation for nolint directives
require-specific: true # require nolint directives to be specific about which linter is being skipped
allow-unused: false # report any unused nolint directives
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-block
- name: error-naming
- name: error-return
- name: error-strings
- name: errorf
- name: exported
arguments:
# enables checking public methods of private types
- checkPrivateReceivers
# make error messages clearer
- sayRepetitiveInsteadOfStutters
- name: increment-decrement
- name: indent-error-flow
- name: range
- name: receiver-naming
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: unreachable-code
- name: unused-parameter
- name: var-declaration
- name: var-naming
- name: bare-return
- name: bool-literal-in-expr
- name: comment-spacings
- name: early-return
- name: defer
- name: deep-exit
- name: unused-receiver
- name: use-any
staticcheck:
checks:
- all
- -QF1001 # TODO remove this line and fix reported errors
- -QF1003 # TODO remove this line and fix reported errors
- -QF1008 # TODO remove this line and fix reported errors
- -ST1000 # TODO remove this line and fix reported errors
usestdlibvars:
time-layout: true
exclusions:
presets:
- common-false-positives # TODO fix errors reported by this and remove this line
- legacy # TODO fix errors reported by this and remove this line
- std-error-handling # TODO remove this line, configure errcheck, and fix reported errors
rules:
- linters:
- dupl
- goconst
- mnd
path: _test\.go
- linters:
- revive
text: "exported (.+) should have comment" # TODO fix errors reported by this and remove this line
paths:
- examples # TODO remove this line and fix reported errors
formatters:
enable:
- gci
- gofmt
settings:
gci:
sections:
- standard
- default
- localmodule
goimports:
local-prefixes:
- gofr.dev
================================================
FILE: .ls-lint.yml
================================================
---
ls:
.go: snake_case
.pb.go: snake_case
================================================
FILE: .qlty/.gitignore
================================================
*
!configs
!configs/**
!hooks
!hooks/**
!qlty.toml
!.gitignore
================================================
FILE: .qlty/configs/.hadolint.yaml
================================================
ignored:
- DL3008
================================================
FILE: .qlty/configs/.yamllint.yaml
================================================
rules:
document-start: disable
quoted-strings:
required: only-when-needed
extra-allowed: ["{|}"]
key-duplicates: {}
octal-values:
forbid-implicit-octal: true
================================================
FILE: .qlty/qlty.toml
================================================
# This file was automatically generated by `qlty init`.
# You can modify it to suit your needs.
# We recommend you to commit this file to your repository.
#
# This configuration is used by both Qlty CLI and Qlty Cloud.
#
# Qlty CLI -- Code quality toolkit for developers
# Qlty Cloud -- Fully automated Code Health Platform
#
# Try Qlty Cloud: https://qlty.sh
#
# For a guide to configuration, visit https://qlty.sh/d/config
# Or for a full reference, visit https://qlty.sh/d/qlty-toml
config_version = "0"
exclude_patterns = [
"*_min.*",
"*-min.*",
"*.min.*",
"**/.yarn/**",
"**/*.d.ts",
"**/assets/**",
"**/bower_components/**",
"**/build/**",
"**/cache/**",
"**/config/**",
"**/db/**",
"**/deps/**",
"**/dist/**",
"**/extern/**",
"**/external/**",
"**/generated/**",
"**/Godeps/**",
"**/gradlew/**",
"**/mvnw/**",
"**/node_modules/**",
"**/protos/**",
"**/seed/**",
"**/target/**",
"**/templates/**",
"**/testdata/**",
"**/vendor/**",
"**/mock_*",
"**/*_test.go",
]
test_patterns = [
"**/test/**",
"**/spec/**",
"**/*.test.*",
"**/*.spec.*",
"**/*_test.*",
"**/*_spec.*",
"**/test_*.*",
"**/spec_*.*",
]
[smells]
mode = "comment"
[smells.boolean_logic]
threshold = 10
enabled = true
[smells.file_complexity]
threshold = 85 # Increased from 66 to match your codeclimate complex-logic: 10 scaled appropriately
enabled = true
[smells.return_statements]
threshold = 25 # Significantly increased from 8 to match your codeclimate threshold
enabled = true
[smells.nested_control_flow]
threshold = 4
enabled = true
[smells.function_parameters]
threshold = 6
enabled = true
[smells.function_complexity]
threshold = 13
enabled = true
# CRITICAL: Completely disable duplication detection like in Code Climate
[smells.duplication]
enabled = false
[[source]]
name = "default"
default = true
# CRITICAL TRIAGE RULES: These will suppress the specific issues shown in your screenshot
# Completely ignore similar-code issues (22 occurrences in your screenshot)
[[triage]]
match.rules = ["qlty:similar-code"]
match.file_patterns = ["**/*.go"]
set.ignored = true
# Completely ignore identical-code issues (4 occurrences in your screenshot)
[[triage]]
match.rules = ["qlty:identical-code"]
match.file_patterns = ["**/*.go"]
set.ignored = true
# Set function-parameters issues to low priority instead of medium (10 occurrences)
[[triage]]
match.rules = ["qlty:function-parameters"]
match.file_patterns = ["**/*.go"]
set.level = "low"
set.mode = "monitor"
# Set return-statements issues to low priority (6 occurrences)
[[triage]]
match.rules = ["qlty:return-statements"]
match.file_patterns = ["**/*.go"]
set.level = "low"
set.mode = "monitor"
# Set file-complexity issues to low priority (1 occurrence)
[[triage]]
match.rules = ["qlty:file-complexity"]
match.file_patterns = ["**/*.go"]
set.level = "low"
set.mode = "monitor"
# Set nested-control-flow issues to low priority (1 occurrence)
[[triage]]
match.rules = ["qlty:nested-control-flow"]
match.file_patterns = ["**/*.go"]
set.level = "low"
set.mode = "monitor"
# Additional safeguard: Set all medium-level structure issues to monitor mode
[[triage]]
match.plugins = ["qlty"]
match.levels = ["medium"]
match.file_patterns = ["**/*.go"]
set.mode = "monitor"
# Set all duplication category issues to be ignored
[[triage]]
match.plugins = ["qlty"]
match.category = ["duplication"]
set.ignored = true
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
support[at]gofr.dev.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
## Contribution Guidelines
* Minor changes can be done directly by editing code on GitHub. GitHub automatically creates a temporary branch and
files a PR. This is only suitable for really small changes like: spelling fixes, variable name changes or error string
change etc. For larger commits, the following steps are recommended.
* (Optional) If you want to discuss your implementation with the users of GoFr, use the GitHub discussions of this repo.
* Configure your editor to use goimports and golangci-lint on file changes. Any code which is not formatted using these
tools, will fail on the pipeline.
* Contributors should begin working on an issue only after it has been assigned to them. To get an issue assigned, please comment on the GitHub thread
and request assignment from a maintainer. This helps avoid duplicate or conflicting pull requests from multiple contributors.
* Issues labeled triage are not open for direct contributions. If you're interested in working on a triage issue, please reach out to the maintainers
to discuss it before proceeding in the GitHub thread.
* We follow **American English** conventions in this project (e.g., *"favor"* instead of *"favour"*). Please keep this consistent across all code comments, documentation, etc.
* All code contributions should have associated tests and all new line additions should be covered in those test cases.
No PR should ever decrease the overall code coverage.
* Once your code changes are done along with the test cases, submit a PR to development branch. Please note that all PRs
are merged from feature branches to development first.
* A PR should be raised only when development is complete and the code is ready for review. This approach helps reduce the number of open pull requests and facilitates a more efficient review process for the team.
* All PRs need to be reviewed by at least 2 GoFr developers. They might reach out to you for any clarification.
* Thank you for your contribution. :)
### GoFr Testing Policy:
Testing is a crucial aspect of software development, and adherence to these guidelines ensures the stability, reliability, and maintainability of the GoFr codebase.
### Guidelines
1. **Test Types:**
- Write unit tests for every new function or method.
- Include integration tests for any major feature added.
2. **Test Coverage:**
- No new code should decrease the existing code coverage for the packages and files.
> The `code-climate` coverage check will not pass if there is any decrease in the test-coverage before and after any new PR is submitted.
3. **Naming Conventions:**
- Prefix unit test functions with `Test`.
- Use clear and descriptive names.
```go
func TestFunctionName(t *testing.T) {
// Test logic
}
```
4. **Table-Driven Tests:**
- Consider using table-driven tests for testing multiple scenarios.
> [!NOTE]
> Some services will be required to pass the entire test suite. We recommend using docker for running those services.
```console
docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=user -e MONGO_INITDB_ROOT_PASSWORD=password mongodb/mongodb-community-server:latest
docker run -d -p 21:21 -p 21000-21010:21000-21010 -e USERS='user|password' delfer/alpine-ftp-server
# the docker image is relatively unstable. Alternatively, refer to official guide of OpenTSDB to locally setup OpenTSDB env.
# http://opentsdb.net/docs/build/html/installation.html#id1
docker run -d --name gofr-opentsdb -p 4242:4242 petergrace/opentsdb-docker:latest
docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30
docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5
docker run --name gofr-solr -p 2020:8983 solr -DzkRun
docker run --name gofr-zipkin -d -p 2005:9411 openzipkin/zipkin:2
docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack
docker run --name cassandra-node -d -p 9042:9042 -v cassandra_data:/var/lib/cassandra cassandra:latest
docker run --name gofr-pgsql -d -e POSTGRES_DB=customers -e POSTGRES_PASSWORD=root123 -p 2006:5432 postgres:15.1
docker run --name gofr-mssql -d -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=reallyStrongPwd123' -p 2007:1433 mcr.microsoft.com/azure-sql-edge
docker run --name kafka-1 -p 9092:9092 \
-e KAFKA_ENABLE_KRAFT=yes \
-e KAFKA_CFG_PROCESS_ROLES=broker,controller \
-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e ALLOW_PLAINTEXT_LISTENER=yes \
-e KAFKA_CFG_NODE_ID=1 \
-v kafka_data:/bitnami \
bitnami/kafka:3.4
docker pull scylladb/scylla
docker run --name scylla -d -p 2025:9042 scylladb/scylla
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:latest -js
docker pull surrealdb/surrealdb:latest
docker run --name surrealdb -d -p 8000:8000 surrealdb/surrealdb:latest start --bind 0.0.0.0:8000
docker run -d --name arangodb \
-p 8529:8529 \
-e ARANGO_ROOT_PASSWORD=rootpassword \
--pull always \
arangodb:latest
docker run --name dynamodb-local -d -p 8000:8000 amazon/dynamodb-local
docker run -d --name db -p 8091-8096:8091-8096 -p 11210-11211:11210-11211 couchbase
docker login container-registry.oracle.com
docker pull container-registry.oracle.com/database/free:latest
docker run -d --name oracle-free -p 1521:1521 -e ORACLE_PWD=YourPasswordHere container-registry.oracle.com/database/free:latest
docker run -it --rm -p 4443:4443 -e STORAGE_EMULATOR_HOST=0.0.0.0:4443 fsouza/fake-gcs-server:latest
```
> [!NOTE]
> Please note that the recommended local port for the services are different from the actual ports. This is done to avoid conflict with the local installation on developer machines. This method also allows a developer to work on multiple projects which uses the same services but bound on different ports. One can choose to change the port for these services. Just remember to add the same in configs/.local.env, if you decide to do that.
### Coding Guidelines
* Use only what is given to you as part of function parameter or receiver. No globals. Inject all dependencies including
DB, Logger etc.
* No magic. So, no init. In a large project, it becomes difficult to track which package is doing what at the
initialization step.
* Exported functions must have an associated godoc.
* Sensitive data(username, password, keys) should not be pushed. Always use environment variables.
* Take interfaces and return concrete types.
- Lean interfaces - take 'exactly' what you need, not more. Onus of interface definition is on the package who is
using it. so, it should be as lean as possible. This makes it easier to test.
- Be careful of type assertions in this context. If you take an interface and type assert to a type - then it's
similar to taking concrete type.
* Uses of context:
- We should use context as a first parameter.
- Can not use string as a key for the context. Define your own type and provide context accessor method to avoid
conflict.
* External Library uses:
- A little copying is better than a little dependency.
- All external dependencies should go through the same careful consideration, we would have done to our own written
code. We need to test the functionality we are going to use from an external library, as sometimes library
implementation may change.
- All dependencies must be abstracted as an interface. This will make it easier to switch libraries at later point
of time.
* Version tagging as per Semantic versioning (https://semver.org/)
### Documentation
* After adding or modifying existing code, update the documentation too - [development/docs](https://github.com/gofr-dev/gofr/tree/development/docs).
* When you consider a new documentation page is needed, start by adding a new file and writing your new documentation. Then - add a reference to it in [navigation.js](https://gofr.dev/docs/navigation.js).
* If needed, update or add proper code examples for your changes.
* In case images are needed, add it to [docs/public](./docs/public) folder.
* Make sure you don't break existing links and references.
* Maintain Markdown standards, you can read more [here](https://www.markdownguide.org/basic-syntax/), this includes:
- Headings (`#`, `##`, etc.) should be placed in order.
- Use trailing white space or the HTML tag at the end of the line.
- Use "`" sign to add single line code and "```" to add multi-line code block.
- Use relative references to images (in `public` folder as mentioned above.)
* The [gofr.dev documentation](https://gofr.dev/docs) site is updated upon push to `/docs` path in the repo. Verify your changes are live after next GoFr version.
================================================
FILE: Dockerfile
================================================
FROM golang:1.24
RUN mkdir -p /go/src/gofr.dev
WORKDIR /go/src/gofr.dev
COPY . .
RUN go build -ldflags "-linkmode external -extldflags -static" -a examples/http-server/main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /go/src/gofr.dev/main /main
EXPOSE 8000
CMD ["/main"]
================================================
FILE: LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: README.md
================================================
## 🎯 **Goal**
GoFr is designed to **simplify microservice development**, with key focuses on **Kubernetes deployment** and **out-of-the-box observability**. While capable of building generic applications, **microservices** remain at its core.
---
## 💡 **Key Features**
1. **Simple API Syntax**
2. **REST Standards by Default**
3. **Configuration Management**
4. **[Observability](https://gofr.dev/docs/quick-start/observability)** (Logs, Traces, Metrics)
5. **Inbuilt [Auth Middleware](https://gofr.dev/docs/advanced-guide/http-authentication)** & Custom Middleware Support
6. **[gRPC Support](https://gofr.dev/docs/advanced-guide/grpc)**
7. **[HTTP Service](https://gofr.dev/docs/advanced-guide/http-communication)** with Circuit Breaker Support
8. **[Pub/Sub](https://gofr.dev/docs/advanced-guide/using-publisher-subscriber)**
9. **[Health Check](https://gofr.dev/docs/advanced-guide/monitoring-service-health)** for All Datasources
10. **[Database Migration](https://gofr.dev/docs/advanced-guide/handling-data-migrations)**
11. **[Cron Jobs](https://gofr.dev/docs/advanced-guide/using-cron)**
12. **Support for [Changing Log Level](https://gofr.dev/docs/advanced-guide/remote-log-level-change) Without Restarting**
13. **[Swagger Rendering](https://gofr.dev/docs/advanced-guide/swagger-documentation)**
14. **[Abstracted File Systems](https://gofr.dev/docs/advanced-guide/handling-file)**
15. **[Websockets](https://gofr.dev/docs/advanced-guide/websocket)**
---
## 🚀 **Getting Started**
### **Prerequisites**
- GoFr requires **[Go](https://go.dev/)** version **[1.24](https://go.dev/doc/devel/release#go1.24.0)** or above.
### **Installation**
To get started with GoFr, add the following import to your code and use Go’s module support to automatically fetch dependencies:
```go
import "gofr.dev/pkg/gofr"
```
Alternatively, use the command:
```bash
go get -u gofr.dev/pkg/gofr
```
---
## 🏃 **Running GoFr**
Here's a simple example to get a GoFr application up and running:
```go
package main
import "gofr.dev/pkg/gofr"
func main() {
app := gofr.New()
app.GET("/greet", func(ctx *gofr.Context) (any, error) {
return "Hello World!", nil
})
app.Run() // listens and serves on localhost:8000
}
```
To run this code:
```bash
$ go run main.go
```
Visit [`localhost:8000/greet`](http://localhost:8000/greet) to see the result.
---
## 📂 **More Examples**
Explore a variety of ready-to-run examples in the [GoFr examples directory](https://github.com/gofr-dev/gofr/tree/development/examples).
---
## 👩💻 **Documentation**
- **[GoDoc](https://pkg.go.dev/gofr.dev)**: Official API documentation.
- **[GoFr Documentation](https://gofr.dev/docs)**: Comprehensive guides and resources.
---
## 👍 **Contribute**
Join Us in Making GoFr Better
**Share your experience**: If you’ve found GoFr helpful, consider writing a review or tutorial on platforms like **[Medium](https://medium.com/)**, **[Dev.to](https://dev.to/)**, or your personal blog.
Your insights could help others get started faster!
**Contribute to the project**: Want to get involved? Check out our **[CONTRIBUTING.md](CONTRIBUTING.md)**
guide to learn how you can contribute code, suggest improvements, or report issues.
---
## 🔒 **Secure Cloning**
To securely clone the GoFr repository, you can use HTTPS or SSH:
### Cloning with HTTPS
```bash
git clone https://github.com/gofr-dev/gofr.git
```
### Cloning with SSH
```bash
git clone git@github.com:gofr-dev/gofr.git
```
### 🎁 **Get a GoFr T-Shirt & Stickers!**
If your PR is merged, or if you contribute by writing articles or promoting GoFr, we invite you to fill out [this form](https://forms.gle/R1Yz7ZzY3U5WWTgy5) to claim your GoFr merchandise as a token of our appreciation!
### Partners
================================================
FILE: SECURITY.md
================================================
# Security Policy
## Supported Versions
Following versions are being supported for security updates.
| Version | Supported |
| ------- | ------------------ |
| 1.0.x | :white_check_mark: |
## Reporting a Vulnerability
To report a vulnerability, please file an issue on this repo and add "security" label. Security related issues will be prioritized over others. We strive to triage the issue within a single working day.
================================================
FILE: docs/Dockerfile
================================================
FROM ghcr.io/gofr-dev/website:latest AS builder
WORKDIR /app
COPY docs/quick-start /app/src/app/docs/quick-start
COPY docs/public/ /app/public
COPY docs/advanced-guide /app/src/app/docs/advanced-guide
COPY docs/datasources /app/src/app/docs/datasources
COPY docs/references /app/src/app/docs/references
COPY docs/page.md /app/src/app/docs
COPY docs/navigation.js /app/src/lib
COPY docs/events.json /app/src/app/events
COPY docs/testimonials.json /app/utils
ENV NODE_ENV=production
RUN npm install
RUN npm run build
# Stage 2: Serve with Static Server
FROM zopdev/static-server:v0.0.5
# Copy static files from the builder stage
COPY --from=builder /app/out static
# Expose the port server is running on
EXPOSE 8000
# Start go server
CMD ["/main"]
================================================
FILE: docs/advanced-guide/authentication/page.md
================================================
# Authentication
Authentication is a crucial aspect of web applications, controlling access to resources based on user roles or permissions.
It is the process of verifying a user's identity to grant access to protected resources. It ensures that only authenticated
users can perform actions or access data within an application.
GoFr offers a **Unified Authentication** model, meaning that once you enable an authentication method, it automatically
applies to both your HTTP and gRPC services.
## Exempted Paths
By default, the authentication middleware exempts the following paths from authentication:
- `/.well-known/alive`: Used for liveness probes, should be publicly accessible for health checks.
The health check endpoint `/.well-known/health` is exempted by default, but as it may contain sensitive information about the service and its dependencies, it is recommended to require authentication for it.
## 1. Basic Auth
*Basic Authentication* is a simple authentication scheme where the user's credentials (username and password) are
transmitted in the request header in a Base64-encoded format.
Basic auth is the simplest way to authenticate your APIs. It's built on
{% new-tab-link title="HTTP protocol authentication scheme" href="https://datatracker.ietf.org/doc/html/rfc7617" /%}.
It involves sending the prefix `Basic` trailed by the Base64-encoded `:` within the standard `Authorization` header.
### Usage in GoFr
GoFr offers two ways to implement basic authentication:
**1. Predefined Credentials**
Use `EnableBasicAuth(username, password)` to configure GoFr with pre-defined credentials.
```go
func main() {
app := gofr.New()
app.EnableBasicAuth("admin", "secret_password") // Replace with your credentials
app.GET("/protected-resource", func(c *gofr.Context) (any, error) {
return "Success", nil
})
app.Run()
}
```
**2. Custom Validation Function**
Use `EnableBasicAuthWithValidator(validationFunc)` to implement your own validation logic for credentials.
The `validationFunc` takes the username and password as arguments and returns true if valid, false otherwise.
```go
func validateUser(c *container.Container, username, password string) bool {
// Implement your credential validation logic here
return username == "john" && password == "doe123"
}
func main() {
app := gofr.New()
app.EnableBasicAuthWithValidator(validateUser)
app.Run()
}
```
## 2. API Keys Auth
*API Key Authentication* is an authentication scheme where a unique API key is included in the request header `X-Api-Key` for validation against a store of authorized keys.
### Usage in GoFr
GoFr offers two ways to implement API Keys authentication.
**1. Framework Default Validation**
- GoFr's default validation can be selected using **_EnableAPIKeyAuth(apiKeys ...string)_**
```go
func main() {
app := gofr.New()
app.EnableAPIKeyAuth("9221e451-451f-4cd6-a23d-2b2d3adea9cf", "0d98ecfe-4677-48aa-b463-d43505766915")
app.Run()
}
```
**2. Custom Validation Function**
- GoFr allows a custom validator function for validating APIKeys using **_EnableAPIKeyAuthWithValidator(validator)_**
```go
func apiKeyValidator(c *container.Container, apiKey string) bool {
validKeys := []string{"f0e1dffd-0ff0-4ac8-92a3-22d44a1464e4"}
return slices.Contains(validKeys, apiKey)
}
func main() {
app := gofr.New()
app.EnableAPIKeyAuthWithValidator(apiKeyValidator)
app.Run()
}
```
## 3. OAuth 2.0
{% new-tab-link title="OAuth" href="https://www.rfc-editor.org/rfc/rfc6749" /%} 2.0 is the industry-standard protocol for authorization.
It involves sending the prefix `Bearer` trailed by the encoded token within the standard `Authorization` header.
### Usage in GoFr
Enable OAuth 2.0 to authenticate requests. Use `EnableOAuth(jwks-endpoint, refresh_interval, options ...jwt.ParserOption)` to configure GoFr.
```go
func main() {
app := gofr.New()
app.EnableOAuth("http://jwks-endpoint", 3600)
app.Run()
}
```
### Available JWT Claim Validations
- **Expiration (`exp`)**: Validated by default if present. Use `jwt.WithExpirationRequired()` to make it mandatory.
- **Audience (`aud`)**: `jwt.WithAudience("https://api.example.com")`
- **Issuer (`iss`)**: `jwt.WithIssuer("https://auth.example.com")`
- **Subject (`sub`)**: `jwt.WithSubject("user@example.com")`
## Accessing Auth Info in Handlers
Once authenticated, you can retrieve the authentication information from the context using the `GetAuthInfo()` method. This works identically for both HTTP and gRPC handlers.
```go
func MyHandler(ctx *gofr.Context) (any, error) {
authInfo := ctx.GetAuthInfo()
// For Basic Auth
username := authInfo.GetUsername()
// For API Key
apiKey := authInfo.GetAPIKey()
// For OAuth
claims := authInfo.GetClaims()
if claims != nil {
// Access specific claims (typecasting is required for specific claim values)
userID := claims["sub"].(string)
}
return "Success", nil
}
```
## Security Best Practices
* **Timing Attacks**: GoFr's Basic Auth and API Key interceptors use `subtle.ConstantTimeCompare` to prevent timing attacks.
* **TLS**: Always use TLS in production to encrypt the authentication credentials and tokens transmitted over the network.
================================================
FILE: docs/advanced-guide/building-cli-applications/page.md
================================================
# Building CLI Applications
GoFr provides a simple way to build command-line applications using `app.NewCMD()`. This creates standalone CLI tools without starting an HTTP server.
## Configuration
To configure logging for CLI applications, set the following environment variable:
- `CMD_LOGS_FILE`: The file path where CLI logs will be written. If not set, logs are discarded.
## Getting Started
Create a basic CLI application with subcommands:
```go
package main
import (
"fmt"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.NewCMD()
// Simple hello command
app.SubCommand("hello", func(c *gofr.Context) (any, error) {
return "Hello World!", nil
}, gofr.AddDescription("Print hello message"))
// Command with parameters
app.SubCommand("greet", func(c *gofr.Context) (any, error) {
name := c.Param("name")
if name == "" {
name = "World"
}
return fmt.Sprintf("Hello, %s!", name), nil
})
app.Run()
}
```
## Key GoFr CLI Methods
- **`app.NewCMD()`**: Initialize a CLI application
- **`app.SubCommand(name, handler, options...)`**: Add a subcommand
- **`gofr.AddDescription(desc)`**: Add help description
- **`gofr.AddHelp(help)`**: Add detailed help text
- **`ctx.Param(name)`**: Get command parameters
- **`ctx.Out.Println()`**: Print to stdout
- **`ctx.Logger`**: Access logging
## Running CLI Applications
Build and run your CLI:
```bash
go build -o mycli
./mycli hello
./mycli greet --name John
./mycli --help
```
## Example Commands
```bash
# Basic command
./mycli hello
# Output: Hello World!
# Command with parameter
./mycli greet --name Alice
# Output: Hello, Alice!
# Help
./mycli --help
```
For more details, see the [sample-cmd example](../../../examples/sample-cmd).
================================================
FILE: docs/advanced-guide/circuit-breaker/page.md
================================================
# Circuit Breaker in HTTP Communication
Calls to remote resources and services can fail due to temporary issues like slow network connections or timeouts, service unavailability. While transient faults can be mitigated using the "Retry pattern", there are cases where continual retries are futile, such as during severe service failures.
In such scenarios, it's crucial for applications to recognize when an operation is unlikely to succeed and handle the failure appropriately rather than persistently retrying. Indiscriminate use of HTTP retries can even lead to unintentional denial-of-service attacks within the software itself, as multiple clients may flood a failing service with retry attempts.
To prevent this, a defense mechanism like the circuit breaker pattern is essential. Unlike the "Retry pattern" which aims to eventually succeed, the circuit breaker pattern focuses on preventing futile operations. While these patterns can be used together, it's vital for the retry logic to be aware of the circuit breaker's feedback and cease retries if the circuit breaker indicates a non-transient fault.
GoFr inherently provides the functionality, it can be enabled by passing circuit breaker configs as options to `AddHTTPService()` method.
## How It Works:
The circuit breaker tracks consecutive failed requests for a downstream service.
- **Threshold:** The number of consecutive failed requests after which the circuit breaker transitions to an open state. While open, all requests to that service will fail immediately without making any actual outbound calls, effectively preventing request overflow to an already failing service.
- **Interval:** Once the circuit is open, GoFr starts a background goroutine that periodically checks the health of the service by making requests to its aliveness endpoint (by default: `/.well-known/alive`) at the specified interval. When the service is deemed healthy again, the circuit breaker transitions directly from **Open** to **Closed**, allowing requests to resume.
> GoFr's circuit breaker implementation does not use a **Half-Open** state. Instead, it relies on periodic asynchronous health checks to determine service recovery.
## Failure Conditions
The Circuit Breaker counts a request as "failed" if:
1. An error occurs during the HTTP request (e.g., network timeout, connection refused).
2. The response status code is **greater than 500** (e.g., 502, 503, 504).
> **Note:** HTTP 500 Internal Server Error is **NOT** counted as a failure for the circuit breaker. This distinguishes between application bugs (500) and service availability issues (> 500).
## Health Check Requirement
For the Circuit Breaker to recover from an **Open** state, the downstream service **must** expose a health check endpoint that returns a `200 OK` status code.
- **Default Endpoint:** `/.well-known/alive`
- **Custom Endpoint:** Can be configured using `service.HealthConfig`.
> [!WARNING]
> If the downstream service does not have a valid health check endpoint (returns 404 or other errors), the Circuit Breaker will **never recover** and will remain permanently Open. Ensure your services implement the health endpoint correctly.
## Interaction with Retry
When using both Retry and Circuit Breaker patterns, the **order of wrapping** is critical for effective resilience:
- **Recommended: Retry as the Outer Layer**
In this configuration, the `Retry` layer wraps the `Circuit Breaker`. Every single retry attempt is tracked by the circuit breaker. If a request retries 5 times, the circuit breaker sees 5 failures. This allows the circuit to trip quickly during a "retry storm," protecting the downstream service from excessive load.
- **Non-Recommended: Circuit Breaker as the Outer Layer**
If the `Circuit Breaker` wraps the `Retry` layer, it only sees the **final result** of the entire retry loop. Even if a request retries 10 times internally, the circuit breaker only counts it as **1 failure**. This delays the circuit's reaction and can lead to hundreds of futile calls hitting a failing service before the breaker finally trips.
> [!IMPORTANT]
> Always ensure `Retry` is the outer layer by providing the `CircuitBreakerConfig` **before** the `RetryConfig` in the `AddHTTPService` options.
> NOTE: Retries only occur when the target service responds with a status code > 500 (e.g., 502 Bad Gateway, 503 Service Unavailable). 500 Internal Server Error and client errors (4xx) are considered non-transient or bug-related and will not trigger retries.
## Usage
```go
package main
import (
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/service"
)
func main() {
// Create a new application
app := gofr.New()
app.AddHTTPService("order", "https://order-func",
&service.CircuitBreakerConfig{
// Number of consecutive failed requests after which circuit breaker will be enabled
Threshold: 4,
// Time interval at which circuit breaker will hit the health endpoint.
Interval: 1 * time.Second,
},
)
app.GET("/order", Get)
// Run the application
app.Run()
}
```
Circuit breaker state changes to open when number of consecutive failed requests increases the threshold.
When it is in open state, GoFr makes request to the health endpoint (default being - /.well-known/alive, or the custom endpoint if configured) at an equal interval of time provided in config.
GoFr publishes the following metric to track circuit breaker state:
- `app_http_circuit_breaker_state`: Current state of the circuit breaker (0 for Closed, 1 for Open). This metric is used to visualize a historical timeline of circuit transitions on the dashboard.
> ##### Check out the example of an inter-service HTTP communication along with circuit-breaker in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-http-service/main.go)
================================================
FILE: docs/advanced-guide/custom-spans-in-tracing/page.md
================================================
# Custom Spans In Tracing
GoFr's built-in tracing provides valuable insights into application's behavior. However, sometimes we might need
even more granular details about specific operations within your application. This is where `custom spans` can be used.
## How it helps?
By adding custom spans in traces to our requests, we can:
- **Gain granular insights:** Custom spans allows us to track specific operations or functions within your application,
providing detailed performance data.
- **Identify bottlenecks:** Analyzing custom spans helps to pinpoint areas of your code that may be causing
performance bottlenecks or inefficiencies.
- **Improve debugging:** Custom spans enhance the ability to debug issues by providing visibility into the execution
flow of an application.
## Usage
To add a custom trace to a request, GoFr context provides `Trace()` method, which takes the name of the span as an argument
and returns a trace.Span.
```go
func MyHandler(c context.Context) error {
span := c.Trace("my-custom-span")
defer span.Close()
// Do some work here
return nil
}
```
In this example, **my-custom-span** is the name of the custom span that is added to the request.
The defer statement ensures that the span is closed even if an error occurs to ensure that the trace is properly recorded.
> ##### Check out the example of creating a custom span in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/http-server/main.go#L58)
================================================
FILE: docs/advanced-guide/dealing-with-sql/page.md
================================================
# Dealing with SQL
GoFr simplifies the process of connecting to SQL databases where one needs to add respective configs in .env,
which allows connecting to different SQL dialects(MySQL, PostgreSQL, SQLite) without going into complexity of configuring connections.
With GoFr, connecting to different SQL databases is as straightforward as setting the DB_DIALECT environment variable to the respective dialect.
## Usage for PostgreSQL and MySQL
To connect with PostgreSQL, set `DB_DIALECT` to `postgres`. Similarly, To connect with MySQL, simply set `DB_DIALECT` to `mysql`.
```dotenv
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=root123
DB_NAME=test_db
DB_PORT=3306
DB_DIALECT=postgres
```
## Usage for SQLite
To connect with SQLite, set `DB_DIALECT` to `sqlite` and `DB_NAME` to the name of your DB File. If the DB file already exists then it will be used otherwise a new one will be created.
```dotenv
DB_NAME=test.db
DB_DIALECT=sqlite
```
## Setting Max open and Idle Connections
To set max open and idle connection for any MySQL, PostgreSQL, SQLite.
Add the following configs in `.env` file.
```dotenv
DB_MAX_IDLE_CONNECTION=5 // Default 2
DB_MAX_OPEN_CONNECTION=5 // Default unlimited
```
> ##### Check out the example on how to add configuration for SQL in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/http-server/configs/.env)
================================================
FILE: docs/advanced-guide/debugging/page.md
================================================
# Using `pprof` in GoFr Applications
In GoFr applications, `pprof` profiling is automatically enabled. The profiling endpoints are served on the `METRICS_PORT`, which defaults to `2121` if not specified.
This guide explains how to enable and use `pprof` in GoFr applications.
---
## Enabling `pprof` in GoFr
### Prerequisites
Ensure the `METRICS_PORT` is set (default is `2121`):
```bash
METRICS_PORT=2121
```
GoFr automatically registers the following `pprof` routes:
- `/debug/pprof/cmdline`
- `/debug/pprof/profile`
- `/debug/pprof/symbol`
- `/debug/pprof/trace`
- `/debug/pprof/` (index)
---
## Accessing `pprof` Endpoints
Once `pprof` is enabled, you can access the profiling endpoints at `http://localhost:/debug/pprof/`. For example, if `METRICS_PORT` is `2121`, the endpoints will be available at:
- `http://localhost:2121/debug/pprof/`
### Available Endpoints
1. **`/debug/pprof/cmdline`**:
- Returns the command-line arguments of the running application.
2. **`/debug/pprof/profile`**:
- Generates a CPU profile for the application.
3. **`/debug/pprof/symbol`**:
- Resolves program counters into function names.
4. **`/debug/pprof/trace`**:
- Captures an execution trace of the application.
5. **`/debug/pprof/` (index)**:
- Provides an index page with links to all available profiling endpoints, including memory, goroutine, and blocking profiles.
---
## Collecting Profiling Data
### 1. **CPU Profiling**
To collect a CPU profile:
```bash
curl -o cpu.pprof http://localhost:2121/debug/pprof/profile
```
### 2. **Memory Profiling**
To collect a memory profile:
```bash
curl -o mem.pprof http://localhost:2121/debug/pprof/heap
```
### 3. **Goroutine Profiling**
To collect information about running goroutines:
```bash
curl -o goroutine.pprof http://localhost:2121/debug/pprof/goroutine
```
### 4. **Execution Trace**
To collect an execution trace:
```bash
curl -o trace.out http://localhost:2121/debug/pprof/trace
```
---
## Analyzing Profiling Data
### 1. Using go tool pprof
To analyze CPU, memory, or goroutine profiles:
```bash
go tool pprof
```
#### **`top`**
Shows the functions consuming the most resources (e.g., CPU or memory).
```bash
go tool pprof cpu.pprof
(pprof) top
```
#### **`list`**
Displays the source code of a specific function, along with resource usage.
```bash
(pprof) list
```
Example:
```bash
(pprof) list main.myFunction
```
#### **`web`**
Generates a visual representation of the profile in your browser. This requires Graphviz to be installed.
```bash
(pprof) web
```
### 2. Using go tool trace
To analyze execution traces:
```bash
go tool trace trace.out
```
---
## Example Workflow
1. **Set Environment Variables**:
```bash
METRICS_PORT=2121
```
2. **Run Your GoFr Application**:
```bash
go run main.go
```
3. **Collect Profiling Data**:
- Collect a CPU profile:
```bash
curl -o cpu.pprof http://localhost:2121/debug/pprof/profile
```
- Collect a memory profile:
```bash
curl -o mem.pprof http://localhost:2121/debug/pprof/heap
```
4. **Analyze the Data**:
- Analyze the CPU profile:
```bash
go tool pprof cpu.pprof
(pprof) top
(pprof) list main.myFunction
(pprof) web
```
- Analyze the memory profile:
```bash
go tool pprof mem.pprof
(pprof) top
(pprof) list main.myFunction
(pprof) web
```
---
## References
- [Go `pprof` Documentation](https://pkg.go.dev/net/http/pprof)
- [Profiling Go Programs](https://blog.golang.org/profiling-go-programs)
- [Go Execution Tracer](https://golang.org/doc/diagnostics.html#tracing)
================================================
FILE: docs/advanced-guide/gofr-errors/page.md
================================================
# Error Handling
GoFr provides a structured error handling approach to simplify error management in your applications.
The errors package in GoFr provides functionality for handling errors in GoFr applications. It includes predefined HTTP
and database errors, as well as the ability to create custom errors with additional context.
## Pre-defined HTTP Errors
GoFr's `http` package offers several predefined error types to represent common HTTP error scenarios. These errors
automatically handle HTTP status code selection. These include:
{% table %}
- Error Type
- Description
- Status Code
---
- `ErrorInvalidParam`
- Represents an error due to an invalid parameter
- 400 (Bad Request)
---
- `ErrorMissingParam`
- Represents an error due to a missing parameter
- 400 (Bad Request)
---
- `ErrorEntityNotFound`
- Represents an error due to a not found entity
- 404 (Not Found)
---
- `ErrorEntityAlreadyExist`
- Represents an error due to creation of duplicate entity
- 409 (Conflict)
---
- `ErrorInvalidRoute`
- Represents an error for invalid route
- 404 (Not Found)
---
- `ErrorRequestTimeout`
- Represents an error for request which timed out
- 408 (Request Timeout)
---
- `ErrorPanicRecovery`
- Represents an error for request which panicked
- 500 (Internal Server Error)
{% /table %}
#### Usage:
To use the predefined HTTP errors, users need to import the GoFr http package and can simply call them:
```go
import "gofr.dev/pkg/gofr/http"
err := http.ErrorMissingParam{Params: []string{"id"}}
```
## Database Errors
Database errors in GoFr, represented in the `datasource` package, encapsulate errors related to database operations such
as database connection, query failure, availability etc. The `ErrorDB` struct can be used to populate `error` as well as
any custom message to it. **Status Code: 500 (Internal Server Error)**
#### Usage:
```go
import "gofr.dev/pkg/gofr/datasource"
// Creating a custom error wrapped in underlying error for database operations
dbErr := datasource.ErrorDB{Err: err, Message: "error from sql db"}
// Adding stack trace to the error
dbErr = dbErr.WithStack()
// Creating a custom error only with error message and no underlying error.
dbErr2 := datasource.ErrorDB{Message : "database connection timed out!"}
```
## Custom Errors
GoFr's error structs implements an interface with `Error() string` and `StatusCode() int` methods, users can override the
status code by implementing it for their custom error.
Users can optionally define a log level for your error with the `LogLevel() logging.Level` methods
#### Usage:
```go
type customError struct {
error string
}
func (c customError) Error() string {
return fmt.Sprintf("custom error: %s", c.error)
}
func (c customError) StatusCode() int {
return http.StatusMethodNotAllowed
}
func (c customError) LogLevel() logging.Level {
return logging.WARN
}
```
## Extended Error Responses
For [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html) style error responses with additional fields, implement the ResponseMarshaller interface:
```go
type ResponseMarshaller interface {
Response() map[string]any
}
```
#### Usage:
```go
type ValidationError struct {
Field string
Message string
Code int
}
func (e ValidationError) Error() string { return e.Message }
func (e ValidationError) StatusCode() int { return e.Code }
func (e ValidationError) Response() map[string]any {
return map[string]any{
"field": e.Field,
"type": "validation_error",
"details": "Invalid input format",
}
}
```
> [!NOTE]
> The `message` field is automatically populated from the `Error()` method. Custom fields with the name "message" in the `Response()` map should not be used as they will be ignored in favor of the `Error()` value.
================================================
FILE: docs/advanced-guide/graphql/page.md
================================================
# GraphQL in GoFr
GoFr provides a **Schema-First** approach to building GraphQL APIs. This means you define your API contract in a standard GraphQL schema file, and GoFr handles the execution, validation, and observability.
## Required Setup
To enable GraphQL, you MUST provide a schema file at the following location:
`./configs/schema.graphqls`
> **Note:** GoFr uses a single schema file. All Query and Mutation types must be defined in this one file.
> You can register multiple resolvers (one per field) using `GraphQLQuery` and `GraphQLMutation`, but
> they all resolve fields within this single schema.
If this file is missing or invalid, GoFr will log a fatal error and the application will fail to start. This fail-fast behavior ensures schema issues are caught at deployment rather than runtime.
## Core Concepts
### 1. [Query](https://graphql.org/learn/queries/)
Queries are used to fetch data. In GoFr, a Query resolver is a function that takes `*gofr.Context` and returns a data object (or `any`) and an error.
### 2. [Mutation](https://graphql.org/learn/queries/#mutations)
Mutations are used to modify data. They follow the same signature as Queries but are intended for side effects.
## The Unified Schema
GoFr aggregates every `GraphQLQuery` and `GraphQLMutation` you register and validates them against your `./configs/schema.graphqls`. The API is served at `/graphql`.
* **Single Endpoint**: All operations go through `POST /graphql`.
* **Playground**: Interactive documentation and testing at `/.well-known/graphql/ui`.
---
## Getting Started
### 1. Define your Schema
Create `configs/schema.graphqls`:
```graphql
type User {
id: Int
name: String
}
type Query {
user(id: Int): User
}
```
### 2. Register Resolvers
In GoFr, resolvers strictly take `*gofr.Context`. You use `c.Bind()` to extract arguments.
```go
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
app := gofr.New()
app.GraphQLQuery("user", func(c *gofr.Context) (any, error) {
var args struct {
ID int `json:"id"`
}
if err := c.Bind(&args); err != nil {
return nil, err
}
// Return a struct - GoFr validates this against the schema at runtime
return User{
ID: args.ID,
Name: "Antigravity",
}, nil
})
app.Run()
}
```
---
## Schema-First Features
### 1. Returns `any`
Unlike standard HTTP handlers which allow `any` but lose structure, GraphQL handlers in GoFr return `any` while **maintaining the contract** defined in your `.graphqls` file.
- GoFr leverages the underlying `graphql-go` engine to validate the returned object against your defined schema.
- If the object does not match the schema types, GoFr returns an error in the `errors` array with partial data where applicable.
### 2. HTTP Status Codes
GoFr follows the standard GraphQL-over-HTTP convention by returning `200 OK` for all successfully processed requests, including those with resolver errors. This ensures that the response body is the source of truth for execution results.
| Status Code | Condition |
|---|---|
| `200 OK` | The request was processed (regardless of whether it returned data or errors). |
| `400 Bad Request` | The request body is not valid JSON. |
**Error response body**:
> **Note:** The GraphQL error format follows the [GraphQL specification](https://spec.graphql.org/October2021/#sec-Errors),
> which uses an `errors` array. This differs from GoFr's REST API format which uses a singular `error` object.
> This is intentional — each protocol follows its own standard.
```json
{
"data": null,
"errors": [
{
"message": "your error message here",
"locations": [{ "line": 1, "column": 3 }],
"path": ["fieldName"]
}
]
}
```
### 3. Argument Binding
Instead of declarative arguments in the function signature, you use the standard `c.Bind()` method. GoFr automatically maps the GraphQL `args` map to your struct using JSON tags.
### 4. Supported Types
GoFr supports all standard GraphQL types including scalars, objects, enums, and input types. For a complete reference on the GraphQL type system, see the [official GraphQL documentation](https://graphql.org/learn/schema/).
---
## Testing Your GraphQL API
### 1. Interactive Exploration
GoFr automatically hosts a **GraphQL Playground** at `/.well-known/graphql/ui` when GraphQL resolvers are registered.
### 2. Standard POST Requests
The `/graphql` endpoint accepts a JSON body with the following fields:
| Field | Type | Description |
|---|---|---|
| `query` | `string` | **Required.** The GraphQL query or mutation string. |
| `operationName` | `string` | Optional. The name of the operation to execute (used for metrics tagging). |
| `variables` | `object` | Optional. A map of variable values for the query. |
**Simple query:**
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "{ user(id: 1) { name } }"}' \
http://localhost:9091/graphql
```
**Named operation with variables:**
```bash
curl -X POST \
-H "Content-Type: application/json" \
-d '{"query": "query GetUser($id: Int) { user(id: $id) { name } }", "operationName": "GetUser", "variables": {"id": 1}}' \
http://localhost:9091/graphql
```
---
## Observability
GoFr provides production-grade observability for GraphQL out of the box.
### 1. Tracing
GoFr automatically instruments your GraphQL API with OpenTelemetry traces:
- **Root Span**: Every request generates a `graphql-request` span.
- **Resolver Spans**: Each individual resolver call generates a nested span (e.g., `graphql-resolver-user`), allowing you to see the exact time spent in each field's business logic.
- **Attributes**: The `graphql.operation_name` and `graphql.operation_type` (query/mutation) are automatically added to the spans.
### 2. Metrics
GoFr exports several GraphQL-specific metrics, all tagged by `operation_name`, `type` (query/mutation), and `status` (success/error):
- **`app_graphql_operations_total`**: Total number of GraphQL operations received.
- **`app_graphql_error_total`**: Total operations that resulted in an error (resolver error or validation failure).
- **`app_graphql_request_duration`**: Histogram of the entire request lifecycle in seconds.
> **Note:** The `operation_name` tag is sourced from the `operationName` field in the POST body. For anonymous operations, it defaults to `"unknown"`. GraphQL requests are only recorded by the GraphQL-specific metrics above — they are excluded from `app_http_response` to avoid double-counting.
---
## Monitoring and Health Checks
### 1. Health Checks
Even when building a GraphQL-first application, GoFr's standard **RESTful health check endpoints** remain the primary way to monitor service availability. These are automatically registered and publicly accessible:
- **Aliveness**: `/.well-known/alive` (Returns `200 OK` if the server is running)
- **Health**: `/.well-known/health` (Returns detailed dependency status)
GoFr does **not** inject an automatic `health` query into your GraphQL schema. This avoids redundancy and keeps your GraphQL contract focused on business logic.
### 2. Status Metric Label
While traditional HTTP metrics (`app_http_response`) use numerical status codes (e.g., `200`, `500`) for the `status` label, GraphQL metrics (`app_graphql_*`) use a simplified `success` or `error` value.
- **`success`**: The request was processed and returned no errors in the `errors` array.
- **`error`**: The request was processed but one or more resolvers failed (returning a `200 OK` with an `errors` array), or the request itself was invalid (e.g., `400 Bad Request`).
This distinction is important because GraphQL often returns `200 OK` even when business logic fails. The `success`/`error` label provides immediate visibility into the health of your resolvers.
---
## Design and Limitations
GoFr's GraphQL implementation is designed for simplicity and strict adherence to standards while maintaining the framework's "sane defaults" philosophy.
### 1. Why `GraphQLQuery` / `GraphQLMutation` instead of `app.POST`?
GoFr provides dedicated `GraphQLQuery` and `GraphQLMutation` methods rather than reusing `app.POST("/graphql", ...)` because the framework handles schema validation, resolver dispatch, per-field tracing, and automatic metrics internally. A raw POST handler would require you to implement all of this manually.
### 2. Why POST-only?
Per the [GraphQL-over-HTTP specification](https://github.com/graphql/graphql-over-http), all GraphQL operations (including Queries) should be performed via `POST`.
- **Security**: Preventing Queries over `GET` avoids accidentally exposing sensitive parameters in server logs or browser history.
- **Consistency**: All operations use the same interaction model, simplifying middleware and observability.
### 3. Why only Query and Mutation?
Currently, GoFr supports the two most common operation types:
- **Query**: For read-only data fetching.
- **Mutation**: For operations that cause side effects.
**Subscriptions** (real-time updates) are currently not supported as they require a persistent stateful connection (like WebSockets), which deviates from the stateless, request-response model of GoFr's standard HTTP handlers.
### 4. Single Schema File
GoFr enforces a single `./configs/schema.graphqls` file to ensure a "Single Source of Truth" for your API contract. While you can register many resolvers, they must all belong to this single unified schema. This prevents fragmentation and makes the API easier to document and maintain.
---
## Best Practices
1. **Keep Schema and Logic in sync**: Since the schema is defined in a separate file, ensure field names in your Go maps/structs match the field names in `schema.graphqls`.
2. **Use c.Bind()**: Always use `c.Bind()` for accessing arguments to benefit from GoFr's internal mapping and validation.
3. **Error Handling**: Return errors from your handlers. GoFr will include them in the `errors` array of the GraphQL response while still returning `200 OK`.
4. **Name your operations**: Use `operationName` in your requests so that metrics are tagged meaningfully (e.g., `GetUser` instead of `unknown`).
================================================
FILE: docs/advanced-guide/grpc/page.md
================================================
# gRPC with Gofr
We have already seen how GoFr can help ease the development of HTTP servers, but there are cases where performance is primarily required sacrificing flexibility. In these types of scenarios gRPC protocol comes into picture. {% new-tab-link title="gRPC" href="https://grpc.io/docs/what-is-grpc/introduction/" /%} is an open-source RPC(Remote Procedure Call) framework initially developed by Google.
GoFr streamlines the creation of gRPC servers and clients with unified GoFr's context support.
It provides built-in tracing, metrics, and logging to ensure seamless performance monitoring for both gRPC servers and inter-service gRPC communication.
With GoFr's context, you can seamlessly define custom metrics and traces across gRPC handlers, ensuring consistent observability and streamlined debugging throughout
your system. Additionally, GoFr provides a built-in health check for all your services and supports inter-service
health checks, allowing gRPC services to monitor each other effortlessly.
## Prerequisites
**1. Protocol Buffer Compiler (`protoc`) Installation:**
- **Linux (using `apt` or `apt-get`):**
```bash
sudo apt install -y protobuf-compiler
protoc --version # Ensure compiler version is 3+
```
- **macOS (using Homebrew):**
```bash
brew install protobuf
protoc --version # Ensure compiler version is 3+
```
**2. Go Plugins for Protocol Compiler:**
a. Install protocol compiler plugins for Go:
```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
```
b. Update `PATH` for `protoc` to locate the plugins:
```bash
export PATH="$PATH:$(go env GOPATH)/bin"
```
## Creating Protocol Buffers
For a detailed guide, refer to the official gRPC documentation's tutorial: {% new-tab-link title="Tutorial" href="https://grpc.io/docs/languages/go/basics/" /%} at official gRPC docs.
**1. Define Your Service and RPC Methods:**
Create a `.proto` file (e.g., `customer.proto`) to define your service and the RPC methods it provides:
```protobuf
// Indicates the protocol buffer version that is being used
syntax = "proto3";
// Indicates the go package where the generated file will be produced
option go_package = "path/to/your/proto/file";
service Service {
rpc () returns () {}
}
```
**2. Specify Request and Response Types:**
Users must define the type of message being exchanged between server and client, for protocol buffer to serialize them when making a remote
procedure call. Below is a generic representation for services' gRPC messages type.
```protobuf
message {
int64 id = 1;
string name = 2;
// other fields that can be passed
}
message {
int64 id = 1;
string name = 2;
string address = 3;
// other customer related fields
}
```
**3. Generate Go Code:**
Run the following command to generate Go code using the Go gRPC plugins:
```bash
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
.proto
```
This command generates two files, `.pb.go` and `_grpc.pb.go`, containing the necessary code for performing RPC calls.
## Prerequisite: gofr-cli must be installed
To install the CLI -
```bash
go install gofr.dev/cli/gofr@latest
```
## Generating gRPC Server Handler Template using `gofr wrap grpc server`
**1. Use the `gofr wrap grpc server` Command:**
```bash
gofr wrap grpc server -proto=./path/your/proto/file
```
This command leverages the `gofr-cli` to generate a `_server.go` file (e.g., `customer_server.go`)
containing a template for your gRPC server implementation, including context support, in the same directory as
that of the specified proto file.
**2. Modify the Generated Code:**
- Customize the `GoFrServer` struct with required dependencies and fields.
- Implement the `` method to handle incoming requests, as required in this usecase:
- Bind the request payload using `ctx.Bind(&)`.
- Process the request and generate a response.
## Registering the gRPC Service with Gofr
**1. Import Necessary Packages:**
```go
import (
"path/to/your/generated-grpc-server/packageName"
"gofr.dev/pkg/gofr"
)
```
**2. Register the Service in your `main.go`:**
```go
func main() {
app := gofr.New()
packageName.RegisterServerWithGofr(app, &.NewGoFrServer())
app.Run()
}
```
>Note: By default, gRPC server will run on port 9000, to customize the port users can set `GRPC_PORT` config in the .env
## Adding gRPC Server Options
To customize your gRPC server, use `AddGRPCServerOptions()`.
### Example: Enabling TLS & other ServerOptions
```go
func main() {
app := gofr.New()
// Add TLS credentials and connection timeout in one call
creds, _ := credentials.NewServerTLSFromFile("server-cert.pem", "server-key.pem")
app.AddGRPCServerOptions(
grpc.Creds(creds),
grpc.ConnectionTimeout(10 * time.Second),
)
packageName.RegisterServerWithGofr(app, &.NewGoFrServer())
app.Run()
}
```
## Adding Custom Unary Interceptors
Interceptors help in implementing authentication, validation, request transformation, and error handling.
### Example: Authentication Interceptor
```go
func main() {
app := gofr.New()
app.AddGRPCUnaryInterceptors(authInterceptor)
packageName.RegisterServerWithGofr(app, &.NewGoFrServer())
app.Run()
}
func authInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if !isAuthenticated(ctx) {
return nil, status.Errorf(codes.Unauthenticated, "authentication failed")
}
return handler(ctx, req)
}
```
## Adding Custom Stream interceptors
For streaming RPCs (client-stream, server-stream, or bidirectional), GoFr allows you to add stream interceptors using `AddGRPCServerStreamInterceptors`. These are useful for handling logic that needs to span the entire lifetime of a stream.
```go
func main() {
app := gofr.New()
app.AddGRPCServerStreamInterceptors(streamAuthInterceptor)
// ... register your service
app.Run()
}
func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// Example: Validate metadata for the entire stream
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok || !isValidToken(md["auth-token"]) {
return status.Errorf(codes.Unauthenticated, "invalid stream token")
}
// If valid, continue processing the stream
return handler(srv, ss)
}
```
For more details on adding additional interceptors and server options, refer to the [official gRPC Go package](https://pkg.go.dev/google.golang.org/grpc#ServerOption).
## Rate Limiter Interceptor for gRPC
GoFr provides built-in rate limiter interceptors for gRPC to protect your services from abuse and ensure fair resource distribution.
It uses the same token bucket algorithm and configuration as the HTTP rate limiter, applied to both unary and streaming RPCs.
### Features
- **Token Bucket Algorithm**: Allows smooth rate limiting with configurable burst capacity
- **Per-IP Rate Limiting**: Each client IP gets its own rate limit bucket (configurable)
- **Unary and Stream Support**: Separate interceptors for unary RPCs and streaming RPCs
- **Prometheus Metrics**: Track rate limit violations via `app_grpc_rate_limit_exceeded_total` counter
- **gRPC Status Code**: Returns `RESOURCE_EXHAUSTED` (gRPC code 8) with a `retry-after` metadata header when the limit is exceeded
### Configuration
```go
import (
"context"
"gofr.dev/pkg/gofr"
gofrGrpc "gofr.dev/pkg/gofr/grpc"
"gofr.dev/pkg/gofr/http/middleware"
)
func main() {
app := gofr.New()
// ctx controls the lifetime of the rate limiter's background cleanup goroutine.
// Cancelling this context stops cleanup gracefully, preventing goroutine leaks
// during rolling restarts. In production, tie this to your server's shutdown signal.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Configure rate limiter (shared config for both unary and stream)
cfg := middleware.RateLimiterConfig{
RequestsPerSecond: 5, // Average requests per second
Burst: 10, // Maximum burst size
PerIP: true, // Enable per-IP limiting
}
// IMPORTANT: create ONE shared store if you want a single budget
// for both unary and stream RPCs. If Store is left nil, each
// interceptor will create its own in-memory store and limits
// will be enforced independently.
store := middleware.NewMemoryRateLimiterStore(cfg)
cfg.Store = store
// Add rate limiter interceptors for gRPC
app.AddGRPCUnaryInterceptors(gofrGrpc.UnaryRateLimitInterceptor(ctx, cfg, app.Logger(), app.Metrics()))
app.AddGRPCServerStreamInterceptors(gofrGrpc.StreamRateLimitInterceptor(ctx, cfg, app.Logger(), app.Metrics()))
// Register your gRPC service
packageName.RegisterServerWithGofr(app, &packageName.NewGoFrServer())
app.Run()
}
```
> **Note**: The example above creates a single shared store so unary and stream RPCs draw from the **same** token bucket.
> If you want **independent limits** for each call type (e.g., high throughput for unary, tight limits for streams),
> omit the shared store and pass separate configs — see [Separate Limits for Unary and Stream RPCs](#separate-limits-for-unary-and-stream-rpcs) below.
> **Graceful Shutdown**: The `ctx` parameter controls the lifetime of the background cleanup goroutine that evicts expired token buckets.
> Cancel this context when the server shuts down to prevent goroutine leaks during rolling restarts.
### Parameters
The gRPC rate limiter uses the same `middleware.RateLimiterConfig` as the HTTP rate limiter:
- `RequestsPerSecond`: Average number of requests allowed per second
- `Burst`: Maximum number of requests that can be made in a burst (allows temporary spikes)
- `PerIP`: Set to `true` for per-IP limiting (recommended) or `false` for a global rate limit across all clients
- `TrustedProxies`: *(Optional)* Set to `true` to trust `X-Forwarded-For` and `X-Real-IP` gRPC metadata headers for IP extraction. Only enable when behind a trusted reverse proxy.
> **Security Warning**: Only set `TrustedProxies: true` if your application is behind a trusted reverse proxy (nginx, ALB, etc.).
> Without a trusted proxy, clients can spoof metadata headers to bypass rate limits.
### Behavior on Rate Limit Exceeded
When a client exceeds the rate limit:
1. The interceptor returns a gRPC error with status code `RESOURCE_EXHAUSTED`
2. A `retry-after` response metadata header is set, indicating how many seconds the client should wait before retrying
3. The `app_grpc_rate_limit_exceeded_total` Prometheus counter is incremented with `method` and `type` (`unary` or `stream`) labels
### Separate Limits for Unary and Stream RPCs
Unary calls and stream connections often have very different resource costs. You can pass independent configurations to each interceptor to enforce separate budgets — for example, allowing a high rate for lightweight unary calls while tightly limiting new stream connections:
```go
unaryCfg := middleware.RateLimiterConfig{
RequestsPerSecond: 100, // High throughput for lightweight unary calls
Burst: 50,
PerIP: true,
}
streamCfg := middleware.RateLimiterConfig{
RequestsPerSecond: 5, // Streams are long-lived and expensive
Burst: 3,
PerIP: true,
}
app.AddGRPCUnaryInterceptors(gofrGrpc.UnaryRateLimitInterceptor(ctx, unaryCfg, app.Logger(), app.Metrics()))
app.AddGRPCServerStreamInterceptors(gofrGrpc.StreamRateLimitInterceptor(ctx, streamCfg, app.Logger(), app.Metrics()))
```
Each config creates its own store (when `Store` is nil), so the limits are completely independent. If you instead want a **single shared budget** across both call types, create one store and assign it to both configs as shown in the [Configuration](#configuration) example above.
## Generating gRPC Client using `gofr wrap grpc client`
**1. Use the `gofr wrap grpc client` Command:**
```bash
gofr wrap grpc client -proto=./path/your/proto/file
```
This command leverages the `gofr-cli` to generate a `_client.go` file (e.g., `customer_client.go`). This file must not be modified.
**2. Register the connection to your gRPC service inside your and make inter-service calls as follows :**
```go
// gRPC Handler with context support
func (ctx *gofr.Context) (*, error) {
// Create the gRPC client
srv, err := NewGoFrClient("your-grpc-server-host", ctx.Metrics())
if err != nil {
return nil, err
}
// Prepare the request
req := &{
// populate fields as necessary
}
// Call the gRPC method with tracing/metrics enabled
res, err := srv.(ctx, req)
if err != nil {
return nil, err
}
return res, nil
}
```
## Error Handling and Validation
GoFr's gRPC implementation includes built-in error handling and validation:
**Port Validation**: Automatically validates that gRPC ports are within valid range (1-65535)
**Port Availability**: Checks if the specified port is available before starting the server
**Server Creation**: Validates server creation and provides detailed error messages
**Container Injection**: Validates container injection into gRPC services with detailed logging
Port Configuration
```bash
// Set custom gRPC port in .env file
GRPC_PORT=9001
// Or use default port 9000 if not specified
```
## gRPC Reflection
GoFr supports gRPC reflection for easier debugging and testing. Enable it using the configuration:
```bash
# In your .env file
GRPC_ENABLE_REFLECTION=true
```
When enabled, you can use tools like grpcurl to inspect and test your gRPC services:
```bash
# List available services
grpcurl -plaintext localhost:9000 list
# Describe a service
grpcurl -plaintext localhost:9000 describe YourService
# Make a test call
grpcurl -plaintext -d '{"name": "test"}' localhost:9000 YourService/YourMethod
```
## Built-in Metrics
GoFr automatically registers the following gRPC metrics:
+ **grpc_server_status**: Gauge indicating server status (1=running, 0=stopped)
+ **grpc_server_errors_total**: Counter for total gRPC server errors
+ **grpc_services_registered_total**: Counter for total registered gRPC services
These metrics are automatically available in your metrics endpoint and can be used for monitoring and alerting.
## Customizing gRPC Client with DialOptions
GoFr provides flexibility to customize your gRPC client connections using gRPC `DialOptions`. This allows users to configure aspects such as transport security, interceptors, and load balancing policies.
You can pass optional parameters while creating your gRPC client to tailor the connection to your needs. Here’s an example of a Unary Interceptor that sets metadata on outgoing requests:
```go
func main() {
app := gofr.New()
// Create a gRPC client for the service
gRPCClient, err := client.NewGoFrClient(
app.Config.Get("GRPC_SERVER_HOST"),
app.Metrics(),
grpc.WithChainUnaryInterceptor(MetadataUnaryInterceptor),
)
if err != nil {
app.Logger().Errorf("Failed to create gRPC client: %v", err)
return
}
greet := NewGreetHandler(gRPCClient)
app.GET("/hello", greet.Hello)
app.Run()
}
// MetadataUnaryInterceptor sets a custom metadata value on outgoing requests
func MetadataUnaryInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
md := metadata.Pairs("client-id", "GoFr-Client-123")
ctx = metadata.NewOutgoingContext(ctx, md)
err := invoker(ctx, method, req, reply, cc, opts...)
if err != nil {
return fmt.Errorf("Error in %s: %v", method, err)
}
return err
}
```
This interceptor sets a metadata key `client-id` with a value of `GoFr-Client-123` for each request. Metadata can be used for authentication, tracing, or custom behaviors.
### Using TLS Credentials and Advanced Service Config
By default, gRPC connections in GoFr are made over insecure connections, which is not recommended for production. You can override this behavior using TLS credentials. Additionally, a more comprehensive service configuration can define retry policies and other settings:
```go
import (
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// The default serviceConfig in GoFr only sets the loadBalancingPolicy to "round_robin".
const serviceConfig = `{
"loadBalancingPolicy": "round_robin",
"methodConfig": [{
"name": [{"service": "HelloService"}],
"retryPolicy": {
"maxAttempts": 4,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2.0,
"retryableStatusCodes": ["UNAVAILABLE", "RESOURCE_EXHAUSTED"]
}
}]
}`
func main() {
app := gofr.New()
creds, err := credentials.NewClientTLSFromFile("path/to/cert.pem", "")
if err != nil {
app.Logger().Errorf("Failed to load TLS certificate: %v", err)
return
}
gRPCClient, err := client.NewGoFrClient(
app.Config.Get("GRPC_SERVER_HOST"),
app.Metrics(),
grpc.WithTransportCredentials(creds),
grpc.WithDefaultServiceConfig(serviceConfig),
)
if err != nil {
app.Logger().Errorf("Failed to create gRPC client: %v", err)
return
}
greet := NewGreetHandler(gRPCClient)
app.GET("/hello", greet.Hello)
app.Run()
}
```
In this example:
- `WithTransportCredentials` sets up TLS security.
- `WithDefaultServiceConfig` defines retry policies with exponential backoff and specific retryable status codes.
### Further Reading
For more details on configurable DialOptions, refer to the [official gRPC package for Go](https://pkg.go.dev/google.golang.org/grpc#DialOption).
## HealthChecks in GoFr's gRPC Service/Clients
Health Checks in GoFr's gRPC Services
GoFr provides built-in health checks for gRPC services, enabling observability, monitoring, and inter-service health verification.
### Client Interface
```go
type GoFrClient interface {
SayHello(*gofr.Context, *HelloRequest, ...grpc.CallOption) (*HelloResponse, error)
health
}
type health interface {
Check(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error)
Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse], error)
}
```
### Server Integration
```go
type GoFrServer struct {
health *healthServer
}
```
Supported Methods for HealthCheck :
```go
func (h *healthServer) Check(ctx *gofr.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error)
func (h *healthServer) Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, stream grpc_health_v1.Health_WatchServer) error
func (h *healthServer) SetServingStatus(ctx *gofr.Context, service string, status grpc_health_v1.HealthCheckResponse_ServingStatus)
func (h *healthServer) Shutdown(ctx *gofr.Context)
func (h *healthServer) Resume(ctx *gofr.Context)
```
> ##### Check out the example of setting up a gRPC server/client in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/tree/main/examples/grpc)
================================================
FILE: docs/advanced-guide/grpc-streaming/page.md
================================================
# gRPC Streaming with GoFr
GoFr provides comprehensive support for gRPC streaming, enabling efficient real-time communication between services. Streaming is particularly useful for scenarios where you need to send or receive multiple messages over a single connection, such as chat applications, real-time data feeds, or large file transfers.
GoFr supports three types of gRPC streaming:
- **Server-side streaming**: The server sends multiple responses to a single client request
- **Client-side streaming**: The client sends multiple requests and receives a single response
- **Bidirectional streaming**: Both client and server can send multiple messages independently
All streaming methods in GoFr include built-in tracing, metrics, and logging support, ensuring seamless observability for your streaming operations.
## Prerequisites
Before implementing gRPC streaming, ensure you have:
1. **Protocol Buffer Compiler (`protoc`)** installed (version 3+)
2. **Go gRPC plugins** installed:
```bash
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
export PATH="$PATH:$(go env GOPATH)/bin"
```
3. **gofr-cli** installed:
```bash
go install gofr.dev/cli/gofr@latest
```
For detailed setup instructions, refer to the [gRPC with GoFr documentation](https://gofr.dev/docs/advanced-guide/grpc).
## Defining Streaming RPCs in Protocol Buffers
To use streaming in your gRPC service, define your RPC methods with the `stream` keyword in your `.proto` file:
```protobuf
syntax = "proto3";
option go_package = "path/to/your/proto/file";
message Request {
string message = 1;
}
message Response {
string message = 1;
}
service ChatService {
// Server-side streaming: client sends one request, server sends multiple responses
rpc ServerStream(Request) returns (stream Response);
// Client-side streaming: client sends multiple requests, server sends one response
rpc ClientStream(stream Request) returns (Response);
// Bidirectional streaming: both client and server can send multiple messages
rpc BiDiStream(stream Request) returns (stream Response);
}
```
## Generating gRPC Streaming Server Code
GoFr CLI automatically generates streaming-aware server templates. Use the `gofr wrap grpc server` command:
```bash
gofr wrap grpc server -proto=./path/to/your/proto/file
```
This command generates:
- `_server.go`: Template file with streaming method signatures
- `_gofr.go`: Generated wrapper with streaming instrumentation
- `request_gofr.go`: Request wrapper for context binding
- `health_gofr.go`: Health check server integration
### Server-Side Streaming Implementation
Server-side streaming allows the server to send multiple responses to a single client request. This is useful for scenarios like real-time notifications or progressive data delivery.
**Example Implementation:**
```go
func (s *ChatServiceGoFrServer) ServerStream(ctx *gofr.Context, stream ChatService_ServerStreamServer) error {
// Bind the initial request
req := Request{}
if err := ctx.Bind(&req); err != nil {
return status.Errorf(codes.InvalidArgument, "invalid request: %v", err)
}
// Send multiple responses
for i := 0; i < 5; i++ {
// Check if context is canceled
select {
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
default:
}
resp := &Response{
Message: fmt.Sprintf("Server stream %d: %s", i, req.Message),
}
if err := stream.Send(resp); err != nil {
return status.Errorf(codes.Internal, "error sending stream: %v", err)
}
time.Sleep(1 * time.Second) // Simulate processing delay
}
return nil
}
```
**Key Points:**
- Use `ctx.Bind()` to extract the initial request
- Return appropriate gRPC status codes for binding errors
- Check for context cancellation before each send operation
- Call `stream.Send()` to send each response message
- Return `nil` when streaming is complete, or an error if something goes wrong
### Client-Side Streaming Implementation
Client-side streaming allows the client to send multiple requests before receiving a single response. This is useful for batch processing or aggregating data from the client.
**Example Implementation:**
```go
func (s *ChatServiceGoFrServer) ClientStream(ctx *gofr.Context, stream ChatService_ClientStreamServer) error {
var messageCount int
var finalMessage strings.Builder
// Receive multiple messages from client
for {
// Check if context is canceled before receiving
select {
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
default:
}
req, err := stream.Recv()
if err == io.EOF {
// Client has finished sending, send final response
return stream.SendAndClose(&Response{
Message: fmt.Sprintf("Received %d messages. Final: %s",
messageCount, finalMessage.String()),
})
}
if err != nil {
return status.Errorf(codes.Internal, "error receiving stream: %v", err)
}
// Process each message
messageCount++
finalMessage.WriteString(req.Message + " ")
}
}
```
**Key Points:**
- Check for context cancellation before each receive operation
- Use `stream.Recv()` in a loop to receive messages
- Check for `io.EOF` to detect when the client has finished sending
- Return appropriate gRPC status codes for receive errors
- Call `stream.SendAndClose()` to send the final response and close the stream
- Process each message as it arrives
### Bidirectional Streaming Implementation
Bidirectional streaming allows both client and server to send messages independently. This is useful for real-time chat applications or interactive protocols.
**Example Implementation:**
```go
func (s *ChatServiceGoFrServer) BiDiStream(ctx *gofr.Context, stream ChatService_BiDiStreamServer) error {
errChan := make(chan error)
// Handle incoming messages in a goroutine
go func() {
for {
// Check if context is canceled
select {
case <-stream.Context().Done():
errChan <- status.Error(codes.Canceled, "client disconnected")
return
default:
}
req, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
errChan <- status.Errorf(codes.Internal, "error receiving stream: %v", err)
return
}
// Process request and send response
resp := &Response{Message: "Echo: " + req.Message}
if err := stream.Send(resp); err != nil {
errChan <- status.Errorf(codes.Internal, "error sending stream: %v", err)
return
}
}
errChan <- nil
}()
// Wait for completion or cancellation
select {
case err := <-errChan:
return err
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
}
}
```
**Key Points:**
- Use goroutines to handle concurrent send/receive operations
- Check for context cancellation in the goroutine before receiving
- Use `stream.Recv()` to receive messages
- Use `stream.Send()` to send responses
- Return appropriate gRPC status codes for errors
- Monitor `stream.Context().Done()` to handle client disconnections
- Use channels to coordinate between goroutines
## Generating gRPC Streaming Client Code
Generate the client code using:
```bash
gofr wrap grpc client -proto=./path/to/your/proto/file
```
This generates `_client.go` with streaming client interfaces.
### Server-Side Streaming Client Usage
**Example Implementation:**
```go
func (c *ChatHandler) ServerStreamHandler(ctx *gofr.Context) (any, error) {
// Initiate server stream
stream, err := c.chatClient.ServerStream(ctx, &client.Request{
Message: "stream request",
})
if err != nil {
return nil, fmt.Errorf("failed to initiate server stream: %v", err)
}
var responses []Response
// Receive all streamed responses
for {
res, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break // Stream completed
}
return nil, fmt.Errorf("stream receive error: %v", err)
}
responses = append(responses, res)
ctx.Logger.Infof("Received: %s", res.Message)
}
return responses, nil
}
```
### Client-Side Streaming Client Usage
**Example Implementation:**
```go
func (c *ChatHandler) ClientStreamHandler(ctx *gofr.Context) (any, error) {
// Initiate client stream
stream, err := c.chatClient.ClientStream(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initiate client stream: %v", err)
}
// Get messages from request body
var requests []*client.Request
if err := ctx.Bind(&requests); err != nil {
return nil, fmt.Errorf("failed to bind requests: %v", err)
}
// Send multiple messages
for _, req := range requests {
if err := stream.Send(req); err != nil {
return nil, fmt.Errorf("failed to send request: %v", err)
}
}
// Close stream and receive final response
response, err := stream.CloseAndRecv()
if err != nil {
return nil, fmt.Errorf("failed to receive final response: %v", err)
}
return response, nil
}
```
### Bidirectional Streaming Client Usage
**Example Implementation:**
```go
func (c *ChatHandler) BiDiStreamHandler(ctx *gofr.Context) (any, error) {
// Initiate bidirectional stream
stream, err := c.chatClient.BiDiStream(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initiate bidirectional stream: %v", err)
}
respChan := make(chan Response)
errChan := make(chan error)
// Receive responses in a goroutine
go func() {
for {
res, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
errChan <- nil
} else {
errChan <- err
}
return
}
respChan <- res
}
}()
// Send messages
messages := []string{"message 1", "message 2", "message 3"}
for _, msg := range messages {
if err := stream.Send(&client.Request{Message: msg}); err != nil {
return nil, fmt.Errorf("failed to send message: %v", err)
}
}
// Close send side
if err := stream.CloseSend(); err != nil {
return nil, fmt.Errorf("failed to close send: %v", err)
}
// Collect responses
var responses []Response
for {
select {
case err := <-errChan:
return responses, err
case resp := <-respChan:
responses = append(responses, resp)
case <-time.After(5 * time.Second):
return nil, errors.New("timeout waiting for responses")
}
}
}
```
## Registering Streaming Services
Register your streaming service in `main.go` just like unary services:
```go
package main
import (
"gofr.dev/examples/grpc/grpc-streaming-server/server"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Register streaming service
server.RegisterChatServiceServerWithGofr(app, server.NewChatServiceGoFrServer())
app.Run()
}
```
## Built-in Observability
GoFr automatically provides observability for all streaming operations:
### Metrics
The following metrics are automatically registered:
- **app_gRPC-Stream_stats**: Histogram tracking stream operation duration (Send, Recv, SendAndClose, CloseSend)
- **app_gRPC-Client-Stream_stats**: Histogram for client-side streaming operations
### Tracing
Each streaming operation (Send, Recv, SendAndClose, CloseSend) automatically creates spans for distributed tracing, allowing you to track the flow of messages through your system.
### Logging
Streaming operations are automatically logged with:
- Operation type (Send, Recv, etc.)
- Method name
- Duration
- Error status (if any)
## Error Handling
### Common Streaming Errors
1. **`io.EOF`**: Indicates the stream has ended normally
- In client-side streaming: Server should call `SendAndClose()`
- In server-side/bidirectional streaming: Client has finished sending
2. **Context Cancellation**: Stream was canceled or timed out
- Check `stream.Context().Done()` for cancellation
- Return appropriate gRPC status codes
3. **Network Errors**: Connection issues during streaming
- Handle gracefully and return appropriate error status
**Example Error Handling:**
```go
func (s *ChatServiceGoFrServer) ServerStream(ctx *gofr.Context, stream ChatService_ServerStreamServer) error {
req := Request{}
if err := ctx.Bind(&req); err != nil {
return status.Errorf(codes.InvalidArgument, "invalid request: %v", err)
}
for i := 0; i < 5; i++ {
// Check if context is canceled
select {
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
default:
}
resp := &Response{Message: fmt.Sprintf("Message %d", i)}
if err := stream.Send(resp); err != nil {
return status.Errorf(codes.Internal, "error sending stream: %v", err)
}
}
return nil
}
```
## Adding Custom Stream interceptors
For streaming RPCs (client-stream, server-stream, or bidirectional), GoFr allows you to add stream interceptors using `AddGRPCServerStreamInterceptors`. These are useful for handling logic that needs to span the entire lifetime of a stream.
```go
func main() {
app := gofr.New()
app.AddGRPCServerStreamInterceptors(streamAuthInterceptor)
// ... register your service
app.Run()
}
func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
// Example: Validate metadata for the entire stream
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok || !isValidToken(md["auth-token"]) {
return status.Errorf(codes.Unauthenticated, "invalid stream token")
}
// If valid, continue processing the stream
return handler(srv, ss)
}
```
For more details on adding additional interceptors and server options, refer to the [official gRPC Go package](https://pkg.go.dev/google.golang.org/grpc#ServerOption).
## Best Practices
1. **Always handle `io.EOF`**: This is the normal way streams end
2. **Monitor context cancellation**: Use `stream.Context().Done()` to detect client disconnections
3. **Use goroutines for bidirectional streams**: Allows concurrent send/receive operations
4. **Close streams properly**: Call `CloseSend()` when done sending in bidirectional streams
5. **Handle errors gracefully**: Return appropriate gRPC status codes
6. **Use timeouts**: Set reasonable timeouts for stream operations
7. **Log important events**: Use `ctx.Logger` to log stream lifecycle events
## Examples
Complete working examples are available in the GoFr repository:
- **Server Example**: `gofr/examples/grpc/grpc-streaming-server`
- **Client Example**: `gofr/examples/grpc/grpc-streaming-client`
These examples demonstrate all three types of streaming with detailed error handling and logging.
## Further Reading
- [gRPC with GoFr](https://gofr.dev/docs/advanced-guide/grpc) - General gRPC documentation
- [gRPC Official Documentation](https://grpc.io/docs/what-is-grpc/introduction/) - Learn more about gRPC streaming concepts
- [GoFr Examples](https://github.com/gofr-dev/gofr/tree/main/examples/grpc) - More gRPC examples
================================================
FILE: docs/advanced-guide/handling-data-migrations/page.md
================================================
# Handling Data Migrations
If you make manual changes to your database, you must inform other developers so they can apply the same changes. Additionally, you need to keep track of which changes should be applied to production machines in the next deployment.
GoFr supports data migrations for MySQL, Postgres, Redis, ClickHouse & Cassandra which allows you to modify database state — such as adding columns, changing data types, adding constraints, or managing keys.
## Usage
### Creating Migration Files
It is recommended to maintain a `migrations` directory in your project root to enhance readability and maintainability.
**Migration file names**
It is recommended that each migration file should be numbered in the format of _YYYYMMDDHHMMSS_ when the migration was created.
This prevents numbering conflicts and ensures migrations sort correctly across different filesystems.
Run the following commands to create a migration file
```shell
# Install GoFr CLI
go install gofr.dev/cli/gofr@latest
# Create migration
gofr migrate create -name=create_employee_table
```
Add the `createTableEmployee` function given below in the created file in `migrations` directory.
**Filename : 20240226153000_create_employee_table.go**
```go
package migrations
import "gofr.dev/pkg/gofr/migration"
const createTable = `CREATE TABLE IF NOT EXISTS employee
(
id int not null
primary key,
name varchar(50) not null,
gender varchar(6) not null,
contact_number varchar(10) not null
);`
func createTableEmployee() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
_, err := d.SQL.Exec(createTable)
if err != nil {
return err
}
return nil
},
}
}
```
`migration.Datasource` contains the supported datasources, i.e., Redis and SQL (MySQL and PostgreSQL).
All migrations run within a transaction.
For MySQL, use `IF EXISTS` and `IF NOT EXISTS` in DDL commands because MySQL implicitly commits these statements.
**Create a function which returns all the migrations in a map**
**Filename : all.go**
```go
package migrations
import "gofr.dev/pkg/gofr/migration"
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
20240226153000: createTableEmployee(),
}
}
```
Migrations run in ascending order of keys in this map.
> **Best Practice:** Before creating multiple migrations, learn about [organizing migrations by feature](#organizing-migrations-by-feature) to avoid creating one migration per table or operation.
### Initialization from main.go
```go
package main
import (
"gofr.dev/examples/using-migrations/migrations"
"gofr.dev/pkg/gofr"
)
func main() {
// Create a new application
a := gofr.New()
// Add migrations to run
a.Migrate(migrations.All())
// Run the application
a.Run()
}
```
When we run the app we will see the following logs for migrations which ran successfully.
```bash
INFO [16:55:46] Migration 20240226153000 ran successfully
```
GoFr maintains the records in the database itself which helps in tracking which migrations have already been executed and ensures that only migrations that have never been run are executed.
## Organizing Migrations by Feature
**Important:** Migrations should be organized by **feature**, not by individual database operations. The migration history should tell the story of feature evolution, not database operation granularity.
### Bad Practice: One Migration Per Operation
A common mistake is to create one migration for each table or operation, even when they're part of the same feature:
```go
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
20251114000001: createTableUsers(),
20251114000002: createTableMonitors(),
20251114000003: createTableCheckResults(),
20251114000004: createTableIncidents(),
}
}
```
**Why this is problematic:**
- When reverting a feature, you want to revert all related changes together
- When deploying, you want to deploy the entire feature atomically
- Having multiple migrations for a single feature creates unnecessary complexity and potential inconsistencies
### Good Practice: One Migration Per Feature
Instead, group all database operations related to a single feature into one migration:
```go
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
20251114000001: addMonitoringFeature(), // Creates all 4 tables together
}
}
func addMonitoringFeature() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
// Create all tables for the monitoring feature
if _, err := d.SQL.Exec(createTableUsers); err != nil {
return err
}
if _, err := d.SQL.Exec(createTableMonitors); err != nil {
return err
}
if _, err := d.SQL.Exec(createTableCheckResults); err != nil {
return err
}
if _, err := d.SQL.Exec(createTableIncidents); err != nil {
return err
}
return nil
},
}
}
```
**Benefits of this approach:**
- **Atomic deployment:** The entire feature is deployed or reverted together
- **Clear history:** Migration history reflects feature evolution, not granular operations
- **Easier rollback:** Reverting a feature means reverting one migration, not tracking multiple related migrations
- **Better organization:** Related changes stay together, making the codebase easier to understand
## Multi-Instance Deployments
When running multiple instances of your application (e.g., in Kubernetes or Docker Swarm), GoFr automatically coordinates migrations to ensure only one instance runs them at a time.
### How It Works
1. **Automatic Coordination:** When multiple instances start simultaneously, they coordinate using distributed locks
2. **One Runs, Others Wait:** The first instance to acquire the lock runs migrations, while others wait
3. **Fast Path:** If migrations are already complete, instances return immediately without acquiring locks
### Lock Mechanism
**SQL (MySQL/PostgreSQL/SQLite):**
- Uses a dedicated `gofr_migration_locks` table
- Lock TTL: 15 seconds
- Heartbeat: Refreshes every 5 seconds for long migrations
**Redis:**
- Uses `SETNX` with TTL
- Lock TTL: 15 seconds
- Heartbeat: Refreshes every 5 seconds for long migrations
**Retry Behavior:**
- Max retries: Indefinite (pods wait until migration is complete)
- Retry interval: 500ms
### What This Means for You
**✅ No code changes needed** - Locking happens automatically
**✅ Safe deployments** - Multiple instances won't corrupt data
**✅ Long migrations supported** - Locks are automatically extended via heartbeat
**✅ Crash recovery** - Locks auto-expire after 15 seconds if a pod crashes
### Example Deployment
\`\`\`yaml
# docker-compose.yaml or Kubernetes deployment
services:
app:
image: myapp:latest
replicas: 3 # All 3 instances coordinate automatically
\`\`\`
When you deploy:
- Instance 1: Acquires lock → Runs migrations → Releases lock
- Instance 2: Waits for lock → Sees migrations complete → Continues startup
- Instance 3: Waits for lock → Sees migrations complete → Continues startup
> **Note:** Single-instance deployments work exactly as before with no performance impact.
## Migration Records
**SQL**
Migration records are stored in **gofr_migrations** table which has the following schema:
{% table %}
- Field
- Type
---
- version
- bigint
---
- method
- varchar(4)
---
- start_time
- timestamp
---
- duration
- bigint
---
{% /table %}
**REDIS**
Migration records are stored and maintained in a Redis Hash named **gofr_migrations** where key is the version and value contains other details in JSON format.
Example :
Key: 20240226153000
Value: {"method":"UP","startTime":"2024-02-26T15:03:46.844558+05:30","duration":0}
Explanation:
**Version** : The migration version is the numeric key defined in the map.
**Start Time** : Time when the migration started in UTC.
**Duration** : Time taken by Migration since it started in milliseconds.
**Method** : It indicates whether the migration ran in UP or DOWN mode.
(For now only method UP is supported)
> **Note**: For Redis migration using **Streams mode**, a consumer group ID is mandatory. An empty group ID will result in an error during subscription, however, publishing will still succeed.
### Migrations in Cassandra
`GoFr` provides support for migrations in Cassandra but does not guarantee atomicity for individual DML commands. To achieve atomicity during migrations, users can leverage batch operations using the `NewBatch`, `BatchQuery`, and `ExecuteBatch` methods. These methods allow multiple queries to be executed as a single atomic operation.
Alternatively, users can construct their batch queries using the `BEGIN BATCH` and `APPLY BATCH` statements to ensure that all the commands within the batch are executed successfully or not at all. This is particularly useful for complex migrations involving multiple inserts, updates, or schema changes in a single transaction-like operation.
When using batch operations, consider using a `LoggedBatch` for atomicity or an `UnloggedBatch` for improved performance where atomicity isn't required. This approach helps maintain data consistency in complex migrations.
> Note: The following example assumes that users have already created the `KEYSPACE` in Cassandra. A `KEYSPACE` in Cassandra is a container for tables that defines data replication settings across the cluster.
```go
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
const (
createTableCassandra = `CREATE TABLE IF NOT EXISTS employee (
id int PRIMARY KEY,
name text,
gender text,
number text
);`
addCassandraRecords = `BEGIN BATCH
INSERT INTO employee (id, name, gender, number) VALUES (1, 'Alison', 'F', '1234567980');
INSERT INTO employee (id, name, gender, number) VALUES (2, 'Alice', 'F', '9876543210');
APPLY BATCH;
`
employeeDataCassandra = `INSERT INTO employee (id, name, gender, number) VALUES (?, ?, ?, ?);`
)
func createTableEmployeeCassandra() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
// Execute the create table statement
if err := d.Cassandra.Exec(createTableCassandra); err != nil {
return err
}
// Batch processes can also be executed in Exec as follows:
if err := d.Cassandra.Exec(addCassandraRecords); err != nil {
return err
}
// Create a new batch operation
batchName := "employeeBatch"
if err := d.Cassandra.NewBatch(batchName, 0); err != nil { // 0 for LoggedBatch
return err
}
// Add multiple queries to the batch
if err := d.Cassandra.BatchQuery(batchName, employeeDataCassandra, 1, "Harry", "M", "1234567980"); err != nil {
return err
}
if err := d.Cassandra.BatchQuery(batchName, employeeDataCassandra, 2, "John", "M", "9876543210"); err != nil {
return err
}
// Execute the batch operation
if err := d.Cassandra.ExecuteBatch(batchName); err != nil {
return err
}
return nil
},
}
}
```
## Migrations in Elasticsearch
GoFr supports Elasticsearch document migrations, including **single-document** and **bulk operations**.
### Single Document Migration
```go
func addSingleProduct() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
product := map[string]any{
"title": "Laptop",
"price": 999.99,
"category": "electronics",
}
return d.Elasticsearch.IndexDocument( context.Background(), "products", "1", product, ) }, }
}
```
### Bulk Operation Migration
```go
func bulkProducts() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
operations := []map[string]any{
{"index": map[string]any{"_index": "products", "_id": "1"}},
{"title": "Phone", "price": 699.99, "category": "electronics"},
{"index": map[string]any{"_index": "products", "_id": "2"}},
{"title": "Mug", "price": 12.99, "category": "kitchen"},
}
_, err := d.Elasticsearch.Bulk(context.Background(), operations) return err },}
}
```
## PubSub in Migrations
GoFr provides support for interacting with PubSub systems during migrations. This is particularly useful for setting up your infrastructure (e.g., creating or deleting topics) before your application logic starts using them.
GoFr does not store migration records in PubSub. Migration version tracking is handled exclusively by primary data stores (SQL or Redis) that support atomicity and locking. This is because many PubSub backends (like Redis Streams or Kafka) persist messages even after they are consumed. If the PubSub bus were used as a source of truth for migration versions, stale data from previous runs or other environments could interfere with the migration process, causing legitimate migrations to be skipped.
### Configuration Requirements
When using PubSub in migrations, keep in mind the configuration requirements of your backend:
- **Publishing**: Generally only requires connection details (brokers, host, etc.).
- **Subscribing**: Requires a **Consumer Group ID** (e.g., `CONSUMER_ID` for Kafka or `REDIS_STREAMS_CONSUMER_GROUP` for Redis Streams). An empty or missing value will cause an error when attempting to subscribe, whereas publishing will still function correctly.
### Usage Examples
You can use the `PubSub` data source inside your `UP` migrations just like any other driver.
**Creating a topic during migration:**
```go
func setupMessagingFeature() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
// Create a topic required for the new feature
if err := d.PubSub.CreateTopic(context.Background(), "user-registrations"); err != nil {
return err
}
return nil
},
}
}
```
**Publishing a message to an existing topic (topic not created by migration):**
```go
func seedInitialEvents() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
// Publish a seed message to a pre-existing topic
return d.PubSub.Publish(context.Background(), "order-events", []byte(`{"event":"system-initialized"}`))
},
}
}
```
> ##### Check out the example to add and run migrations in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-migrations/main.go)
================================================
FILE: docs/advanced-guide/handling-file/page.md
================================================
# Handling File
GoFr simplifies the complexity of working with different file stores by offering a uniform API. This allows developers to interact with different storage systems using the same set of methods, without needing to understand the underlying implementation details of each file store.
## USAGE
By default, local file-store is initialized and user can access it from the context.
GoFr also supports FTP/SFTP file-store. Developers can also connect and use their cloud storage bucket as a file-store. Following cloud storage options are currently supported:
- **AWS S3**
- **Google Cloud Storage (GCS)**
- **Azure File Storage**
The file-store can be initialized as follows:
### FTP file-store
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file/ftp"
)
func main() {
app := gofr.New()
app.AddFileStore(ftp.New(&ftp.Config{
Host: "127.0.0.1",
User: "user",
Password: "password",
Port: 21,
RemoteDir: "/ftp/user",
}))
app.Run()
}
```
### SFTP file-store
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file/sftp"
)
func main() {
app := gofr.New()
app.AddFileStore(sftp.New(&sftp.Config{
Host: "127.0.0.1",
User: "user",
Password: "password",
Port: 22,
}))
app.Run()
}
```
### AWS S3 Bucket as File-Store
To run S3 File-Store locally we can use localstack,
`docker run --rm -it -p 4566:4566 -p 4510-4559:4510-4559 localstack/localstack`
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file/s3"
)
func main() {
app := gofr.New()
// Note that currently we do not handle connections through session token.
// BaseEndpoint is not necessary while connecting to AWS as it automatically resolves it on the basis of region.
// However, in case we are using any other AWS compatible service, such like running or testing locally, then this needs to be set.
// Note that locally, AccessKeyID & SecretAccessKey is not checked if we use localstack.
app.AddFileStore(s3.New(&s3.Config{
EndPoint: "http://localhost:4566",
BucketName: "gofr-bucket-2",
Region: "us-east-1",
AccessKeyID: app.Config.Get("AWS_ACCESS_KEY_ID"),
SecretAccessKey: app.Config.Get("AWS_SECRET_ACCESS_KEY"),
}))
app.Run()
}
```
> Note: The current implementation supports handling only one bucket at a time,
> as shown in the example with `gofr-bucket-2`. Bucket switching mid-operation is not supported.
### Google Cloud Storage (GCS) Bucket as File-Store
**Local Setup with fake-gcs-server:**
1. Start fake-gcs-server with HTTP:
```bash
docker run -d --name fake-gcs-server -p 4443:4443 \
fsouza/fake-gcs-server -scheme http -port 4443
```
2. Create a bucket:
```bash
curl -X POST http://localhost:4443/storage/v1/b?project=my-project-id \
-H "Content-Type: application/json" \
-d '{"name":"my-bucket"}'
```
3. Set environment variable in your `configs/.env` file:
```bash
STORAGE_EMULATOR_HOST=localhost:4443
```
4. Connect to GCS in your application:
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file/gcs"
)
func main() {
app := gofr.New()
// Local setup with fake-gcs-server (uses STORAGE_EMULATOR_HOST)
app.AddFileStore(gcs.New(&gcs.Config{
BucketName: "my-bucket",
ProjectID: "my-project-id",
}))
app.Run()
app.Run()
}
```
**Production Setup:**
For production, authenticate using one of these methods:
```go
// Option 1: Using GOOGLE_APPLICATION_CREDENTIALS environment variable
// Set: export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
app.AddFileStore(gcs.New(&gcs.Config{
BucketName: "my-bucket",
ProjectID: "my-project-id",
}))
// Option 2: Using CredentialsJSON directly
credJSON, _ := os.ReadFile("gcs-credentials.json")
app.AddFileStore(gcs.New(&gcs.Config{
BucketName: "my-bucket",
CredentialsJSON: string(credJSON),
ProjectID: "my-project-id",
}))
```
> **Note:**
> - When `STORAGE_EMULATOR_HOST` is set, the client automatically connects to the local emulator without authentication.
> - For production, use either `GOOGLE_APPLICATION_CREDENTIALS` environment variable or `CredentialsJSON` config field
> - Currently supports one bucket per file-store instance
### Azure File Storage as File-Store
Azure File Storage provides fully managed file shares in the cloud. To use Azure File Storage with GoFr:
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file/azure"
)
func main() {
app := gofr.New()
// Create Azure File Storage filesystem
fs, err := azure.New(&azure.Config{
AccountName: "mystorageaccount",
AccountKey: "myaccountkey",
ShareName: "myshare",
// Endpoint is optional, defaults to https://{AccountName}.file.core.windows.net
// Endpoint: "https://custom-endpoint.file.core.windows.net",
})
if err != nil {
app.Logger().Fatalf("Failed to initialize Azure File Storage: %v", err)
}
app.AddFileStore(fs)
app.Run()
}
```
> **Note:**
> - Azure File Storage uses file shares (similar to S3 buckets or GCS buckets)
> - Authentication requires both `AccountName` and `AccountKey`
> - The `Endpoint` field is optional and defaults to `https://{AccountName}.file.core.windows.net`
> - Currently supports one file share per file-store instance
> - The implementation automatically retries connection if the initial connection fails
> - **Automatic parent directory creation**: When creating files in nested paths (e.g., `dir1/subdir/file.txt`), parent directories are automatically created, matching local filesystem behavior
> - **Content type detection**: Content types are automatically detected based on file extensions (e.g., `.json` → `application/json`, `.txt` → `text/plain`)
## Cloud-Specific Operations
Beyond the standard filesystem interface, some cloud storage providers support richer capabilities — setting file metadata on upload and generating secure, time-limited download URLs. These are available through the `CloudFileSystem` interface.
> **Note:** These operations are currently supported only for **Google Cloud Storage (GCS)**. Other cloud providers may gain support in future releases.
### Checking Cloud Support
Use `file.AsCloud()` to safely check whether the configured file store supports cloud-specific operations. This avoids a raw type assertion and returns a typed interface:
```go
import "gofr.dev/pkg/gofr/datasource/file"
cfs, ok := file.AsCloud(c.File)
if !ok {
return nil, file.ErrSignedURLsNotSupported
}
```
### Uploading a File with Metadata
`CreateWithOptions` works like `Create` but lets you set a `Content-Type`, `Content-Disposition`, and arbitrary key-value metadata on the object at upload time:
```go
f, err := cfs.CreateWithOptions(c, "reports/q1.csv", &file.FileOptions{
ContentType: "text/csv",
ContentDisposition: `attachment; filename="q1.csv"`,
Metadata: map[string]string{
"uploaded-by": "invoice-service",
"report-quarter": "Q1-2026",
},
})
if err != nil {
return nil, err
}
defer f.Close()
_, err = f.Write(csvData)
```
Setting `ContentDisposition` ensures browsers download the file as an attachment rather than attempting to render it inline. Custom `Metadata` fields are stored on the GCS object and visible in the GCS console and `gsutil` output.
### Generating a Signed URL
`GenerateSignedURL` creates a time-limited, pre-authenticated URL that allows anyone with the link to download the file — no GCS credentials required on the client side:
```go
url, err := cfs.GenerateSignedURL(c, "reports/q1.csv", 15*time.Minute, nil)
if err != nil {
return nil, err
}
return url, nil
```
Pass `FileOptions` as the last argument to override the `Content-Disposition` header that the signed URL serves — useful when the object was uploaded without a disposition header but you want the browser to treat it as a download:
```go
url, err := cfs.GenerateSignedURL(c, "reports/q1.csv", 1*time.Hour, &file.FileOptions{
ContentDisposition: `attachment; filename="report.csv"`,
})
```
> **Note:**
> - Signed URLs require the GCS service account to have the `iam.serviceAccounts.signBlob` IAM permission.
> - The URL is pre-authenticated — anyone who has it can download the file until it expires.
> - Expiry is measured from the moment `GenerateSignedURL` is called.
> - `file.AsCloud` returns `(nil, false)` for local, FTP, and SFTP file stores — always check the `ok` result.
### Creating Directory
To create a single directory
```go
err := ctx.File.Mkdir("my_dir",os.ModePerm)
```
To create subdirectories as well
```go
err := ctx.File.MkdirAll("my_dir/sub_dir", os.ModePerm)
```
### Get current Directory
```go
currentDir, err := ctx.File.Getwd()
```
### Change current Directory
To switch to parent directory
```go
currentDir, err := ctx.File.Chdir("..")
```
To switch to another directory in same parent directory
```go
currentDir, err := ctx.File.Chdir("../my_dir2")
```
To switch to a subfolder of the current directory
```go
currentDir, err := ctx.File.Chdir("sub_dir")
```
> Note: This method attempts to change the directory, but S3's flat structure and fixed bucket
> make this operation inapplicable. Similarly, GCS uses a flat structure where directories are simulated through object prefixes.
> Azure File Storage supports directory operations natively, so `Chdir` works as expected.
### Read a Directory
The ReadDir function reads the specified directory and returns a sorted list of its entries as FileInfo objects. Each FileInfo object provides access to its associated methods, eliminating the need for additional stat calls.
If an error occurs during the read operation, ReadDir returns the successfully read entries up to the point of the error along with the error itself. Passing "." as the directory argument returns the entries for the current directory.
```go
entries, err := ctx.File.ReadDir("../testdir")
for _, entry := range entries {
entryType := "File"
if entry.IsDir() {
entryType = "Dir"
}
fmt.Printf("%v: %v Size: %v Last Modified Time : %v\n", entryType, entry.Name(), entry.Size(), entry.ModTime())
}
```
> Note: In S3 and GCS, directories are represented as prefixes of file keys/object names. This method retrieves file
> entries only from the immediate level within the specified directory. Azure File Storage supports native directory
> structures, so `ReadDir` works with actual directories.
### Creating and Save a File with Content
```go
file, _ := ctx.File.Create("my_file.text")
_, _ = file.Write([]byte("Hello World!"))
// Closes and saves the file.
file.Close()
```
> **Note for Azure File Storage:**
> - Files can be created in nested directories (e.g., `dir1/subdir/file.txt`). Parent directories are automatically created if they don't exist
> - Content types are automatically detected based on file extensions (e.g., `.json`, `.txt`, `.csv`, `.xml`, `.html`, `.pdf`)
> - This behavior matches local filesystem operations for consistency
### Reading file as CSV/JSON/TEXT
GoFr support reading CSV/JSON/TEXT files line by line.
```go
reader, err := file.ReadAll()
for reader.Next() {
var b string
// For reading CSV/TEXT files user need to pass pointer to string to SCAN.
// In case of JSON user should pass structs with JSON tags as defined in encoding/json.
err = reader.Scan(&b)
fmt.Println(b)
}
```
### Opening and Reading Content from a File
To open a file with default settings, use the `Open` command, which provides read and seek permissions only. For write permissions, use `OpenFile` with the appropriate file modes.
> Note: In FTP, file permissions are not differentiated; both `Open` and `OpenFile` allow all file operations regardless of specified permissions.
```go
csvFile, _ := ctx.File.Open("my_file.csv")
b := make([]byte, 200)
// Read reads up to len(b) bytes into b.
_, _ = file.Read(b)
csvFile.Close()
csvFile, err = ctx.File.OpenFile("my_file.csv", os.O_RDWR, os.ModePerm)
// WriteAt writes the buffer content at the specified offset.
_, err = csvFile.WriteAt([]byte("test content"), 4)
if err != nil {
return nil, err
}
```
### Getting Information of the file/directory
Stat retrieves details of a file or directory, including its name, size, last modified time, and type (such as whether it is a file or folder)
```go
file, _ := ctx.File.Stat("my_file.text")
entryType := "File"
if entry.IsDir() {
entryType = "Dir"
}
fmt.Printf("%v: %v Size: %v Last Modified Time : %v\n", entryType, entry.Name(), entry.Size(), entry.ModTime())
```
> Note: In S3 and GCS:
>
> - Names without a file extension are treated as directories by default.
> - Names starting with "0" are interpreted as binary files, with the "0" prefix removed (S3 specific behavior).
>
> For directories, the method calculates the total size of all contained objects and returns the most recent modification time. For files, it directly returns the file's size and last modified time.
>
> Azure File Storage supports native file and directory structures, so `Stat` returns accurate metadata for both files and directories.
### Rename/Move a File
To rename or move a file, provide source and destination fields.
In case of renaming a file provide current name as source, new_name in destination.
To move file from one location to another provide current location as source and new location as destination.
```go
err := ctx.File.Rename("old_name.text", "new_name.text")
```
### Deleting Files
`Remove` deletes a single file
> Note: Currently, the S3 package supports the deletion of unversioned files from general-purpose buckets only. Directory buckets and versioned files are not supported for deletion by this method. GCS supports deletion of both files and empty directories. Azure File Storage supports deletion of both files and empty directories.
```go
err := ctx.File.Remove("my_dir")
```
The `RemoveAll` command deletes all subdirectories as well. If you delete the current working directory, such as "../currentDir", the working directory will be reset to its parent directory.
> Note: In S3, RemoveAll only supports deleting directories and will return an error if a file path (as indicated by a file extension) is provided for S3.
> GCS and Azure File Storage handle both files and directories.
```go
err := ctx.File.RemoveAll("my_dir/my_text")
```
> GoFr supports relative paths, allowing locations to be referenced relative to the current working directory. However, since S3 and GCS use
> a flat file structure, all methods require a full path relative to the bucket. Azure File Storage supports native directory structures,
> so relative paths work as expected with directory navigation.
> Errors have been skipped in the example to focus on the core logic, it is recommended to handle all the errors.
================================================
FILE: docs/advanced-guide/http-communication/page.md
================================================
# Inter-Service HTTP Calls
GoFr promotes microservice architecture and to facilitate the same, it provides the support to initialize HTTP services
at application level using `AddHTTPService()` method.
Support for inter-service HTTP calls provide the following benefits:
1. Access to the methods from container - GET, PUT, POST, PATCH, DELETE.
2. Logs and traces for the request.
3. {% new-tab-link newtab=false title="Circuit breaking" href="/docs/advanced-guide/circuit-breaker" /%} for enhanced resilience and fault tolerance.
4. {% new-tab-link newtab=false title="Custom Health Check" href="/docs/advanced-guide/monitoring-service-health" /%} Endpoints
## Usage
### Registering a simple HTTP Service
GoFr allows registering a new HTTP service using the application method `AddHTTPService()`.
It takes in a service name and service address argument to register the dependent service at application level.
Registration of multiple dependent services is quite easier, which is a common use case in a microservice architecture.
> The services instances are maintained by the container.
Other provided options can be added additionally to coat the basic HTTP client with features like circuit-breaker and
custom health check and add to the functionality of the HTTP service.
The design choice for this was made such as many options as required can be added and are order agnostic,
i.e. the order of the options is not important.
> Service names are to be kept unique to one service.
```go
app.AddHTTPService(, )
```
#### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// Create a new application
app := gofr.New()
// register a payment service which is hosted at http://localhost:9000
app.AddHTTPService("payment", "http://localhost:9000")
app.GET("/customer", Customer)
// Run the application
app.Run()
}
```
### Accessing HTTP Service in handler
The HTTP service client is accessible anywhere from `gofr.Context` that gets passed on from the handler.
Using the `GetHTTPService` method with the service name that was given at the time of registering the service,
the client can be retrieved as shown below:
```go
svc := ctx.GetHTTPService()
```
#### Available Methods
The HTTP service client provides methods for making requests to downstream services:
- `Get(ctx, path, queryParams)`
- `Post(ctx, path, queryParams, body)`
- `Put(ctx, path, queryParams, body)`
- `Patch(ctx, path, queryParams, body)`
- `Delete(ctx, path, body)`
**For scenarios requiring custom header propagation (authentication, multi-tenancy, user identity propagation), use the `WithHeaders` variants:**
- `GetWithHeaders(ctx, path, queryParams, headers)`
- `PostWithHeaders(ctx, path, queryParams, body, headers)`
- `PutWithHeaders(ctx, path, queryParams, body, headers)`
- `PatchWithHeaders(ctx, path, queryParams, body, headers)`
- `DeleteWithHeaders(ctx, path, body, headers)`
```go
func Customer(ctx *gofr.Context) (any, error) {
// Get the payment service client
paymentSvc := ctx.GetHTTPService("payment")
// Use the Get method to call the GET /user endpoint of payments service
resp, err := paymentSvc.Get(ctx, "user", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return string(body), nil
}
// For microservice patterns involving authentication (ex: JWT Token Forwarding), use WithHeaders methods to forward custom headers.
func GatewayHandler(ctx *gofr.Context) (any, error) {
authInfo := ctx.GetAuthInfo()
claims := authInfo.GetClaims()
userID, _ := claims.GetSubject()
headers := map[string]string{
"X-User-ID": userID,
}
userSvc := ctx.GetHTTPService("user-service")
resp, err := userSvc.GetWithHeaders(ctx, "api/user/profile", nil, headers)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return string(body), nil
}
```
### Additional Configurational Options
GoFr provides its user with additional configurational options while registering HTTP service for communication. These are:
- **ConnectionPoolConfig** - This option allows the user to configure HTTP connection pool settings to optimize performance for high-frequency requests. The default Go HTTP client has `MaxIdleConnsPerHost: 2`, which is often insufficient for microservices making frequent requests to the same host. This configuration allows customizing:
- `MaxIdleConns`: Maximum idle connections across all hosts. If not explicitly set (0), a default of 100 will be used.
- `MaxIdleConnsPerHost`: Maximum idle connections per host (critical for performance). If set to 0, Go's DefaultMaxIdleConnsPerHost (2) will be used. Negative values will cause validation error.
- `IdleConnTimeout`: How long to keep idle connections alive. If not explicitly set (0), a default of 90 seconds will be used.
**Important**: `ConnectionPoolConfig` must be applied **first** when using multiple options, as it needs access to the underlying HTTP client transport.
- **APIKeyConfig** - This option allows the user to set the `API-Key` Based authentication as the default auth for downstream HTTP Service.
- **BasicAuthConfig** - This option allows the user to set basic auth (username and password) as the default auth for downstream HTTP Service.
**Important:** The password must be base64 encoded in your configuration/environment variables. GoFr will decode it internally before creating the Authorization header.
**Example:**
```bash
# Generate base64 encoded password
echo -n "your-password" | base64
# Output: eW91ci1wYXNzd29yZA==
```
- **OAuthConfig** - This option allows the user to add `OAuth` as default auth for downstream HTTP Service.
- **CircuitBreakerConfig** - This option allows the user to configure the GoFr Circuit Breaker's `threshold` and `interval` for the failing downstream HTTP Service calls. If the failing calls exceeds the threshold the circuit breaker will automatically be enabled.
- **DefaultHeaders** - This option allows the user to set some default headers that will be propagated to the downstream HTTP Service every time it is being called.
- **HealthConfig** - This option allows the user to add the `HealthEndpoint` along with `Timeout` to enable and perform the timely health checks for downstream HTTP Service.
- **RetryConfig** - This option allows the user to add the maximum number of retry count before returning error if any downstream HTTP Service fails. Retries are triggered for network errors and status codes **> 500** (e.g., 503 Service Unavailable). HTTP 500 is not retried.
- **RateLimiterConfig** - This option allows the user to configure rate limiting for downstream service calls using token bucket algorithm. It controls the request rate to prevent overwhelming dependent services and supports both in-memory and Redis-based implementations.
**Rate Limiter Store: Customization**
GoFr allows you to use a custom rate limiter store by implementing the RateLimiterStore interface. This enables integration with any backend (e.g., Redis, database, or custom logic)
**Interface:**
```go
type RateLimiterStore interface {
Allow(ctx context.Context, key string, config RateLimiterConfig) (allowed bool, retryAfter time.Duration, err error)
StartCleanup(ctx context.Context)
StopCleanup()
}
```
#### Usage:
```go
rc := redis.NewClient(a.Config, a.Logger(), a.Metrics())
a.AddHTTPService("cat-facts", "https://catfact.ninja",
// ConnectionPoolConfig must be applied FIRST
&service.ConnectionPoolConfig{
MaxIdleConns: 100, // Maximum idle connections across all hosts
MaxIdleConnsPerHost: 20, // Maximum idle connections per host (increased from default 2)
IdleConnTimeout: 90 * time.Second, // Keep connections alive for 90 seconds
},
// Other options can follow in any order
service.NewAPIKeyConfig("some-random-key"),
service.NewBasicAuthConfig("username", "password"),
&service.CircuitBreakerConfig{
Threshold: 4,
Interval: 1 * time.Second,
},
&service.DefaultHeaders{Headers: map[string]string{"key": "value"}},
&service.HealthConfig{
HealthEndpoint: "breeds",
},
service.NewOAuthConfig("clientID", "clientSecret",
"https://tokenurl.com", nil, nil, 0),
&service.RetryConfig{
MaxRetries: 5,
},
&service.RateLimiterConfig{
Requests: 5,
Window: time.Minute,
Burst: 10,
Store: service.NewRedisRateLimiterStore(rc), // Skip this field to use in-memory store
},
)
```
**Best Practices:**
- For distributed systems: It is strongly recommended to use Redis-based store (`NewRedisRateLimiterStore`) to ensure consistent rate limiting across multiple instances of your application.
- For single-instance applications: The default in-memory store (`NewLocalRateLimiterStore`) is sufficient and provides better performance.
- Rate configuration: Set Burst higher than Requests to allow short traffic bursts while maintaining average rate limits.
## Metrics
GoFr publishes the following metrics for HTTP service communication:
- `app_http_retry_count`: Total number of retry events. (labels: `service`)
- `app_http_circuit_breaker_state`: Current state of the circuit breaker (0 for Closed, 1 for Open). (labels: `service`)
- `app_http_service_response`: Response time of HTTP service requests in seconds (histogram). (labels: `service`, `path`, `method`, `status`)
================================================
FILE: docs/advanced-guide/injecting-databases-drivers/page.md
================================================
# Injecting Database Drivers
Keeping in mind the size of the framework in the final build, it felt counter-productive to keep all the database drivers within
the framework itself. Keeping only the most used MySQL and Redis within the framework, users can now inject databases
in the server that satisfies the base interface defined by GoFr. This helps in reducing the build size and in turn build time
as unnecessary database drivers are not being compiled and added to the build.
> We are planning to provide custom drivers for most common databases, and is in the pipeline for upcoming releases!
## Supported Databases
{% table %}
- Datasource
- Health-Check
- Logs
- Metrics
- Traces
- Version-Migrations
---
- MySQL
- ✅
- ✅
- ✅
- ✅
- ✅
---
- REDIS
- ✅
- ✅
- ✅
- ✅
- ✅
---
- PostgreSQL
- ✅
- ✅
- ✅
- ✅
- ✅
---
- ArangoDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- BadgerDB
- ✅
- ✅
- ✅
- ✅
-
---
- Cassandra
- ✅
- ✅
- ✅
- ✅
- ✅
---
- ClickHouse
-
- ✅
- ✅
- ✅
- ✅
---
- DGraph
- ✅
- ✅
- ✅
- ✅
-
---
- MongoDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- NATS KV
- ✅
- ✅
- ✅
- ✅
-
---
- OpenTSDB
- ✅
- ✅
-
- ✅
-
---
- ScyllaDB
- ✅
- ✅
- ✅
- ✅
-
---
- Solr
-
- ✅
- ✅
- ✅
-
---
- SQLite
- ✅
- ✅
- ✅
- ✅
- ✅
---
- SurrealDB
- ✅
- ✅
-
- ✅
-
---
================================================
FILE: docs/advanced-guide/key-value-store/page.md
================================================
# Key Value Store
A key-value store is a type of NoSQL database that uses a simple data model: each item is stored as a pair consisting of a unique key and a value.
This simplicity offers high performance and scalability, making key-value stores ideal for applications requiring fast and efficient data retrieval and storage.
GoFr supports multiple key-value stores including BadgerDB, NATS-KV, and DynamoDB. Support for other key-value stores will be added in the future.
Keeping in mind the size of the application in the final build, it felt counter-productive to keep the drivers within
the framework itself. GoFr provide the following functionalities for its key-value store.
```go
type KVStore interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
}
```
## BadgerDB
GoFr supports injecting BadgerDB that supports the following interface. Any driver that implements the interface can be added
using `app.AddKVStore()` method, and user's can use BadgerDB across application with `gofr.Context`.
User's can easily inject a driver that supports this interface, this provides usability without
compromising the extensibility to use multiple databases.
Import the gofr's external driver for BadgerDB:
```go
go get gofr.dev/pkg/gofr/datasource/kv-store/badger
```
### Example
```go
package main
import (
"fmt"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/kv-store/badger"
)
type User struct {
ID string
Name string
Age string
}
func main() {
app := gofr.New()
app.AddKVStore(badger.New(badger.Configs{DirPath: "badger-example"}))
app.POST("/user", Post)
app.GET("/user", Get)
app.DELETE("/user", Delete)
app.Run()
}
func Post(ctx *gofr.Context) (any, error) {
err := ctx.KVStore.Set(ctx, "name", "gofr")
if err != nil {
return nil, err
}
return "Insertion to Key Value Store Successful", nil
}
func Get(ctx *gofr.Context) (any, error) {
value, err := ctx.KVStore.Get(ctx, "name")
if err != nil {
return nil, err
}
return value, nil
}
func Delete(ctx *gofr.Context) (any, error) {
err := ctx.KVStore.Delete(ctx, "name")
if err != nil {
return nil, err
}
return fmt.Sprintf("Deleted Successfully key %v from Key-Value Store", "name"), nil
}
```
## NATS-KV
GoFr supports injecting NATS-KV that supports the above KVStore interface. Any driver that implements the interface can be added
using `app.AddKVStore()` method, and user's can use NATS-KV across application with `gofr.Context`.
User's can easily inject a driver that supports this interface, this provides usability without
compromising the extensibility to use multiple databases.
Import the gofr's external driver for NATS-KV:
```go
go get gofr.dev/pkg/gofr/datasource/kv-store/nats
```
### Example
```go
package main
import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/kv-store/nats"
"gofr.dev/pkg/gofr/http"
)
type Person struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}
func main() {
app := gofr.New()
app.AddKVStore(nats.New(nats.Configs{
Server: "nats://localhost:4222",
Bucket: "persons",
}))
app.POST("/person", CreatePerson)
app.GET("/person/{id}", GetPerson)
app.PUT("/person/{id}", UpdatePerson)
app.DELETE("/person/{id}", DeletePerson)
app.Run()
}
func CreatePerson(ctx *gofr.Context) (any, error) {
var person Person
if err := ctx.Bind(&person); err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"body"}}
}
person.ID = uuid.New().String()
personData, err := json.Marshal(person)
if err != nil {
return nil, fmt.Errorf("failed to serialize person")
}
if err := ctx.KVStore.Set(ctx, person.ID, string(personData)); err != nil {
return nil, err
}
return person, nil
}
func GetPerson(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, http.ErrorInvalidParam{Params: []string{"id"}}
}
value, err := ctx.KVStore.Get(ctx, id)
if err != nil {
return nil, fmt.Errorf("person not found")
}
var person Person
if err := json.Unmarshal([]byte(value), &person); err != nil {
return nil, fmt.Errorf("failed to parse person data")
}
return person, nil
}
func UpdatePerson(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, http.ErrorInvalidParam{Params: []string{"id"}}
}
var person Person
if err := ctx.Bind(&person); err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"body"}}
}
person.ID = id
personData, err := json.Marshal(person)
if err != nil {
return nil, fmt.Errorf("failed to serialize person")
}
if err := ctx.KVStore.Set(ctx, id, string(personData)); err != nil {
return nil, err
}
return person, nil
}
func DeletePerson(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, http.ErrorInvalidParam{Params: []string{"id"}}
}
if err := ctx.KVStore.Delete(ctx, id); err != nil {
return nil, fmt.Errorf("person not found")
}
return map[string]string{"message": "Person deleted successfully"}, nil
}
```
## DynamoDB
GoFr supports injecting DynamoDB as a key-value store that implements the standard KVStore interface. Any driver that implements the interface can be added using `app.AddKVStore()` method, and users can use DynamoDB across application with `gofr.Context`.
DynamoDB is a fully managed NoSQL database service that provides fast and predictable performance with seamless scalability. It's ideal for applications that need consistent, single-digit millisecond latency at any scale.
Import the gofr's external driver for DynamoDB:
```shell
go get gofr.dev/pkg/gofr/datasource/kv-store/dynamodb@latest
```
### Configuration
```go
type Configs struct {
Table string // DynamoDB table name
Region string // AWS region (e.g., "us-east-1")
Endpoint string // Leave empty for real AWS; set for local DynamoDB
PartitionKeyName string // Default is "pk" if not specified
}
```
### Local Development Setup
For local development, you can use DynamoDB Local with Docker:
```bash
# Start DynamoDB Local
docker run --name dynamodb-local -d -p 8000:8000 amazon/dynamodb-local
# Create a table
aws dynamodb create-table \
--table-name gofr-kv-store \
--attribute-definitions AttributeName=pk,AttributeType=S \
--key-schema AttributeName=pk,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--endpoint-url http://localhost:8000 \
--region us-east-1
```
### JSON Helper Functions
The DynamoDB package provides helper functions for JSON serialization/deserialization that work with the standard KVStore interface:
```go
// ToJSON converts any struct to JSON string
func ToJSON(value any) (string, error)
// FromJSON converts JSON string to struct
func FromJSON(jsonData string, dest any) error
```
### Example
```go
package main
import (
"fmt"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/kv-store/dynamodb"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
}
func main() {
app := gofr.New()
// Create DynamoDB client with configuration
db := dynamodb.New(dynamodb.Configs{
Table: "gofr-kv-store",
Region: "us-east-1",
Endpoint: "http://localhost:8000", // For local DynamoDB
PartitionKeyName: "pk",
})
// Connect to DynamoDB
db.Connect()
// Inject the DynamoDB into gofr
app.AddKVStore(db)
app.POST("/user", CreateUser)
app.GET("/user/{id}", GetUser)
app.PUT("/user/{id}", UpdateUser)
app.DELETE("/user/{id}", DeleteUser)
app.Run()
}
func CreateUser(ctx *gofr.Context) (any, error) {
var user User
if err := ctx.Bind(&user); err != nil {
return nil, err
}
user.ID = fmt.Sprintf("user_%d", time.Now().UnixNano())
user.CreatedAt = time.Now()
// Convert struct to JSON string using helper function
userData, err := dynamodb.ToJSON(user)
if err != nil {
return nil, fmt.Errorf("failed to serialize user: %w", err)
}
// Store using standard KVStore interface
if err := ctx.KVStore.Set(ctx, user.ID, userData); err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return user, nil
}
func GetUser(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, fmt.Errorf("user ID is required")
}
// Get JSON string from KVStore
userData, err := ctx.KVStore.Get(ctx, id)
if err != nil {
return nil, fmt.Errorf("user not found: %w", err)
}
// Convert JSON string to struct using helper function
var user User
if err := dynamodb.FromJSON(userData, &user); err != nil {
return nil, fmt.Errorf("failed to parse user data: %w", err)
}
return user, nil
}
func UpdateUser(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, fmt.Errorf("user ID is required")
}
var user User
if err := ctx.Bind(&user); err != nil {
return nil, err
}
user.ID = id
// Convert struct to JSON string using helper function
userData, err := dynamodb.ToJSON(user)
if err != nil {
return nil, fmt.Errorf("failed to serialize user: %w", err)
}
// Update in DynamoDB using standard KVStore interface
if err := ctx.KVStore.Set(ctx, id, userData); err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
return user, nil
}
func DeleteUser(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
if id == "" {
return nil, fmt.Errorf("user ID is required")
}
// Delete from DynamoDB using standard KVStore interface
if err := ctx.KVStore.Delete(ctx, id); err != nil {
return nil, fmt.Errorf("failed to delete user: %w", err)
}
return map[string]string{"message": "User deleted successfully"}, nil
}
```
### Production Configuration
For production use, remove the `Endpoint` field to connect to real AWS DynamoDB:
```go
db := dynamodb.New(dynamodb.Configs{
Table: "gofr-kv-store",
Region: "us-east-1",
// Endpoint: "", // Remove this for production
PartitionKeyName: "pk",
})
```
### AWS Credentials
For production, ensure your AWS credentials are configured through:
- AWS IAM roles (recommended for EC2/ECS/Lambda)
- AWS credentials file (`~/.aws/credentials`)
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
================================================
FILE: docs/advanced-guide/middlewares/page.md
================================================
# Middleware in GoFr
Middleware allows you intercepting and manipulating HTTP requests and responses flowing through your application's
router. Middlewares can perform tasks such as authentication, authorization, caching etc. before
or after the request reaches your application's handler.
## CORS Middleware in GoFr
GoFr includes built-in CORS (Cross-Origin Resource Sharing) middleware to handle CORS-related headers.
This middleware allows you to control access to your API from different origins. It automatically adds the necessary
headers to responses, allowing or restricting cross-origin requests. User can also override the default response headers
sent by GoFr by providing the suitable CORS configs.
The CORS middleware provides the following overridable configs:
- `ACCESS_CONTROL_ALLOW_ORIGIN`: Set the allowed origin(s) for cross-origin requests. By default, it allows all origins (*).
- `ACCESS_CONTROL_ALLOW_HEADERS`: Define the allowed request headers (e.g., Authorization, Content-Type).
- `ACCESS_CONTROL_ALLOW_CREDENTIALS`: Set to true to allow credentials (cookies, HTTP authentication) in requests.
- `ACCESS_CONTROL_EXPOSE_HEADERS`: Specify additional headers exposed to the client.
- `ACCESS_CONTROL_MAX_AGE`: Set the maximum time (in seconds) for preflight request caching.
> Note: GoFr automatically interprets the registered route methods and based on that sets the value of `ACCESS_CONTROL_ALLOW_METHODS`
## Adding Custom Middleware in GoFr
By adding custom middleware to your GoFr application, user can easily extend its functionality and implement
cross-cutting concerns in a modular and reusable way.
User can use the `UseMiddleware` or `UseMiddlewareWithContainer` method on your GoFr application instance to register your custom middleware.
### Using UseMiddleware method for Custom Middleware
The UseMiddleware method is ideal for simple middleware that doesn't need direct access to the application's container.
#### Example:
```go
import (
"net/http"
gofrHTTP "gofr.dev/pkg/gofr/http"
)
// Define your custom middleware function
func customMiddleware() gofrHTTP.Middleware {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Your custom logic here
// For example, logging, authentication, etc.
// Call the next handler in the chain
inner.ServeHTTP(w, r)
})
}
}
func main() {
// Create a new instance of your GoFr application
app := gofr.New()
// Add your custom middleware to the application
app.UseMiddleware(customMiddleware())
// Define your application routes and handlers
// ...
// Run your GoFr application
app.Run()
}
```
## Rate Limiter Middleware in GoFr
GoFr provides a built-in rate limiter middleware to protect your API from abuse and ensure fair resource distribution.
It uses a token bucket algorithm for smooth rate limiting with configurable burst capacity.
### Features
- **Token Bucket Algorithm**: Allows smooth rate limiting with configurable burst capacity
- **Per-IP Rate Limiting**: Each client IP gets its own rate limit (configurable)
- **Health Check Exemption**: `/.well-known/alive` and `/.well-known/health` endpoints are automatically exempt
- **Prometheus Metrics**: Track rate limit violations via `app_http_rate_limit_exceeded_total` counter
- **429 Status Code**: Returns standard HTTP 429 (Too Many Requests) when limit is exceeded
### Configuration
```go
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http/middleware"
)
func main() {
app := gofr.New()
// Configure rate limiter
rateLimiterConfig := middleware.RateLimiterConfig{
RequestsPerSecond: 5, // Average requests per second
Burst: 10, // Maximum burst size
PerIP: true, // Enable per-IP limiting
}
// Add rate limiter middleware
app.UseMiddleware(middleware.RateLimiter(rateLimiterConfig, app.Metrics()))
app.GET("/api/resource", handler)
app.Run()
}
```
### Parameters
- `RequestsPerSecond`: Average number of requests allowed per second
- `Burst`: Maximum number of requests that can be made in a burst (allows temporary spikes)
- `PerIP`: Set to `true` for per-IP limiting (recommended) or `false` for global rate limit across all clients
- `TrustedProxies`: *(Optional)* Set to `true` to trust `X-Forwarded-For` and `X-Real-IP` headers for IP extraction. Only enable when behind a trusted reverse proxy.
> **Security Warning**: Only set `TrustedProxies: true` if your application is behind a trusted reverse proxy (nginx, ALB, etc.).
> Without a trusted proxy, clients can spoof headers to bypass rate limits.
================================================
FILE: docs/advanced-guide/monitoring-service-health/page.md
================================================
# Monitoring Service Health
Health check in microservices refers to a mechanism or process implemented within each service to assess its operational status
and readiness to handle requests. It involves regularly querying the service to determine if it is functioning correctly,
typically by evaluating its responsiveness and ability to perform essential tasks. Health checks play a critical role in ensuring service availability,
detecting failures, preventing cascading issues, and facilitating effective traffic routing in distributed systems.
## GoFr by default registers two endpoints which are:
### 1. Aliveness - /.well-known/alive
It is an endpoint which returns the following response with a 200 status code, when the service is UP.
```json
{
"data": {
"status": "UP"
}
}
```
It is also used when state of {% new-tab-link newtab=false title="circuit breaker" href="/docs/advanced-guide/circuit-breaker" /%} is open.
To override this endpoint, pass the following option while registering HTTP Service:
```go
&service.HealthConfig{
HealthEndpoint: "breeds",
}
```
### 2. Health-Check - /.well-known/health
It is an endpoint which returns whether the service is UP or DOWN along with stats, host, status about the dependent datasources and services.
Sample response of how it appears when all the services, and connected data sources are UP:
```json
{
"data": {
"anotherService": {
"status": "UP",
"details": {
"host": "localhost:9000"
}
},
"redis": {
"status": "UP",
"details": {
"host": "localhost:2002",
"stats": {
"active_defrag_hits": "0",
"active_defrag_key_hits": "0",
"active_defrag_key_misses": "0",
"active_defrag_misses": "0",
"current_active_defrag_time": "0",
"current_eviction_exceeded_time": "0",
"dump_payload_sanitizations": "0",
"evicted_clients": "0",
"evicted_keys": "0",
"expire_cycle_cpu_milliseconds": "1",
"expired_keys": "0",
"expired_stale_perc": "0.00",
"expired_time_cap_reached_count": "0",
"instantaneous_input_kbps": "0.00",
"instantaneous_input_repl_kbps": "0.00",
"instantaneous_ops_per_sec": "0",
"instantaneous_output_kbps": "0.00",
"instantaneous_output_repl_kbps": "0.00",
"io_threaded_reads_processed": "0",
"io_threaded_writes_processed": "0",
"keyspace_hits": "0",
"keyspace_misses": "0",
"latest_fork_usec": "0",
"migrate_cached_sockets": "0",
"pubsub_channels": "0",
"pubsub_patterns": "0",
"pubsubshard_channels": "0",
"rejected_connections": "0",
"reply_buffer_expands": "0",
"reply_buffer_shrinks": "1",
"slave_expires_tracked_keys": "0",
"sync_full": "0",
"sync_partial_err": "0",
"sync_partial_ok": "0",
"total_active_defrag_time": "0",
"total_commands_processed": "2",
"total_connections_received": "1",
"total_error_replies": "2",
"total_eviction_exceeded_time": "0",
"total_forks": "0",
"total_net_input_bytes": "183",
"total_net_output_bytes": "257",
"total_net_repl_input_bytes": "0",
"total_net_repl_output_bytes": "0",
"total_reads_processed": "5",
"total_writes_processed": "4",
"tracking_total_items": "0",
"tracking_total_keys": "0",
"tracking_total_prefixes": "0",
"unexpected_error_replies": "0"
}
}
},
"sql": {
"status": "UP",
"details": {
"host": "localhost:2001/test",
"stats": {
"maxOpenConnections": 0,
"openConnections": 1,
"inUse": 0,
"idle": 1,
"waitCount": 0,
"waitDuration": 0,
"maxIdleClosed": 0,
"maxIdleTimeClosed": 0,
"maxLifetimeClosed": 0
}
}
}
}
}
```
================================================
FILE: docs/advanced-guide/overriding-default/page.md
================================================
# Overriding Default
GoFr allows overriding default behavior of its features.
## Raw response format
GoFr by default wraps a handler's return value and assigns it to the `data` field in a response.
### Example
```go
package main
import "gofr.dev/pkg/gofr"
type user struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
app := gofr.New()
app.GET("/users", func(ctx *gofr.Context) (any, error) {
users := []user{{ID: 1, Name: "Daria"}, {ID: 2, Name: "Ihor"}}
return users, nil
})
app.Run()
}
```
Response example:
```json
{
"data": [
{
"id": 1,
"name": "Daria"
},
{
"id": 2,
"name": "Ihor"
}
]
}
```
If you want to have a raw response structure - wrap it in `response.Raw`:
```go
app.GET("/users", func(ctx *gofr.Context) (any, error) {
users := []user{{ID: 1, Name: "Daria"}, {ID: 2, Name: "Ihor"}}
return response.Raw{Data: users}, nil
})
```
Response example:
```json
[
{
"id": 1,
"name": "Daria"
},
{
"id": 2,
"name": "Ihor"
}
]
```
### XML responses
If you need to respond with XML without JSON encoding, return `response.XML`. It bypasses JSON encoding just like `response.File` or `response.Template` and writes the bytes directly to the client. The `ContentType` defaults to `application/xml` but can be overridden.
```go
app.GET("/legacy/xml", func(ctx *gofr.Context) (any, error) {
payload := []byte(`Hello `)
return response.XML{Content: payload}, nil
})
```
```xml
Hello
```
## Rendering Templates
GoFr makes it easy to render HTML and HTMX templates directly from your handlers using the response.Template type.
By convention, all template files—whether HTML or HTMX—should be placed inside a templates directory located at the root of your project.
### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http/response"
)
func main() {
app := gofr.New()
app.GET("/list", listHandler)
app.AddStaticFiles("/", "./static")
app.Run()
}
type Todo struct {
Title string
Done bool
}
type TodoPageData struct {
PageTitle string
Todos []Todo
}
func listHandler(ctx *gofr.Context) (any, error) {
// Get data from somewhere
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{Title: "Expand on Gofr documentation ", Done: false},
{Title: "Add more examples", Done: true},
{Title: "Write some articles", Done: false},
},
}
return response.Template{Data: data, Name: "todo.html"}, nil
}
```
## HTTP Redirects
GoFr allows redirecting HTTP requests to other URLs using the `response.Redirect` type.
### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http/response"
)
func main() {
app := gofr.New()
app.GET("/old-page", func(ctx *gofr.Context) (any, error) {
// Redirect to a new URL
return response.Redirect{URL: "https://example.com/new-page"}, nil
})
app.Run()
}
```
In GoFr, the following HTTP methods can be redirected, along with their corresponding status codes:
- **GET (302 Found)**: It is safe to redirect because the request remains a GET after the redirect.
- **POST (303 See Other)**: The browser converts the POST request to a GET on redirect.
- **PUT (303 See Other)**: The browser converts the PUT request to a GET on redirect.
- **PATCH (303 See Other)**: The browser converts the PATCH request to a GET on redirect.
- **DELETE (302 Found)**: This is a temporary redirect, but method handling is ambiguous, as most browsers historically convert the DELETE request into a GET.
## Favicon.ico
By default, GoFr loads its own `favicon.ico` present in root directory for an application. To override `favicon.ico` user
can place its custom icon in the **static** directory of its application.
> [!NOTE]
> The custom favicon should also be named as `favicon.ico` in the static directory of application.
================================================
FILE: docs/advanced-guide/publishing-custom-metrics/page.md
================================================
# Publishing Custom Metrics
GoFr publishes some {% new-tab-link newtab=false title="default metrics" href="/docs/quick-start/observability" /%}.
GoFr can handle multiple different metrics concurrently, each uniquely identified by its name during initialization.
It supports the following {% new-tab-link title="metrics" href="https://opentelemetry.io/docs/specs/otel/metrics/" /%} types in Prometheus format:
1. `Counter`
2. `UpDownCounter`
3. `Histogram`
4. `Gauge`
If any custom metric is required, it can be created by using custom metrics as shown below:
## Usage
## 1. Counter Metrics
Counter is a {% new-tab-link title="synchronous Instrument" href="https://opentelemetry.io/docs/specs/otel/metrics/api/#synchronous-instrument-api" /%} which supports non-negative increments.
### Usage
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// initialize gofr object
app := gofr.New()
app.Metrics().NewCounter("transaction_success", "used to track the count of successful transactions")
app.POST("/transaction", func(ctx *gofr.Context) (any, error) {
ctx.Metrics().IncrementCounter(ctx, "transaction_success")
return "Transaction Successful", nil
})
app.Run()
}
```
## 2. UpDown Counter Metrics
`UpDownCounter` is a {% new-tab-link title="synchronous Instrument" href="https://opentelemetry.io/docs/specs/otel/metrics/api/#synchronous-instrument-api" /%} which supports increments and decrements.
Note: If the value is monotonically increasing, use Counter instead.
### Usage
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// initialize gofr object
app := gofr.New()
app.Metrics().NewUpDownCounter("total_credit_day_sale", "used to track the total credit sales in a day")
app.POST("/sale", func(ctx *gofr.Context) (any, error) {
ctx.Metrics().DeltaUpDownCounter(ctx, "total_credit_day_sale", 1000)
return "Sale Completed", nil
})
app.Run()
}
```
## 3. Histogram Metrics
Histogram is a {% new-tab-link title="synchronous Instrument" href="https://opentelemetry.io/docs/specs/otel/metrics/api/#synchronous-instrument-api" /%} which can be used to
report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile.
### Usage
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// initialize gofr object
app := gofr.New()
app.Metrics().NewHistogram("transaction_time", "used to track the time taken by a transaction",
5, 10, 15, 20, 25, 35)
app.POST("/transaction", func(ctx *gofr.Context) (any, error) {
transactionStartTime := time.Now()
// transaction logic
tranTime := time.Now().Sub(transactionStartTime).Milliseconds()
ctx.Metrics().RecordHistogram(ctx, "transaction_time", float64(tranTime))
return "Transaction Completed", nil
})
app.Run()
}
```
## 4. Gauge Metrics
Gauge is a {% new-tab-link title="synchronous Instrument" href="https://opentelemetry.io/docs/specs/otel/metrics/api/#synchronous-instrument-api" /%} which can be used to record non-additive value(s) when changes occur.
### Usage
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// initialize gofr object
app := gofr.New()
app.Metrics().NewGauge("product_stock", "used to track the number of products in stock")
app.POST("/sale", func(ctx *gofr.Context) (any, error) {
ctx.Metrics().SetGauge("product_stock", 10)
return "Sale Completed", nil
})
app.Run()
}
```
## Adding Labels to Custom Metrics
GoFr leverages metrics support by enabling labels. Labels are a key feature in metrics that allows us to categorize and filter metrics based on relevant information.
### Understanding Labels
Labels are key-value pairs attached to metrics. They provide additional context about the metric data.
Common examples of labels include:
- environment: (e.g., "production", "staging")
- service: (e.g., "api-gateway", "database")
- status: (e.g., "success", "failure")
By adding labels, we can create different time series for the same metric based on the label values.
This allows for more granular analysis and visualization in Grafana (or any other) dashboards.
### Additional Considerations
- Prefer to keep the number of labels manageable to avoid overwhelming complexity.
- Choose meaningful label names that clearly describe the data point.
- Ensure consistency in label naming conventions across your application.
By effectively using labels in GoFr, we can enrich your custom metrics and gain deeper insights into your application's performance and behavior.
### Usage:
Labels are added while populating the data for metrics, by passing them as arguments (comma separated key-value pairs)
in the GoFr's methods (namely: `IncrementCounter`, `DeltaUpDownCounter`, `RecordHistogram`, `SetGauge`).
Example: `c.Metrics().IncrementCounter(c, "metric-name", "metric-value", "label-1", "value-1", "label-2", "value-2")`
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
// Initialize gofr object
a := gofr.New()
// Add custom metrics
a.Metrics().NewUpDownCounter("total_credit_day_sale", "used to track the total credit sales in a day")
// Add all the routes
a.POST("/sale", SaleHandler)
a.POST("/return", ReturnHandler)
// Run the application
a.Run()
}
func SaleHandler(c *gofr.Context) (any, error) {
// logic to create sales
c.Metrics().DeltaUpDownCounter(c, "total_credit_day_sale", 10, "sale_type", "credit", "product_type", "beverage") // Here "sale_type" & "product_type" are the labels and "credit" & "beverage" are the values
return "Sale Successful", nil
}
func ReturnHandler(c *gofr.Context) (any, error) {
// logic to create a sales return
c.Metrics().DeltaUpDownCounter(c, "total_credit_day_sale", -5, "sale_type", "credit_return", "product_type", "dairy")
return "Return Successful", nil
}
```
**Good To Know**
```doc
While registering a metrics 2 key pieces of information of required:
- Name
- Description
When a registered metrics has to be used 3 key pieces of information are required:
- Name
- Value
- A set of key-value pairs called tags or labels.
A permutation of these key-value values provides the metric cardinality.
Lower the cardinality, faster the query performance and lower the monitoring resource utilization.
```
> #### Check out the example on how to publish custom metrics in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-custom-metrics/main.go)
================================================
FILE: docs/advanced-guide/rbac/page.md
================================================
# Role-Based Access Control (RBAC) in GoFr
Role-Based Access Control (RBAC) is a security mechanism that restricts access to resources based on user roles and permissions. GoFr provides a pure config-based RBAC middleware that supports multiple authentication methods, fine-grained permissions, and role inheritance.
## Overview
- ✅ **Pure Config-Based** - All authorization rules in JSON/YAML files
- ✅ **Two-Level Authorization Model** - Roles define permissions, endpoints require permissions (no direct role-to-route mapping)
- ✅ **Multiple Auth Methods** - Header-based and JWT-based role extraction
- ✅ **Permission-Based** - Fine-grained permissions
- ✅ **Role Inheritance** - Roles inherit permissions from other roles
## Quick Start
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Use default paths (configs/rbac.json, configs/rbac.yaml, configs/rbac.yml)
// Uses rbac.DefaultConfigPath internally (empty string triggers default path resolution)
// Tries configs/rbac.json, then configs/rbac.yaml, then configs/rbac.yml
app.EnableRBAC()
// Or with custom config path
app.EnableRBAC("configs/custom-rbac.json")
app.GET("/api/users", handler)
app.Run()
}
```
**Configuration** (`configs/rbac.json`):
```json
{
"roleHeader": "X-User-Role",
"roles": [
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"]
},
{
"name": "editor",
"permissions": ["users:write", "posts:write"],
"inheritsFrom": ["viewer"]
},
{
"name": "viewer",
"permissions": ["users:read", "posts:read"]
}
],
"endpoints": [
{
"path": "/health",
"methods": ["GET"],
"public": true
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users",
"methods": ["POST"],
"requiredPermissions": ["users:write"]
}
]
}
```
> **💡 Best Practice**: For production/public APIs, use JWT-based RBAC instead of header-based RBAC for better security.
## Configuration
### Role Extraction
**Header-Based** (for internal/trusted networks):
```json
{
"roleHeader": "X-User-Role"
}
```
**JWT-Based** (for production/public APIs):
```json
{
"jwtClaimPath": "role" // or "roles[0]", "permissions.role", etc.
}
```
**Precedence**: If both are set, **only JWT is considered**. The header is not checked when `jwtClaimPath` is configured, even if JWT extraction fails.
**JWT Claim Path Formats**:
- `"role"` → `{"role": "admin"}`
- `"roles[0]"` → `{"roles": ["admin", "user"]}` (first element)
- `"permissions.role"` → `{"permissions": {"role": "admin"}}`
### Roles and Permissions
```json
{
"roles": [
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"] // Explicit permissions (wildcards not supported)
},
{
"name": "editor",
"permissions": ["users:write", "posts:write"], // Only additional permissions
"inheritsFrom": ["viewer"] // Inherits viewer's permissions
},
{
"name": "viewer",
"permissions": ["users:read", "posts:read"]
}
]
}
```
**Note**: When using `inheritsFrom`, only specify additional permissions - inherited ones are automatically included.
### Endpoint Mapping
```json
{
"endpoints": [
{
"path": "/health",
"methods": ["GET"],
"public": true // Bypasses authorization
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users/{id:[0-9]+}", // Mux pattern with constraint (numeric IDs only)
"methods": ["DELETE"],
"requiredPermissions": ["users:delete"]
},
{
"path": "/api/{resource}", // Single-level pattern - matches /api/users, /api/posts
"methods": ["GET"],
"requiredPermissions": ["api:read"]
},
{
"path": "/api/{path:.*}", // Multi-level pattern - matches /api/users/123, /api/posts/comments
"methods": ["*"], // All methods
"requiredPermissions": ["admin:read", "admin:write"] // Multiple permissions (OR logic)
},
{
"path": "/api/{category}/posts", // Middle variable - matches /api/tech/posts, /api/news/posts
"methods": ["GET"],
"requiredPermissions": ["posts:read"]
}
]
}
```
### Mux Pattern Syntax
RBAC uses **gorilla/mux route pattern conventions** for endpoint matching. This ensures perfect alignment with how routes are registered in GoFr.
**Important**: The RBAC middleware uses the same router configuration as GoFr's application router (`StrictSlash(false)`), ensuring consistent behavior for trailing slashes. This means `/api/users` and `/api/users/` are treated as the same route in both RBAC authorization checks and actual route matching.
**Pattern Types**:
- **Exact**: `"/api/users"` matches exactly `/api/users`
- **Single Variable**: `"/api/users/{id}"` matches `/api/users/123`, `/api/users/abc` (any single segment)
- **Variable with Constraint**: `"/api/users/{id:[0-9]+}"` matches `/api/users/123` (numeric IDs only)
- **Single-Level Pattern**: `"/api/{resource}"` matches `/api/users`, `/api/posts` (one segment)
- **Multi-Level Pattern**: `"/api/{path:.*}"` matches `/api/users/123`, `/api/posts/comments` (any depth)
- **Middle Variable**: `"/api/{category}/posts"` matches `/api/tech/posts`, `/api/news/posts`
**Common Patterns**:
- Numeric IDs: `"/api/users/{id:[0-9]+}"` (matches `/api/users/123`)
- UUIDs: `"/api/users/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}"` (matches `/api/users/550e8400-e29b-41d4-a716-446655440000`)
- Alphanumeric: `"/api/users/{name:[a-zA-Z0-9]+}"` (matches `/api/users/user123`)
**Grouped Endpoints**:
For endpoints that need to match multiple paths, use mux patterns:
- **Single-level wildcard**: Use `"/api/{resource}"` instead of `"/api/*"`
- Matches: `/api/users`, `/api/posts` (one segment)
- **Multi-level wildcard**: Use `"/api/{path:.*}"` instead of `"/api/*"`
- Matches: `/api/users/123`, `/api/posts/comments` (any depth)
- **Middle variable**: Use `"/api/{category}/posts"` instead of `"/api/*/posts"`
- Matches: `/api/tech/posts`, `/api/news/posts`
## JWT-Based RBAC
For production/public APIs, use JWT-based role extraction:
```go
app := gofr.New()
// Enable OAuth middleware first (required for JWT validation)
app.EnableOAuth("https://auth.example.com/.well-known/jwks.json", 10)
// Enable RBAC with config path (or use app.EnableRBAC() for default paths using rbac.DefaultConfigPath)
app.EnableRBAC("configs/rbac.json")
```
**Configuration** (`configs/rbac.json`):
```json
{
"jwtClaimPath": "role", // or "roles[0]", "permissions.role", etc.
"roles": [...],
"endpoints": [...]
}
```
## Accessing Role in Handlers
For business logic, you can access the user's role from the request context:
**JWT-Based RBAC** (when using JWT role extraction):
```go
import (
"encoding/json"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http"
)
// JWTClaims represents the JWT claims structure
type JWTClaims struct {
Role string `json:"role"`
Sub string `json:"sub"`
// Add other claim fields as needed
}
func handler(ctx *gofr.Context) (interface{}, error) {
// Get JWT claims from context
claimsMap := ctx.GetAuthInfo().GetClaims()
if claimsMap == nil {
return nil, http.ErrorInvalidParam{Params: []string{"authorization"}}
}
// Convert map claims to struct (recommended GoFr pattern)
var claims JWTClaims
claimsBytes, err := json.Marshal(claimsMap)
if err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"claims"}}
}
if err := json.Unmarshal(claimsBytes, &claims); err != nil {
return nil, http.ErrorInvalidParam{Params: []string{"claims"}}
}
// Use role for business logic (e.g., personalize UI, filter data)
// The role field matches the jwtClaimPath configured in rbac.json
return map[string]string{"userRole": claims.Role}, nil
}
```
**Note**: All authorization is handled automatically by the middleware. Accessing the role in handlers is only for business logic purposes (e.g., personalizing UI, filtering data).
## Permission Naming Conventions
### Recommended Format
Use the format: `resource:action`
- **Resource**: The entity being accessed (e.g., `users`, `posts`, `orders`)
- **Action**: The operation being performed (e.g., `read`, `write`, `delete`, `update`)
### Examples:
```editorconfig
"users:read" // Read users
"users:write" // Create/update users
"users:delete" // Delete users
"posts:read" // Read posts
"posts:write" // Create/update posts
"orders:approve" // Approve orders
"reports:export" // Export reports
```
**Avoid inconsistent formats**:
- ❌ `"read_users"`, `"writeUsers"`, `"DELETE_POSTS"`
- ✅ `"users:read"`, `"users:write"`, `"posts:delete"`
### Wildcards Not Supported
**Important**: Wildcards are **NOT supported** in permissions. Only exact matches are allowed.
- ❌ `"*:*"` - Does not match all permissions
- ❌ `"users:*"` - Does not match all user permissions
- ✅ `"users:read"` - Exact match only
- ✅ `"users:write"` - Exact match only
If you need multiple permissions, specify them explicitly:
```json
{
"name": "admin",
"permissions": ["users:read", "users:write", "users:delete", "posts:read", "posts:write"]
}
```
Or use role inheritance to avoid duplication:
```json
{
"name": "editor",
"permissions": ["users:write", "posts:write"],
"inheritsFrom": ["viewer"] // Inherits viewer's permissions
}
```
## Common Patterns
### CRUD Permissions
```json
{
"roles": [
{
"name": "admin",
"permissions": ["users:delete"],
"inheritsFrom": ["editor"]
},
{
"name": "editor",
"permissions": ["users:create", "users:update"],
"inheritsFrom": ["viewer"]
},
{
"name": "viewer",
"permissions": ["users:read"]
}
],
"endpoints": [
{
"path": "/api/users",
"methods": ["POST"],
"requiredPermissions": ["users:create"]
},
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
},
{
"path": "/api/users/{id:[0-9]+}",
"methods": ["PUT", "PATCH"],
"requiredPermissions": ["users:update"]
},
{
"path": "/api/users/{id:[0-9]+}",
"methods": ["DELETE"],
"requiredPermissions": ["users:delete"]
}
]
}
```
### Resource-Specific Permissions
```json
{
"roles": [
{
"name": "admin",
"permissions": ["own:posts:read", "own:posts:write", "all:posts:read", "all:posts:write"]
},
{
"name": "author",
"permissions": ["own:posts:read", "own:posts:write"]
},
{
"name": "viewer",
"permissions": ["own:posts:read", "all:posts:read"]
}
],
"endpoints": [
{
"path": "/api/posts/my-posts",
"methods": ["GET"],
"requiredPermissions": ["own:posts:read"]
},
{
"path": "/api/posts",
"methods": ["GET"],
"requiredPermissions": ["all:posts:read"]
}
]
}
```
## Best Practices
### Security
- **Never use header-based RBAC for public APIs** - Use JWT-based RBAC
- **Always validate JWT tokens** - Use proper JWKS endpoints with HTTPS
- **Use HTTPS in production** - Protect tokens and headers
- **Monitor logs** - Track authorization decisions
### Configuration
- **Use role inheritance** - Avoid duplicating permissions (only specify additional ones)
- **Use consistent naming** - Follow `resource:action` format (e.g., `users:read`, `posts:write`)
- **Group related permissions** - Organize by resource type
- **Version control configs** - Track RBAC changes in git
## Troubleshooting
**Role not being extracted**
- Ensure `roleHeader` or `jwtClaimPath` is set in config file
- For header-based: check that the header is present in requests
- For JWT-based: ensure OAuth middleware is enabled before RBAC
**Permission checks failing**
- Verify `roles[].permissions` is properly configured
- Check that `endpoints[].requiredPermissions` matches your routes correctly
- Ensure role has the required permission (check inherited permissions too)
- Verify route pattern matches exactly (mux patterns supported)
- Check role inheritance - ensure inherited permissions are included
**Permission always denied**
- Check role assignment - verify user's role has the required permission
- Review role permissions - ensure `roles[].permissions` includes the required permission
- Enable debug logging - check debug logs for authorization decisions
**Permission always allowed**
- Check if endpoint is in RBAC config - routes not in config are allowed to proceed
- Check public endpoints - verify endpoint is not marked as `public: true`
- Review endpoint configuration - ensure `endpoints[].requiredPermissions` is set correctly
- Verify permission check - check logs to see if permission check is being performed
**JWT role extraction failing**
- Ensure OAuth middleware is enabled before RBAC
- Verify JWT claim path is correct
**Config file not found**
- Ensure config file exists at the specified path
- Or use default paths (`configs/rbac.json`, `configs/rbac.yaml`, `configs/rbac.yml`)
**Route not being protected by RBAC**
- Verify the route is explicitly configured in `endpoints[]` array
- Check that the path pattern matches exactly (case-sensitive)
- Ensure HTTP method matches (or use `["*"]` for all methods)
- Remember: Routes not in RBAC config are allowed to proceed (not blocked)
## How It Works
1. **Role Extraction**: Extracts user role from header (`X-User-Role`) or JWT claims
2. **Endpoint Matching**: Matches request method + path to endpoint configuration
3. **Permission Check**: Verifies role has required permission for the endpoint
4. **Authorization**: Allows or denies request based on permission check
The middleware automatically handles all authorization - you just define routes normally.
### Unmatched Routes Behavior
**Important**: RBAC only enforces authorization for endpoints that are **explicitly configured** in the RBAC config file.
- ✅ **Routes in RBAC config**: Authorization is enforced (requires valid role and permissions)
- ✅ **Routes NOT in RBAC config**: Requests are allowed to proceed to normal route matching
- If the route exists in your application, it will be handled normally
- If the route doesn't exist, it will return 404 (route not registered)
**Example**:
```json
{
"endpoints": [
{
"path": "/api/users",
"methods": ["GET"],
"requiredPermissions": ["users:read"]
}
]
}
```
In this configuration:
- `GET /api/users` → **RBAC enforced** (requires `users:read` permission)
- `POST /api/users` → **Not in RBAC config** → Allowed to proceed (may return 404 if route doesn't exist)
- `GET /api/posts` → **Not in RBAC config** → Allowed to proceed (may return 404 if route doesn't exist)
- `GET /health` → **Not in RBAC config** → Allowed to proceed (will work if route exists)
This design allows you to:
- Gradually add RBAC protection to specific endpoints
- Keep some routes unprotected (not in RBAC config)
- Let the router handle 404s for non-existent routes
## Security and Privacy
### Telemetry Data Protection
RBAC middleware implements industry-standard security practices to protect sensitive data:
**Traces (OpenTelemetry):**
- ✅ HTTP method and route patterns included
- ✅ Authorization status (allowed/denied) included
- ❌ Roles excluded (privacy protection - roles are PII)
- ❌ Error messages sanitized (prevent information leakage)
**Metrics:**
- ✅ Authorization decision counts included
- ✅ Status (allowed/denied) included
- ❌ Roles excluded (avoid high cardinality and PII concerns)
**Logs:**
- ✅ Roles included (required for compliance: SOC 2, PCI-DSS, NIST)
- ✅ HTTP method, route, status, and reason included
- ❌ No authorization tokens, headers, or request bodies logged
- ❌ No user IDs or personal information logged
### What's Never Logged
RBAC middleware never logs:
- Authorization tokens (Bearer tokens, API keys)
- Request bodies or headers
- User IDs or personal information
- IP addresses in traces/metrics
- Detailed error messages exposing internal details
## Related Documentation
- [HTTP Authentication](https://gofr.dev/docs/advanced-guide/http-authentication) - Basic Auth, API Keys, OAuth 2.0
- [HTTP Communication](https://gofr.dev/docs/advanced-guide/http-communication) - Inter-service HTTP calls
- [Middlewares](https://gofr.dev/docs/advanced-guide/middlewares) - Custom middleware implementation
================================================
FILE: docs/advanced-guide/remote-log-level-change/page.md
================================================
# Remote Log Level Change
GoFr makes it easy to adjust the details captured in the application's logs, even while it's running!
This feature allows users to effortlessly fine-tune logging levels without the need for redeployment, enhancing the monitoring and debugging experience.
It is facilitated through simple configuration settings.
## How it helps?
- **Effortless Adjustments:** Modify the log level anytime without restarting the application. This is especially helpful during troubleshooting.
- **Enhanced Visibility:** Easily switch to a more detailed log level (e.g., `DEBUG`) to gain deeper insights into specific issues,
and then switch back to a less detailed level (e.g., `INFO`) for regular operation.
- **Improved Performance:** Generating a large number of logs can overwhelm the logging system, leading to increased I/O operations and resource consumption,
changing to Warn or Error Level reduces the number of logs, and enhancing performance.
## Configuration
To enable remote log level update, users need to specify the following configuration parameter:
```dotenv
REMOTE_LOG_URL= (e.g., https://log-service.com/log-levels)
REMOTE_LOG_FETCH_INTERVAL= (default: 15)
```
- **REMOTE_LOG_URL:** Specifies the URL of the remote log level endpoint.
- **REMOTE_LOG_FETCH_INTERVAL:** Defines the time interval (in seconds) at which GoFr fetches log level configurations from the endpoint.
> [!NOTE]
> If not provided the default interval between the request to fetch log level is **15 seconds**.
## Remote Log Level Endpoint
The remote log level endpoint should return a JSON response in the following format:
```json
{
"data": {
"serviceName": "test-service",
"logLevel": "DEBUG"
}
}
```
- **serviceName:** Identifies the service for which log levels are configured.
- **logLevel:** The new log level user want to set for the specified service.
GoFr parses this response and adjusts log levels based on the provided configurations.
================================================
FILE: docs/advanced-guide/serving-static-files/page.md
================================================
# Serving Static Files using GoFr
Often, we are required to serve static content such as a default profile image, a favicon, or a background image for our
web application. We want to have a mechanism to serve that static content without the hassle of implementing it from scratch.
GoFr provides a default mechanism where if a `static` folder is available in the directory of the application,
it automatically provides an endpoint with `/static/`, here filename refers to the file we want to get static content to be served.
Example project structure:
```dotenv
project_folder
|
|---configs
| .env
|---static
| img1.jpeg
| img2.png
| img3.jpeg
| main.go
| main_test.go
```
main.go code:
```go
package main
import "gofr.dev/pkg/gofr"
func main() {
app := gofr.New()
app.Run()
}
```
Additionally, if we want to serve more static endpoints, we have a dedicated function called `AddStaticFiles()`
which takes 2 parameters `endpoint` and the `filepath` of the static folder which we want to serve. If the folder
contains a `404.html` file, GoFr automatically serves it for any missing URL, redirecting all "Not Found" requests
to this page.
Example project structure:
```dotenv
project_folder
|
|---configs
| .env
|---static
| img1.jpeg
| img2.png
| img3.jpeg
|---public
| |---css
| | main.css
| |---js
| | main.js
| | index.html
| | 404.html
| main.go
| main_test.go
```
main.go file:
```go
package main
import "gofr.dev/pkg/gofr"
func main() {
app := gofr.New()
app.AddStaticFiles("public", "./public")
app.Run()
}
```
In the above example, both endpoints `/public` and `/static` are available for the app to render the static content.
================================================
FILE: docs/advanced-guide/setting-custom-response-headers/page.md
================================================
# Custom Response Headers and Metadata in GoFr
GoFr simplifies the process of adding custom HTTP response headers and metadata to API responses using the `Response` struct. This feature allows you to include additional information such as custom headers or metadata to enhance client-server communication while keeping your data payload clean and structured.
## Features
1. **Custom Headers**: Add key-value pairs for headers, useful for:
- Security policies
- Debugging information
- Versioning details
**Type**: `map[string]string`
- Keys and values must be strings.
2. **Metadata**: Include optional contextual information like:
- Deployment environment
- Request-specific details (e.g., timestamps, tracing IDs)
**Type**: `map[string]any`
- Keys must be strings, and values can be of any type.
When metadata is included, the response structure is:
```json
{
"data": {},
"metadata": {}
}
```
If metadata is omitted, the response defaults to:
```json
{
"data": {}
}
```
### Example Usage
#### Adding Custom Headers and Metadata
To include custom headers and metadata in your response, populate the Headers and MetaData fields of the Response struct in your handler function.
```go
package main
import (
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http/response"
)
func main() {
app := gofr.New()
app.GET("/hello", HelloHandler)
app.Run()
}
func HelloHandler(c *gofr.Context) (any, error) {
name := c.Param("name")
if name == "" {
c.Log("Name parameter is empty, defaulting to 'World'")
name = "World"
}
// Define custom headers (map[string]string)
headers := map[string]string{
"X-Custom-Header": "CustomValue",
"X-Another-Header": "AnotherValue",
}
// Define metadata (map[string]any)
metaData := map[string]any{
"environment": "staging",
"timestamp": time.Now(),
}
// Return response with custom headers and metadata
return response.Response{
Data: map[string]string{"message": "Hello, " + name + "!"},
Metadata: metaData,
Headers: headers,
}, nil
}
```
### Example Responses
#### Response with Metadata:
When metadata is included, the response contains the metadata field:
```json
{
"data": {
"message": "Hello, World!"
},
"metadata": {
"environment": "staging",
"timestamp": "2024-12-23T12:34:56Z"
}
}
```
#### Response without Metadata:
If no metadata is provided, the response only includes the data field:
```json
{
"data": {
"message": "Hello, World!"
}
}
```
This functionality offers a convenient, structured way to include additional response information without altering the
core data payload.
================================================
FILE: docs/advanced-guide/startup-hooks/page.md
================================================
# Startup Hooks
GoFr provides a way to run synchronous jobs when your application starts, before any servers begin handling requests. This is useful for tasks like seeding a database, warming up a cache, or performing other critical setup procedures.
## OnStart
You can register a startup hook using the `a.OnStart()` method on your `app` instance.
## Usage
The method accepts a function with the signature:
The method accepts a function with the signature `func(ctx *gofr.Context) error`.
- The `*gofr.Context` passed to the hook is fully initialized and provides access to all dependency-injection-managed services (e.g., `ctx.Container.SQL`, `ctx.Container.Redis`).
- If any `OnStart` hook returns an error, the application will log the error and refuse to start.
### Example: Warming up a Cache
Here is an example of using `OnStart` to set an initial value in a Redis cache when the application starts.
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
a := gofr.New()
// Register an OnStart hook to warm up a cache.
a.OnStart(func(ctx *gofr.Context) error {
ctx.Logger.Info("Warming up the cache...")
// In a real app, this might come from a database or another service.
cacheKey := "initial-data"
cacheValue := "This is some data cached at startup."
err := ctx.Redis.Set(ctx, cacheKey, cacheValue, 0).Err()
if err != nil {
ctx.Logger.Errorf("Failed to warm up cache: %v", err)
return err // Return the error to halt startup if caching fails.
}
ctx.Logger.Info("Cache warmed up successfully!")
return nil
})
// ... register your routes
a.Run()
}
```
This ensures that critical startup tasks are completed successfully before the application begins accepting traffic.
================================================
FILE: docs/advanced-guide/swagger-documentation/page.md
================================================
# Rendering OpenAPI Documentation in GoFr
GoFr supports automatic rendering of OpenAPI (also known as Swagger) documentation. This feature allows you to
easily provide interactive API documentation for your users.
## What is OpenAPI/Swagger Documentation?
OpenAPI, also known as Swagger, is a specification for building APIs. An OpenAPI file allows you to describe your entire API, including:
- Available endpoints (/users) and operations on each endpoint (GET /users, DELETE /users/{id})
- Operation parameters, input, and output for each operation
- Authentication methods
- Contact information, license, terms of use, and other information.
API specifications can be written in YAML or JSON. The format is easy to learn and readable to both humans and machines.
The complete OpenAPI Specification can be found on the official [Swagger website](https://swagger.io/).
## Enabling GoFr to render your openapi.json file
To allow GoFr to render your OpenAPI documentation, simply place your `openapi.json` file inside the `static` directory of your project.
GoFr will automatically render the Swagger documentation at the `/.well-known/swagger` endpoint.
Here are the steps:
- Create an `openapi.json` file that describes your API according to the OpenAPI specification.
- Place the `openapi.json` file inside the `static` directory in your project.
- Start your GoFr server.
- Navigate to `/.well-known/swagger` on your server’s URL.
You should now see a beautifully rendered, interactive documentation for your API that users can use to understand and interact with your API.
================================================
FILE: docs/advanced-guide/using-cron/page.md
================================================
# Cron job scheduling
Cron is a task scheduler that allows user to automate commands or scripts to
run at specific times, dates, or intervals. This makes cron a powerful tool for system administrators and developers who
want to automate repetitive tasks.
What can users automate with cron?
- **System maintenance**: Cron can be used to schedule regular backups, update software packages, or clean up temporary files.
- **Data processing**: Users can use cron to download data from the internet at specific times, process it, and generate reports.
- **Sending notifications**: Cron can be used to trigger emails or other notifications based on events or system logs.
Basically, any task that can be expressed as a command or script can be automated with cron.
Writing a cron job!
On Linux like systems cron jobs can be added by adding a line to the crontab file, specifying the schedule and the command
that needs to be run at that schedule. The cron schedule is expressed in the following format:
`minute hour day_of_month month day_of_week`
GoFr also allows an optional field for `second` as first part in the schedule format, like in the following format:
`second minute hour day_of_month month day_of_week`
Each field can take a specific value or combination of values to define the schedule. Users can use special characters like
`*` (asterisk) to represent **any** value and `,` (comma) to separate multiple values. It also supports `0-n` to define a
range of values for which the cron should run and `*/n` to define number of times the cron should run. Here n is an integer.
## Adding cron jobs in GoFr applications
Adding cron jobs to GoFr applications is made easy with a simple injection of user's function to the cron table maintained
by the GoFr. The minimum time difference between cron job's two consecutive runs is a minute as it is the least significant
scheduling time parameter.
Cron job with generic format:
```go
app.AddCronJob("* * * * *", "job-name", func(ctx *gofr.Context) {
// the cron job that needs to be executed at every minute
})
```
Cron job with optional second in format:
```go
app.AddCronJob("* * * * * *", "job-name", func(ctx *gofr.Context) {
// the cron job that needs to be executed at every second
})
```
The `AddCronJob` methods takes three arguments—a cron schedule, the cron job name(for tracing) and the set of statements
that are to be executed at the given schedule.
### Example
```go
package main
import (
"time"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Run the cron job every 5 hours(*/5)
app.AddCronJob("* */5 * * *", "", func(ctx *gofr.Context) {
ctx.Logger.Infof("current time is %v", time.Now())
})
// Run the cron job every 10 seconds(*/10)
app.AddCronJob("*/10 * * * * *", "", func(ctx *gofr.Context) {
ctx.Logger.Infof("current time is %v", time.Now())
})
app.Run()
}
```
### Cron job metrics
GoFr automatically collects metrics for all registered cron jobs. These metrics are available on the `/metrics` endpoint (default port 2121) and include:
- `app_cron_job_total`: Total number of times a cron job has been triggered.
- `app_cron_job_success`: Number of successful executions.
- `app_cron_job_failures`: Number of failed executions (including panics).
- `app_cron_job_duration`: Duration of execution in **seconds**.
Each metric is labeled with the `job` (user-defined name) to allow fine-grained filtering and monitoring.
> #### Check out the example on how to add cron jobs in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-cron-jobs/main.go)
================================================
FILE: docs/advanced-guide/using-publisher-subscriber/page.md
================================================
# Publisher Subscriber
Publisher Subscriber is an architectural design pattern for asynchronous communication between different entities.
These could be different applications or different instances of the same application.
Thus, the movement of messages between the components is made possible without the components being aware of each other's
identities, meaning the components are decoupled.
This makes the application/system more flexible and scalable as each component can be
scaled and maintained according to its own requirement.
## Design choice
In GoFr application if a user wants to use the Publisher-Subscriber design, it supports several message brokers,
including Apache Kafka, Google PubSub, MQTT, NATS JetStream, and Redis Pub/Sub.
The initialization of the PubSub is done in an IoC container which handles the PubSub client dependency.
With this, the control lies with the framework and thus promotes modularity, testability, and re-usability.
Users can do publish and subscribe to multiple topics in a single application, by providing the topic name.
Users can access the methods of the container to get the Publisher and Subscriber interface to perform subscription
to get a single message or publish a message on the message broker.
> Container is part of the GoFr Context
## Configuration and Setup
Some of the configurations that are required to configure the PubSub backend that an application is to use
that are specific for the type of message broker user wants to use.
`PUBSUB_BACKEND` defines which message broker the application needs to use.
### Kafka
#### Configs
{% table %}
- Name
- Description
- Required
- Default
- Example
- Valid format
---
- `PUBSUB_BACKEND`
- Using Apache Kafka as message broker.
- `+`
-
- `KAFKA`
- Not empty string
---
- `PUBSUB_BROKER`
- Address to connect to kafka broker. Multiple brokers can be added as comma separated values.
- `+`
-
- `localhost:9092` or `localhost:8087,localhost:8088,localhost:8089`
- Not empty string
---
- `CONSUMER_ID`
- Consumer group id to uniquely identify the consumer group.
- if consuming
-
- `order-consumer`
- Not empty string
---
- `PUBSUB_OFFSET`
- Determines from whence the consumer group should begin consuming when it finds a partition without a committed offset.
- `-`
- `-1`
- `10`
- int
---
- `KAFKA_BATCH_SIZE`
- Limit on how many messages will be buffered before being sent to a partition.
- `-`
- `100`
- `10`
- Positive int
---
- `KAFKA_BATCH_BYTES`
- Limit the maximum size of a request in bytes before being sent to a partition.
- `-`
- `1048576`
- `65536`
- Positive int
---
- `KAFKA_BATCH_TIMEOUT`
- Time limit on how often incomplete message batches will be flushed to Kafka (in milliseconds).
- `-`
- `1000`
- `300`
- Positive int
---
- `KAFKA_SECURITY_PROTOCOL`
- Security protocol used to communicate with Kafka (e.g., PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL).
- `-`
- `PLAINTEXT`
- `SASL_SSL`
- String
---
- `KAFKA_SASL_MECHANISM`
- SASL mechanism for authentication (e.g., PLAIN, SCRAM-SHA-256, SCRAM-SHA-512).
- `-`
- `""`
- `PLAIN`
- String
---
- `KAFKA_SASL_USERNAME`
- Username for SASL authentication.
- `-`
- `""`
- `user`
- String
---
- `KAFKA_SASL_PASSWORD`
- Password for SASL authentication.
- `-`
- `""`
- `password`
- String
---
- `KAFKA_TLS_CERT_FILE`
- Path to the TLS certificate file.
- `-`
- `""`
- `/path/to/cert.pem`
- Path
---
- `KAFKA_TLS_KEY_FILE`
- Path to the TLS key file.
- `-`
- `""`
- `/path/to/key.pem`
- Path
---
- `KAFKA_TLS_CA_CERT_FILE`
- Path to the TLS CA certificate file.
- `-`
- `""`
- `/path/to/ca.pem`
- Path
---
- `KAFKA_TLS_INSECURE_SKIP_VERIFY`
- Skip TLS certificate verification.
- `-`
- `false`
- `true`
- Boolean
{% /table %}
```dotenv
PUBSUB_BACKEND=KAFKA# using apache kafka as message broker
PUBSUB_BROKER=localhost:9092
CONSUMER_ID=order-consumer
KAFKA_BATCH_SIZE=1000
KAFKA_BATCH_BYTES=1048576
KAFKA_BATCH_TIMEOUT=300
KAFKA_SASL_MECHANISM=PLAIN
KAFKA_SASL_USERNAME=user
KAFKA_SASL_PASSWORD=password
KAFKA_TLS_CERT_FILE=/path/to/cert.pem
KAFKA_TLS_KEY_FILE=/path/to/key.pem
KAFKA_TLS_CA_CERT_FILE=/path/to/ca.pem
KAFKA_TLS_INSECURE_SKIP_VERIFY=true
#### Docker setup
```shell
docker run --name kafka-1 -p 9092:9092 \
-e KAFKA_ENABLE_KRAFT=yes \
-e KAFKA_CFG_PROCESS_ROLES=broker,controller \
-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e ALLOW_PLAINTEXT_LISTENER=yes \
-e KAFKA_CFG_NODE_ID=1 \
-v kafka_data:/bitnami \
bitnami/kafka:3.4
```
### GOOGLE
#### Configs
```dotenv
PUBSUB_BACKEND=GOOGLE // using Google PubSub as message broker
GOOGLE_PROJECT_ID=project-order // google projectId where the PubSub is configured
GOOGLE_SUBSCRIPTION_NAME=order-consumer // unique subscription name to identify the subscribing entity
```
#### Docker setup
```shell
docker pull gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators
docker run --name=gcloud-emulator -d -p 8086:8086 \
gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators gcloud beta emulators pubsub start --project=test123 \
--host-port=0.0.0.0:8086
```
> **Note**: To set GOOGLE_APPLICATION_CREDENTIAL - refer {% new-tab-link title="here" href="https://cloud.google.com/docs/authentication/application-default-credentials" /%}
> **Note**: In Google PubSub only one subscription name can access one topic, framework appends the topic name and subscription name to form the
> unique subscription name on the Google client.
### MQTT
#### Configs
```dotenv
PUBSUB_BACKEND=MQTT // using MQTT as pubsub
MQTT_HOST=localhost // broker host URL
MQTT_PORT=1883 // broker port
MQTT_CLIENT_ID_SUFFIX=test // suffix to a random generated client-id(uuid v4)
#some additional configs(optional)
MQTT_PROTOCOL=tcp // protocol for connecting to broker can be tcp, tls, ws or wss
MQTT_MESSAGE_ORDER=true // config to maintain/retain message publish order, by default this is false
MQTT_USER=username // authentication username
MQTT_PASSWORD=password // authentication password
```
> **Note** : If `MQTT_HOST` config is not provided, the application will connect to a public broker
> {% new-tab-link title="EMQX Broker" href="https://www.emqx.com/en/mqtt/public-mqtt5-broker" /%}
#### Docker setup
```shell
docker run -d \
--name mqtt \
-p 8883:8883 \
-v \
eclipse-mosquitto:latest /mosquitto.conf:/mosquitto/config/mosquitto.conf
```
> **Note**: find the default mosquitto config file {% new-tab-link title="here" href="https://github.com/eclipse/mosquitto/blob/master/mosquitto.conf" /%}
### NATS JetStream
NATS JetStream is supported as an external PubSub provider, meaning if you're not using it, it won't be added to your binary.
**References**
https://docs.nats.io/
https://docs.nats.io/nats-concepts/jetstream
https://docs.nats.io/using-nats/developer/connecting/creds
#### Configs
```dotenv
PUBSUB_BACKEND=NATS
PUBSUB_BROKER=nats://localhost:4222
NATS_STREAM=mystream
NATS_SUBJECTS=orders.*,shipments.*
NATS_MAX_WAIT=5s
NATS_MAX_PULL_WAIT=500ms
NATS_CONSUMER=my-consumer
NATS_CREDS_FILE=/path/to/creds.json
```
#### Setup
To set up NATS JetStream, follow these steps:
1. Import the external driver for NATS JetStream:
```bash
go get gofr.dev/pkg/gofr/datasources/pubsub/nats
```
2. Use the `AddPubSub` method to add the NATS JetStream driver to your application:
```go
app := gofr.New()
app.AddPubSub(nats.New(nats.Config{
Server: "nats://localhost:4222",
Stream: nats.StreamConfig{
Stream: "mystream",
Subjects: []string{"orders.*", "shipments.*"},
},
MaxWait: 5 * time.Second,
MaxPullWait: 500 * time.Millisecond,
Consumer: "my-consumer",
CredsFile: "/path/to/creds.json",
}))
```
#### Docker setup
```shell
docker run -d \
--name nats \
-p 4222:4222 \
-p 8222:8222 \
-v \
nats:2.9.16 /nats.conf:/nats/config/nats.conf
```
#### Configuration Options
| Name | Description | Required | Default | Example |
|------|-------------|----------|---------|---------|
| `PUBSUB_BACKEND` | Set to "NATS" to use NATS JetStream as the message broker | Yes | - | `NATS` |
| `PUBSUB_BROKER` | NATS server URL | Yes | - | `nats://localhost:4222` |
| `NATS_STREAM` | Name of the NATS stream | Yes | - | `mystream` |
| `NATS_SUBJECTS` | Comma-separated list of subjects to subscribe to | Yes | - | `orders.*,shipments.*` |
| `NATS_MAX_WAIT` | Maximum wait time for batch requests | No | - | `5s` |
| `NATS_MAX_PULL_WAIT` | Maximum wait time for individual pull requests | No | 0 | `500ms` |
| `NATS_CONSUMER` | Name of the NATS consumer | No | - | `my-consumer` |
| `NATS_CREDS_FILE` | Path to the credentials file for authentication | No | - | `/path/to/creds.json` |
#### Usage
When subscribing or publishing using NATS JetStream, make sure to use the appropriate subject name that matches your stream configuration.
For more information on setting up and using NATS JetStream, refer to the official NATS documentation.
### Redis Pub/Sub
Redis Pub/Sub is a lightweight messaging system. GoFr supports two modes:
1. **Streams Mode** (Default): Uses Redis Streams for persistent messaging with consumer groups and acknowledgments.
2. **PubSub Mode**: Standard Redis Pub/Sub (fire-and-forget, no persistence).
#### Redis connection
Redis Pub/Sub uses the same Redis connection configuration as the Redis datasource (`REDIS_HOST`, `REDIS_PORT`, `REDIS_DB`, TLS, etc.).
See the config reference: `https://gofr.dev/docs/references/configs#redis`.
#### Example `.env`
```dotenv
PUBSUB_BACKEND=REDIS
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_USER=myuser
REDIS_PASSWORD=mypassword
REDIS_DB=0
REDIS_PUBSUB_DB=1
REDIS_TLS_ENABLED=true
REDIS_TLS_CA_CERT=/path/to/ca.pem
REDIS_TLS_CERT=/path/to/cert.pem
REDIS_TLS_KEY=/path/to/key.pem
# Streams mode (default) - requires consumer group
REDIS_STREAMS_CONSUMER_GROUP=my-group
REDIS_STREAMS_CONSUMER_NAME=my-consumer
REDIS_STREAMS_BLOCK_TIMEOUT=5s
REDIS_STREAMS_PEL_RATIO=0.7 # 70% PEL, 30% new messages
REDIS_STREAMS_MAXLEN=1000
# To use PubSub mode instead, set:
# REDIS_PUBSUB_MODE=pubsub
```
#### Docker setup
```shell
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpine
```
For Redis with password authentication:
```shell
docker run -d \
--name redis \
-p 6379:6379 \
redis:7-alpine redis-server --requirepass mypassword
```
#### Redis configs
The following configs apply specifically to Redis Pub/Sub behavior. For base Redis connection/TLS configs, refer to
`https://gofr.dev/docs/references/configs#redis`.
{% table %}
- Name
- Description
- Default
- Example
---
- `PUBSUB_BACKEND`
- Set to `REDIS` to use Redis as the Pub/Sub backend.
- -
- `REDIS`
---
- `REDIS_PUBSUB_MODE`
- Mode: `streams` (default, at-least-once) or `pubsub` (at-most-once)
- `streams`
- `pubsub`
---
- `REDIS_STREAMS_CONSUMER_GROUP`
- Consumer group name (required in streams mode)
- -
- `mygroup`
---
- `REDIS_STREAMS_CONSUMER_NAME`
- Consumer name (optional; auto-generated if empty)
- -
- `consumer-1`
---
- `REDIS_STREAMS_BLOCK_TIMEOUT`
- Blocking timeout for stream reads. Lower values (1s-2s) = faster detection, higher CPU. Higher values (10s-30s) = lower CPU, higher latency.
- `5s`
- `2s` or `30s`
> **Important**: If `REDIS_STREAMS_CONSUMER_GROUP` is empty or not provided, an error will occur when attempting to subscribe. However, publishing will work correctly without it.
---
- `REDIS_STREAMS_PEL_RATIO`
- Ratio of PEL (pending) messages to read vs new messages (0.0-1.0). Ratio determines initial PEL allocation; all remaining capacity is always filled with new messages.
- `0.7`
- `0.5` or `0.8`
---
- `REDIS_STREAMS_MAXLEN`
- Max stream length for trimming (approximate). Set to `0` for unlimited.
- `0` (unlimited)
- `10000`
---
- `REDIS_PUBSUB_DB`
- Redis DB for Pub/Sub operations. Keep different from `REDIS_DB` when using migrations + streams mode.
- `15`
- `1`
---
- `REDIS_PUBSUB_BUFFER_SIZE`
- Message buffer size
- `100`
- `1000`
---
- `REDIS_PUBSUB_QUERY_TIMEOUT`
- Timeout for Query operations
- `5s`
- `30s`
---
- `REDIS_PUBSUB_QUERY_LIMIT`
- Message limit for Query operations
- `10`
- `50`
{% /table %}
For Redis with TLS:
```shell
docker run -d \
--name redis \
-p 6379:6379 \
-v /path/to/certs:/tls \
redis:7-alpine redis-server \
--tls-port 6380 \
--port 0 \
--tls-cert-file /tls/redis.crt \
--tls-key-file /tls/redis.key \
--tls-ca-cert-file /tls/ca.crt
```
> **Note**: Topics are auto-created on first publish. When using GoFr migrations with Streams mode, keep `REDIS_DB` and `REDIS_PUBSUB_DB` separate (defaults: 0 and 15). For `REDIS_STREAMS_BLOCK_TIMEOUT`: use 1s-2s for real-time or 10s-30s for batch processing.
### Azure Event Hubs
GoFr supports Event Hubs starting gofr version v1.22.0.
While subscribing gofr reads from all the partitions of the consumer group provided in the configuration reducing hassle to manage them.
#### Setup
Azure Event Hubs is supported as an external PubSub provider such that if you are not using it, it doesn't get added in your binary.
Import the external driver for `eventhub` using the following command.
```bash
go get gofr.dev/pkg/gofr/datasource/pubsub/eventhub
```
Use the `AddPubSub` method of GoFr's app to connect
**Example**
```go
app := gofr.New()
app.AddPubSub(eventhub.New(eventhub.Config{
ConnectionString: "Endpoint=sb://gofr-dev.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=",
ContainerConnectionString: "DefaultEndpointsProtocol=https;AccountName=gofrdev;AccountKey=;EndpointSuffix=core.windows.net",
StorageServiceURL: "https://gofrdev.windows.net/",
StorageContainerName: "test",
EventhubName: "test1",
ConsumerGroup: "$Default",
}))
```
While subscribing/publishing from Event Hubs make sure to keep the topic-name same as event-hub name.
#### Configs
1. To set up Azure Event Hubs refer the following [documentation](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-create).
2. As GoFr manages reading from all the partitions it needs to store the information about what has been read and what is left for that GoFr uses Azure Container which can be setup from the following [documentation](https://learn.microsoft.com/en-us/azure/storage/blobs/blob-containers-portal).
##### Mandatory Configs Configuration Map
{% table %}
- ConnectionString
- [connection-string-primary-key](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-get-connection-string)
---
- ContainerConnectionString
- [ConnectionString](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-keys-manage?toc=%2Fazure%2Fstorage%2Fblobs%2Ftoc.json&bc=%2Fazure%2Fstorage%2Fblobs%2Fbreadcrumb%2Ftoc.json&tabs=azure-portal#view-account-access-keys)
---
- StorageServiceURL
- [Blob Service URL](https://learn.microsoft.com/en-us/azure/storage/common/storage-account-get-info?tabs=portal#get-service-endpoints-for-the-storage-account)
---
- StorageContainerName
- [Container Name](https://learn.microsoft.com/en-us/azure/storage/blobs/blob-containers-portal#create-a-container)
---
- EventhubName
- [Eventhub](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-create#create-an-event-hub)
{% /table %}
### Amazon SQS
GoFr supports Amazon Simple Queue Service (SQS) as an external PubSub provider. SQS is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications.
#### Setup
Import the external driver for `sqs` using the following command.
```bash
go get gofr.dev/pkg/gofr/datasource/pubsub/sqs
```
Use the `AddPubSub` method of GoFr's app to connect.
**Example**
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/pubsub/sqs"
)
func main() {
app := gofr.New()
app.AddPubSub(sqs.New(&sqs.Config{
Region: "us-east-1",
AccessKeyID: "your-access-key-id", // optional if using IAM roles
SecretAccessKey: "your-secret-access-key", // optional if using IAM roles
// Endpoint: "http://localhost:4566", // optional: for LocalStack
}))
app.Run()
}
```
> **Note**: When using IAM roles (e.g., on EC2 or ECS), you can omit `AccessKeyID` and `SecretAccessKey`. The SDK will automatically use the instance's IAM role credentials.
#### Configs
{% table %}
- Name
- Description
- Required
- Default
- Example
---
- `Region`
- AWS region where the SQS queue is located.
- Yes
- -
- `us-east-1`
---
- `AccessKeyID`
- AWS access key ID for authentication.
- No
- Uses default credential chain
- `AKIAIOSFODNN7EXAMPLE`
---
- `SecretAccessKey`
- AWS secret access key for authentication.
- No
- Uses default credential chain
- `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`
---
- `SessionToken`
- AWS session token for temporary credentials.
- No
- -
- `FwoGZXIvYXdzE...`
---
- `Endpoint`
- Custom endpoint URL for SQS. Useful for local development with LocalStack.
- No
- AWS default endpoint
- `http://localhost:4566`
{% /table %}
> **Note**: SQS queues must be created before publishing or subscribing. Use AWS CLI, AWS Console, or the `CreateTopic` method in migrations to create queues programmatically. GoFr supports Standard Queues by default—FIFO queues are not currently supported. Advanced features like Dead Letter Queues (DLQ) and Broadcast (SNS) can be configured at the infrastructure level.
## Subscribing
Adding a subscriber is similar to adding an HTTP handler, which makes it easier to develop scalable applications,
as it decoupled from the Sender/Publisher.
Users can define a subscriber handler and do the message processing and
use `app.Subscribe` to inject the handler into the application.
This is inversion of control pattern, which lets the control stay with the framework and eases the development
and debugging process.
The subscriber handler has the following signature.
```go
func (ctx *gofr.Context) error
```
`Subscribe` method of GoFr App will continuously read a message from the configured `PUBSUB_BACKEND` which
can be `KAFKA`, `GOOGLE`, `MQTT`, `NATS`, `REDIS`, or `AZURE_EVENTHUB`. These can be configured in the configs folder under `.env`
> The returned error determines which messages are to be committed and which ones are to be consumed again.
```go
// First argument is the `topic name` followed by a handler which would process the
// published messages continuously and asynchronously.
app.Subscribe("order-status", func(ctx *gofr.Context)error{
// Handle the pub-sub message here
})
```
The context `ctx` provides user with the following methods:
* `Bind()` - Binds the message value to a given data type. Message can be converted to `struct`, `map[string]any`, `int`, `bool`, `float64` and `string` types.
* `Param(p string)/PathParam(p string)` - Returns the topic when the same is passed as param.
### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.Subscribe("order-status", func(c *gofr.Context) error {
var orderStatus struct {
OrderId string `json:"orderId"`
Status string `json:"status"`
}
err := c.Bind(&orderStatus)
if err != nil {
c.Logger.Error(err)
// returning nil here as we would like to ignore the
// incompatible message and continue reading forward
return nil
}
c.Logger.Info("Received order ", orderStatus)
return nil
})
app.Run()
}
```
## Publishing
The publishing of message is advised to done at the point where the message is being generated.
To facilitate this, user can access the publishing interface from `gofr Context(ctx)` to publish messages.
```go
ctx.GetPublisher().Publish(ctx, "topic", msg)
```
Users can provide the topic to which the message is to be published.
GoFr also supports multiple topic publishing.
This is beneficial as applications may need to send multiple kinds of messages in multiple topics.
### Example
```go
package main
import (
"encoding/json"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.POST("/publish-order", order)
app.Run()
}
func order(ctx *gofr.Context) (any, error) {
type orderStatus struct {
OrderId string `json:"orderId"`
Status string `json:"status"`
}
var data orderStatus
err := ctx.Bind(&data)
if err != nil {
return nil, err
}
msg, _ := json.Marshal(data)
err = ctx.GetPublisher().Publish(ctx, "order-logs", msg)
if err != nil {
return nil, err
}
return "Published", nil
}
```
> #### Check out the following examples on how to publish/subscribe to given topics:
> ##### [Subscribing Topics](https://github.com/gofr-dev/gofr/blob/main/examples/using-subscriber/main.go)
> ##### [Publishing Topics](https://github.com/gofr-dev/gofr/blob/main/examples/using-publisher/main.go)
================================================
FILE: docs/advanced-guide/websocket/page.md
================================================
# Websockets
WebSockets provide a full-duplex communication channel over a single, long-lived connection, making them ideal for
real-time applications like chat, notifications, and live updates. GoFr provides a convenient way to integrate websockets
into your application. By leveraging GoFr's WebSocket support and customizable upgrader options,
users can efficiently manage real-time communication in your applications.
## Usage in GoFr
Here is a simple example to set up a WebSocket server in GoFr:
```go
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.WebSocket("/ws", WSHandler)
app.Run()
}
func WSHandler(ctx *gofr.Context) (any, error) {
var message string
err := ctx.Bind(&message)
if err != nil {
ctx.Logger.Errorf("Error binding message: %v", err)
return nil, err
}
ctx.Logger.Infof("Received message: %s", message)
return message, nil
}
```
## Configuration Options
GoFr allows us to customize the WebSocket upgrader with several options. We can set these options using the
`websocket.NewWSUpgrader` function. Here is the list of options we can apply to your websocket upgrader using GoFr.
- `HandshakeTimeout (WithHandshakeTimeout)`: Sets the handshake timeout.
- `ReadBufferSize (WithReadBufferSize)`: Sets the size of the read buffer.
- `WriteBufferSize (WithWriteBufferSize)`: Sets the size of the write buffer.
- `Subprotocols (WithSubprotocols)`: Sets the supported sub-protocols.
- `Error (WithError)`: Sets a custom error handler.
- `CheckOrigin (WithCheckOrigin)`: Sets a custom origin check function.
- `Compression (WithCompression)`: Enables compression.
## Writing Messages
GoFr provides the `WriteMessageToSocket` method to send messages to the underlying websocket connection in a thread-safe way. The data parameter can be a string, []byte, or any struct that can be marshaled to JSON.
## Example:
We can configure the Upgrader by creating a chain of option functions provided by GoFr.
```go
package main
import (
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/websocket"
)
func main() {
app := gofr.New()
wsUpgrader := websocket.NewWSUpgrader(
websocket.WithHandshakeTimeout(5*time.Second), // Set handshake timeout
websocket.WithReadBufferSize(2048), // Set read buffer size
websocket.WithWriteBufferSize(2048), // Set write buffer size
websocket.WithSubprotocols("chat", "binary"), // Specify subprotocols
websocket.WithCompression(), // Enable compression
)
app.OverrideWebsocketUpgrader(wsUpgrader)
app.WebSocket("/ws", WSHandler)
app.Run()
}
func WSHandler(ctx *gofr.Context) (any, error) {
var message string
err := ctx.Bind(&message)
if err != nil {
ctx.Logger.Errorf("Error binding message: %v", err)
return nil, err
}
ctx.Logger.Infof("Received message: %s", message)
err = ctx.WriteMessageToSocket("Hello! GoFr")
if err != nil {
return nil, err
}
return message, nil
}
```
> #### Check out the example on how to read/write through a WebSocket in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/blob/main/examples/using-web-socket/main.go)
## Inter-Service WebSocket Communication
GoFr also supports Inter-Service WebSocket Communication, enabling seamless communication between services using WebSocket connections.
This feature is particularly useful for microservices architectures where services need to exchange real-time data.
## Key Methods:
1. **AddWSService**
This method registers a WebSocket service and establishes a persistent connection to the specified service. It also supports automatic reconnection in case of connection failures.
**Parameters:**
- `serviceName (string)`: A unique name for the WebSocket service.
- `url (string)`: The WebSocket URL of the target service.
- `headers ( map[string][]string)`: HTTP headers to include in the WebSocket handshake.
- `enableReconnection (bool)`: A boolean to enable automatic reconnection.
- `retryInterval (time.Duration)`: The interval between reconnection attempts.
2. **WriteMessageToService**
This method sends a message to a WebSocket connection associated with a specific service.
**Parameters:**
- `serviceName (string)`: The name of the WebSocket service.
- `data (any)`: The message to send. It can be a string, []byte, or any struct that can be marshaled to JSON.
## Usage in GoFr
```go
package main
import (
"time"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Add a WebSocket service
err := app.AddWSService("notification-service", "ws://notifications.example.com/ws", nil, true, 5*time.Second)
if err != nil {
app.Logger.Errorf("Failed to add WebSocket service: %v", err)
return
}
// Example route to send a message to the notification service
app.POST("/send-notification", func(ctx *gofr.Context) (any, error) {
message := map[string]string{
"title": "New Message",
"content": "You have a new notification!",
}
err := ctx.WriteMessageToService("notification-service", message)
if err != nil {
ctx.Logger.Errorf("Failed to send message: %v", err)
return nil, err
}
return "Notification sent successfully!", nil
})
app.Run()
}
```
================================================
FILE: docs/datasources/arangodb/page.md
================================================
# ArangoDB
## Configuration
To connect to `ArangoDB`, you need to provide the following environment variables:
- `HOST`: The hostname or IP address of your `ArangoDB` server.
- `USER`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
- `PORT`: The port number
## Setup
GoFr supports injecting `ArangoDB` that implements the following interface. Any driver that implements the interface can be
added using the `app.AddArangoDB()` method, and users can use ArangoDB across the application with `gofr.Context`.
```go
type ArangoDB interface {
// CreateDB creates a new database in ArangoDB.
CreateDB(ctx context.Context, database string) error
// DropDB deletes an existing database in ArangoDB.
DropDB(ctx context.Context, database string) error
// CreateCollection creates a new collection in a database with specified type.
CreateCollection(ctx context.Context, database, collection string, isEdge bool) error
// DropCollection deletes an existing collection from a database.
DropCollection(ctx context.Context, database, collection string) error
// CreateGraph creates a new graph in a database.
CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error
// DropGraph deletes an existing graph from a database.
DropGraph(ctx context.Context, database, graph string) error
// CreateDocument creates a new document in the specified collection.
CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error)
// GetDocument retrieves a document by its ID from the specified collection.
GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error
// UpdateDocument updates an existing document in the specified collection.
UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error
// DeleteDocument deletes a document by its ID from the specified collection.
DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error
// GetEdges retrieves all the edge documents connected to a specific vertex in an ArangoDB graph.
GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string, resp any) error
// Query executes an AQL query and binds the results
Query(ctx context.Context, dbName string, query string, bindVars map[string]any, result any, options ...map[string]any) error
HealthCheck(context.Context) (any, error)
}
```
Users can easily inject a driver that supports this interface, providing usability without compromising the extensibility to use multiple databases.
Import the GoFr's external driver for ArangoDB:
```shell
go get gofr.dev/pkg/gofr/datasource/arangodb@latest
```
## Example
```go
package main
import (
"fmt"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/arangodb"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
app := gofr.New()
// Configure the ArangoDB client
arangoClient := arangodb.New(arangodb.Config{
Host: app.Config.Get("HOST"),
User: app.Config.Get("USER"),
Password: app.Config.Get("PASSWORD"),
Port: app.Config.Get("PORT"),
})
app.AddArangoDB(arangoClient)
// Example routes demonstrating different types of operations
app.POST("/setup", Setup)
app.POST("/users/{name}", CreateUserHandler)
app.POST("/friends", CreateFriendship)
app.GET("/friends/{collection}/{vertexID}", GetEdgesHandler)
app.Run()
}
// Setup demonstrates database and collection creation
func Setup(ctx *gofr.Context) (any, error) {
_, err := ctx.ArangoDB.CreateDocument(ctx, "social_network", "", nil)
if err != nil {
return nil, fmt.Errorf("failed to create database: %w", err)
}
if err := createCollection(ctx, "social_network", "persons"); err != nil {
return nil, err
}
if err := createCollection(ctx, "social_network", "friendships"); err != nil {
return nil, err
}
// Define and create the graph
edgeDefs := arangodb.EdgeDefinition{
{Collection: "friendships", From: []string{"persons"}, To: []string{"persons"}},
}
_, err = ctx.ArangoDB.CreateDocument(ctx, "social_network", "social_graph", edgeDefs)
if err != nil {
return nil, fmt.Errorf("failed to create graph: %w", err)
}
return "Setup completed successfully", nil
}
// Helper function to create collections
func createCollection(ctx *gofr.Context, dbName, collectionName string) error {
_, err := ctx.ArangoDB.CreateDocument(ctx, dbName, collectionName, nil)
if err != nil {
return fmt.Errorf("failed to create collection %s: %w", collectionName, err)
}
return nil
}
// CreateUserHandler demonstrates user management and document creation
func CreateUserHandler(ctx *gofr.Context) (any, error) {
name := ctx.PathParam("name")
// Create a person document
person := Person{
Name: name,
Age: 25,
}
docID, err := ctx.ArangoDB.CreateDocument(ctx, "social_network", "persons", person)
if err != nil {
return nil, fmt.Errorf("failed to create person document: %w", err)
}
return map[string]string{
"message": "User created successfully",
"docID": docID,
}, nil
}
// CreateFriendship demonstrates edge document creation
func CreateFriendship(ctx *gofr.Context) (any, error) {
var req struct {
From string `json:"from"`
To string `json:"to"`
StartDate string `json:"startDate"`
}
if err := ctx.Bind(&req); err != nil {
return nil, err
}
edgeDocument := map[string]any{
"_from": fmt.Sprintf("persons/%s", req.From),
"_to": fmt.Sprintf("persons/%s", req.To),
"startDate": req.StartDate,
}
// Create an edge document for the friendship
edgeID, err := ctx.ArangoDB.CreateDocument(ctx, "social_network", "friendships", edgeDocument)
if err != nil {
return nil, fmt.Errorf("failed to create friendship: %w", err)
}
return map[string]string{
"message": "Friendship created successfully",
"edgeID": edgeID,
}, nil
}
// GetEdgesHandler demonstrates fetching edges connected to a vertex
func GetEdgesHandler(ctx *gofr.Context) (any, error) {
collection := ctx.PathParam("collection")
vertexID := ctx.PathParam("vertexID")
fullVertexID := fmt.Sprintf("%s/%s", collection, vertexID)
// Prepare a slice to hold edge details
edges := make(arangodb.EdgeDetails, 0)
// Fetch all edges connected to the given vertex
err := ctx.ArangoDB.GetEdges(ctx, "social_network", "social_graph", "friendships",
fullVertexID, &edges)
if err != nil {
return nil, fmt.Errorf("failed to get edges: %w", err)
}
return map[string]any{
"vertexID": vertexID,
"edges": edges,
}, nil
}
```
================================================
FILE: docs/datasources/cassandra/page.md
================================================
# Cassandra
GoFr supports pluggable Cassandra drivers.
## Configuration
To connect to `Cassandra`, you need to provide the following environment variables:
- `HOSTS`: The hostname or IP address of your Cassandra server.
- `KEYSPACE`: The name of the keyspace (like a database) that holds your tables and defines replication and durability settings.
- `PORT`: The port number
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
## Setup
GoFr defines an interface that specifies the required methods for interacting
with Cassandra. Any driver implementation that adheres to this interface can be integrated into GoFr using the
`app.AddCassandra()` method. This approach promotes flexibility and allows you to choose the Cassandra driver that best
suits your project's needs.
```go
type CassandraWithContext interface {
QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error
ExecWithCtx(ctx context.Context, stmt string, values ...any) error
ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error)
NewBatchWithCtx(ctx context.Context, name string, batchType int) error
Cassandra
CassandraBatchWithContext
}
type CassandraBatchWithContext interface {
BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error
ExecuteBatchWithCtx(ctx context.Context, name string) error
ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error)
}
```
GoFr simplifies Cassandra integration with a well-defined interface. Users can easily implement any driver that adheres
to this interface, fostering a user-friendly experience.
Import the gofr's external driver for Cassandra:
```shell
go get gofr.dev/pkg/gofr/datasource/cassandra@latest
```
### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
cassandraPkg "gofr.dev/pkg/gofr/datasource/cassandra"
)
type Person struct {
ID int `json:"id,omitempty"`
Name string `json:"name"`
Age int `json:"age"`
// db tag specifies the actual column name in the database
State string `json:"state" db:"location"`
}
func main() {
app := gofr.New()
config := cassandraPkg.Config{
Hosts: app.Config.Get("HOSTS"),
Keyspace: app.Config.Get("KEYSPACE"),
Port: app.Config.Get("PORT"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD"),
}
cassandra := cassandraPkg.New(config)
app.AddCassandra(cassandra)
app.POST("/user", func(c *gofr.Context) (any, error) {
person := Person{}
err := c.Bind(&person)
if err != nil {
return nil, err
}
err = c.Cassandra.ExecWithCtx(c, `INSERT INTO persons(id, name, age, location) VALUES(?, ?, ?, ?)`,
person.ID, person.Name, person.Age, person.State)
if err != nil {
return nil, err
}
return "created", nil
})
app.GET("/user", func(c *gofr.Context) (any, error) {
persons := make([]Person, 0)
err := c.Cassandra.QueryWithCtx(c, &persons, `SELECT id, name, age, location FROM persons`)
return persons, err
})
app.Run()
}
```
================================================
FILE: docs/datasources/clickhouse/page.md
================================================
# ClickHouse
## Configuration
To connect to `ClickHouse`, you need to provide the following environment variables and use it:
- `HOSTS`: The hostname or IP address of your `ClickHouse` server.
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
- `DATABASE`: The name of the database to connect to.
## Setup
GoFr supports injecting ClickHouse that supports the following interface. Any driver that implements the interface can be added
using `app.AddClickhouse()` method, and user's can use ClickHouse across application with `gofr.Context`.
```go
type Clickhouse interface {
Exec(ctx context.Context, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error
}
```
User's can easily inject a driver that supports this interface, this provides usability without
compromising the extensibility to use multiple databases.
Import the gofr's external driver for ClickHouse:
```shell
go get gofr.dev/pkg/gofr/datasource/clickhouse@latest
```
### Example
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/clickhouse"
)
type User struct {
Id string `ch:"id"`
Name string `ch:"name"`
Age int `ch:"age"`
}
func main() {
app := gofr.New()
app.AddClickhouse(clickhouse.New(clickhouse.Config{
Hosts: app.Config.Get("HOSTS"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD"),
Database: app.Config.Get("DATABASE"),
}))
app.POST("/user", Post)
app.GET("/user", Get)
app.Run()
}
func Post(ctx *gofr.Context) (any, error) {
err := ctx.Clickhouse.Exec(ctx, "INSERT INTO users (id, name, age) VALUES (?, ?, ?)", "8f165e2d-feef-416c-95f6-913ce3172e15", "aryan", 10)
if err != nil {
return nil, err
}
return "successfully inserted", nil
}
func Get(ctx *gofr.Context) (any, error) {
var user []User
err := ctx.Clickhouse.Select(ctx, &user, "SELECT * FROM users")
if err != nil {
return nil, err
}
return user, nil
}
```
================================================
FILE: docs/datasources/cockroachdb/page.md
================================================
# CockroachDB
GoFr provides support for CockroachDB, a cloud-native SQL database that is compatible with PostgreSQL.
## Configuration
To connect to CockroachDB, you need to provide the following environment variables:
* `DB_DIALECT`: Set to `cockroachdb`
* `DB_HOST`: The hostname or IP address of your CockroachDB server.
* `DB_PORT`: The port number (default is 26257).
* `DB_USER`: The username for connecting to the database.
* `DB_PASSWORD`: The password for the specified user.
* `DB_NAME`: The name of the database to connect to.
* `DB_SSL_MODE`: SSL mode (e.g., `disable`, `require`). CockroachDB Cloud requires SSL.
## Example
```go
package main
import (
"context"
"gofr.dev/pkg/gofr"
)
func main() {
// Create a new GoFr app
app := gofr.New()
app.GET("/user", GetUser)
app.Run()
}
func GetUser(ctx *gofr.Context)(any, error){
// Example: Performing a simple query
rows, err := ctx.SQL.QueryContext(context.Background(), "SELECT 1")
if err != nil {
return nil, err
}
defer rows.Close()
return "Connection to cockroachDB Successful.", nil
}
```
For more detailed examples and advanced usage, please refer to the [SQL usage guide](/advanced-guide/dealing-with-sql/).
================================================
FILE: docs/datasources/couchbase/page.md
================================================
# Couchbase
## Configuration
To connect to `Couchbase`, you need to provide the following environment variables and use it:
- `HOST`: The hostname or IP address of your Couchbase server.
- `USER`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
- `BUCKET`: Top level container
## Setup
GoFr supports injecting `Couchbase` that implements the following interface. Any driver that implements the interface can be
added using the `app.AddCouchbase()` method, and users can use Couchbase across the application with `gofr.Context`.
```go
type Couchbase interface {
Get(ctx context.Context, key string, result any) error
Insert(ctx context.Context, key string, document, result any) error
Upsert(ctx context.Context, key string, document any, result any) error
Remove(ctx context.Context, key string) error
Query(ctx context.Context, statement string, params map[string]any, result any) error
AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error
}
```
Users can easily inject a driver that supports this interface, providing usability without compromising the extensibility to use multiple databases.
Don't forget to serup the Couchbase cluster in Couchbase Web Console first. [Follow for more details](https://docs.couchbase.com/server/current/install/getting-started-docker.html#section_jvt_zvj_42b).
To begin using Couchbase in your GoFr application, you need to import the Couchbase datasource package:
```shell
go get gofr.dev/pkg/gofr/datasource/couchbase@latest
```
### Example
Here is an example of how to use the Couchbase datasource in a GoFr application:
```go
package main
import (
"context"
"fmt"
"log"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/couchbase"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
// Create a new GoFr application
a := gofr.New()
// Add the Couchbase datasource to the application
a.AddCouchbase(couchbase.New(&couchbase.Config{
Host: app.Config.Get("HOST"),
User: app.Config.Get("USER"),
Password: app.Config.Get("PASSWORD"),
Bucket: app.Config.Get("BUCKET"),
}))
// Add the routes
a.GET("/users/{id}", getUser)
a.POST("/users", createUser)
a.DELETE("/users/{id}", deleteUser)
// Run the application
a.Run()
}
func getUser(c *gofr.Context) (any, error) {
// Get the user ID from the URL path
id := c.PathParam("id")
// Get the user from Couchbase
var user User
if err := c.Couchbase.Get(c, id, &user); err != nil {
return nil, err
}
return user, nil
}
func createUser(c *gofr.Context) (any, error) {
// Get the user from the request body
var user User
if err := c.Bind(&user); err != nil {
return nil, err
}
// Insert the user into Couchbase
if err := c.Couchbase.Insert(c, user.ID, user, nil); err != nil {
return nil, err
}
return "user created successfully", nil
}
func deleteUser(c *gofr.Context) (any, error) {
// Get the user ID from the URL path
id := c.PathParam("id")
// Remove the user from Couchbase
if err := c.Couchbase.Remove(c, id); err != nil {
return nil, err
}
return "user deleted successfully", nil
}
```
================================================
FILE: docs/datasources/dgraph/page.md
================================================
# Dgraph
## Configuration
To connect to `Dgraph`, you need to provide the following environment variables and use it:
- `HOST`: The hostname or IP address of your Dgraph server.
- `PORT`: The port number.
## Setup
GoFr supports injecting Dgraph with an interface that defines the necessary methods for interacting with the Dgraph
database. Any driver that implements the following interface can be added using the app.AddDgraph() method.
```go
// Dgraph defines the methods for interacting with a Dgraph database.
type Dgraph interface {
// ApplySchema applies or updates the complete database schema.
ApplySchema(ctx context.Context, schema string) error
// AddOrUpdateField atomically creates or updates a single field definition.
AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error
// DropField permanently removes a field/predicate and all its associated data.
DropField(ctx context.Context, fieldName string) error
// Query executes a read-only query in the Dgraph database and returns the result.
Query(ctx context.Context, query string) (any, error)
// QueryWithVars executes a read-only query with variables in the Dgraph database.
QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error)
// Mutate executes a write operation (mutation) in the Dgraph database and returns the result.
Mutate(ctx context.Context, mu any) (any, error)
// Alter applies schema or other changes to the Dgraph database.
Alter(ctx context.Context, op any) error
// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database.
NewTxn() any
// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database.
NewReadOnlyTxn() any
// HealthChecker checks the health of the Dgraph instance.
HealthChecker
}
```
Users can easily inject a driver that supports this interface, allowing for flexibility without compromising usability.
This structure supports both queries and mutations in Dgraph.
Import the gofr's external driver for DGraph:
```shell
go get gofr.dev/pkg/gofr/datasource/dgraph@latest
```
### Example
```go
package main
import (
"encoding/json"
"fmt"
"github.com/dgraph-io/dgo/v210/protos/api"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/dgraph"
)
func main() {
// Create a new application
app := gofr.New()
db := dgraph.New(dgraph.Config{
Host: app.Config.Get("HOST"),
Port: app.Config.Get("PORT"),
})
// Connect to Dgraph running on localhost:9080
app.AddDgraph(db)
// Add routes for Dgraph operations
app.POST("/dgraph", DGraphInsertHandler)
app.GET("/dgraph", DGraphQueryHandler)
// Run the application
app.Run()
}
// DGraphInsertHandler handles POST requests to insert data into Dgraph
func DGraphInsertHandler(c *gofr.Context) (any, error) {
// Example mutation data to insert into Dgraph
mutationData := `
{
"set": [
{
"name": "GoFr Dev"
},
{
"name": "James Doe"
}
]
}
`
// Create an api.Mutation object
mutation := &api.Mutation{
SetJson: []byte(mutationData), // Set the JSON payload
CommitNow: true, // Auto-commit the transaction
}
// Run the mutation in Dgraph
response, err := c.DGraph.Mutate(c, mutation)
if err != nil {
return nil, err
}
return response, nil
}
// DGraphQueryHandler handles GET requests to fetch data from Dgraph
func DGraphQueryHandler(c *gofr.Context) (any, error) {
// A simple query to fetch all persons with a name in Dgraph
response, err := c.DGraph.Query(c, "{ persons(func: has(name)) { uid name } }")
if err != nil {
return nil, err
}
// Cast response to *api.Response (the correct type returned by Dgraph Query)
resp, ok := response.(*api.Response)
if !ok {
return nil, fmt.Errorf("unexpected response type")
}
// Parse the response JSON
var result map[string]any
err = json.Unmarshal(resp.Json, &result)
if err != nil {
return nil, err
}
return result, nil
}
```
================================================
FILE: docs/datasources/elasticsearch/page.md
================================================
# Elasticsearch
## Configuration
To connect to `Elasticsearch`, you need to provide the following environment variables:
- `ADDRESSES`: Set of elasticsearch node URLs that the client will connect to.
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
## Setup
GoFr supports injecting Elasticsearch with an interface that defines the
necessary methods for interacting with Elasticsearch.
Any driver that implements the following interface can be added using
the app.AddElasticsearch() method.
```go
// Elasticsearch defines the methods for interacting with an Elasticsearch database.
type Elasticsearch interface {
// Connect initializes the Elasticsearch client with the provided configuration.
Connect()
// CreateIndex creates an index with specified settings.
CreateIndex(ctx context.Context, index string, settings map[string]any) error
// DeleteIndex removes an index from Elasticsearch.
DeleteIndex(ctx context.Context, index string) error
// IndexDocument creates or replaces a document in the specified index.
IndexDocument(ctx context.Context, index, id string, document any) error
// GetDocument retrieves a document by its ID.
GetDocument(ctx context.Context, index, id string) (map[string]any, error)
// UpdateDocument applies a partial update to an existing document.
UpdateDocument(ctx context.Context, index, id string, update map[string]any) error
// DeleteDocument removes a document from an index.
DeleteDocument(ctx context.Context, index, id string) error
// Search executes a search query against one or more indices.
Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error)
// Bulk executes multiple operations in a single API call.
Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error)
// HealthCheck verifies connectivity to the Elasticsearch cluster.
HealthChecker
}
```
Users can easily inject a driver that supports this interface, allowing for flexibility
without compromising usability. This structure supports all common Elasticsearch
operations including indexing, searching, and document management.
Import the gofr's external driver for Elasticsearch:
```shell
go get gofr.dev/pkg/gofr/datasource/elasticsearch@latest
```
### Example
```go
package main
import (
"context"
"encoding/json"
"net/http"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/elasticsearch"
)
func main() {
// Create a new application
app := gofr.New()
// Create Elasticsearch client with configuration
es := elasticsearch.New(elasticsearch.Config{
Addresses: app.Config.Get("ADDRESSES"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD"),
})
// Add Elasticsearch to the application
app.AddElasticsearch(es)
// Add routes for Elasticsearch operations
app.POST("/documents", CreateDocumentHandler)
app.GET("/documents/{id}", GetDocumentHandler)
app.GET("/search", SearchDocumentsHandler)
// Run the application
app.Run()
}
// CreateDocumentHandler handles POST requests to create documents in Elasticsearch
func CreateDocumentHandler(c *gofr.Context) (any, error) {
// Parse request body
var document map[string]any
if err := json.NewDecoder(c.Request().Body).Decode(&document); err != nil {
return nil, err
}
// Get document ID from request or generate one
id := c.Param("id")
if id == "" {
id = c.Header("X-Document-ID")
}
// Index the document in Elasticsearch
err := c.Elasticsearch.IndexDocument(c, "products", id, document)
if err != nil {
return nil, err
}
return map[string]string{"status": "document created", "id": id}, nil
}
// GetDocumentHandler handles GET requests to retrieve documents from Elasticsearch
func GetDocumentHandler(c *gofr.Context) (any, error) {
// Get document ID from URL parameter
id := c.PathParam("id")
if id == "" {
return nil, gofr.NewError(http.StatusBadRequest, "document ID is required")
}
// Retrieve the document from Elasticsearch
result, err := c.Elasticsearch.GetDocument(c, "products", id)
if err != nil {
return nil, err
}
return result["_source"], nil
}
// SearchDocumentsHandler handles GET requests to search documents in Elasticsearch
func SearchDocumentsHandler(c *gofr.Context) (any, error) {
query := c.Param("q")
// Build search query
searchQuery := map[string]any{
"query": map[string]any{
"multi_match": map[string]any{
"query": query,
"fields": []string{"name", "description"},
},
},
}
// Execute search
result, err := c.Elasticsearch.Search(c, []string{"products"}, searchQuery)
if err != nil {
return nil, err
}
// Process and return search hits
hits := result["hits"].(map[string]any)["hits"].([]any)
documents := make([]map[string]any, len(hits))
for i, hit := range hits {
hitMap := hit.(map[string]any)
documents[i] = hitMap["_source"].(map[string]any)
documents[i]["id"] = hitMap["_id"]
}
return documents, nil
}
```
================================================
FILE: docs/datasources/getting-started/page.md
================================================
# Getting Started
GoFr adopts an interface-driven architecture for datasource integration, providing a consistent way to work with various databases.
Each datasource implements predefined interfaces that define core functionality, enabling you to inject any database client that satisfies these interface contracts.
Users can inject any client that satisfies the base interface defined by GoFr, making it easy to swap out or add new datasources as needed.
Keeping in mind the size of the framework in the final build, it felt counter-productive to keep all the database drivers within
the framework itself. Keeping only the most used MySQL and Redis within the framework, users can now inject databases
in the server that satisfies the base interface defined by GoFr. This helps in reducing the build size and in turn build time
as unnecessary database drivers are not being compiled and added to the build.
> We are planning to provide custom drivers for most common databases, and is in the pipeline for upcoming releases!
## Supported Databases
{% table %}
- Datasource
- Health-Check
- Logs
- Metrics
- Traces
- Version-Migrations
---
- MySQL
- ✅
- ✅
- ✅
- ✅
- ✅
---
- REDIS
- ✅
- ✅
- ✅
- ✅
- ✅
---
- PostgreSQL
- ✅
- ✅
- ✅
- ✅
- ✅
---
- CockroachDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- ArangoDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- BadgerDB
- ✅
- ✅
- ✅
- ✅
-
---
- Cassandra
- ✅
- ✅
- ✅
- ✅
- ✅
---
- ClickHouse
-
- ✅
- ✅
- ✅
- ✅
---
- DGraph
- ✅
- ✅
- ✅
- ✅
- ✅
---
- MongoDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- NATS KV
- ✅
- ✅
- ✅
- ✅
-
---
- OpenTSDB
- ✅
- ✅
-
- ✅
-
---
- ScyllaDB
- ✅
- ✅
- ✅
- ✅
-
---
- Solr
-
- ✅
- ✅
- ✅
-
---
- SQLite
- ✅
- ✅
- ✅
- ✅
- ✅
---
- SurrealDB
- ✅
- ✅
- ✅
- ✅
- ✅
---
- Elasticsearch
- ✅
- ✅
- ✅
- ✅
- ✅
---
================================================
FILE: docs/datasources/influxdb/page.md
================================================
# InfluxDB
GoFr supports injecting InfluxDB using an interface that defines the necessary methods to interact with InfluxDB v2+.
Any driver that implements this interface can be injected via the `app.AddInfluxDB()` method.
---
## Interface
```go
// InfluxDB defines the methods for interacting with an InfluxDB database.
type InfluxDB interface {
CreateOrganization(ctx context.Context, orgName string) (string, error)
DeleteOrganization(ctx context.Context, orgID string) error
ListOrganization(ctx context.Context) (map[string]string, error)
CreateBucket(ctx context.Context, orgID string, bucketName string, retentionPeriod time.Duration) (string, error)
DeleteBucket(ctx context.Context, orgID, bucketID string) error
ListBuckets(ctx context.Context, org string) (map[string]string, error)
Ping(ctx context.Context) (bool, error)
HealthCheck(ctx context.Context) (any, error)
Query(ctx context.Context, org string, fluxQuery string) ([]map[string]any, error)
WritePoints(ctx context.Context, bucket string, org string, points []container.InfluxPoint) error)
}
```
This structure supports all essential InfluxDB operations including organization/bucket management, health checks, and metrics ingestion.
Import the gofr's external driver for influxdb:
```bash
go get gofr.dev/pkg/gofr/datasource/influxdb@latest
```
## Example
```go
package main
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/influxdb"
)
func main() {
// Create a new GoFr application
app := gofr.New()
// Initialize InfluxDB client
client := influxdb.New(influxdb.Config{
Url: "http://localhost:8086",
Username: "admin",
Password: "admin1234",
Token: "",
})
// Add InfluxDB to application context
app.AddInfluxDB(client)
// Sample route
app.GET("/greet", func(ctx *gofr.Context) (any, error) {
return "Hello World!", nil
})
// Ping InfluxDB
ok, err := client.Ping(context.Background())
if err != nil {
app.Logger().Debug(err)
return
}
app.Logger().Debug("InfluxDB connected: ", ok)
// Create organization
orgID, err := client.CreateOrganization(context.Background(), "demo-org")
if err != nil {
app.Logger().Debug(err)
return
}
// List organizations
orgs, _ := client.ListOrganization(context.Background())
app.Logger().Debug("Organizations: ")
for id, name := range orgs {
app.Logger().Debug(id, name)
}
// Create bucket
bucketID, err := client.CreateBucket(context.Background(), orgID, "demo-bucket")
if err != nil {
app.Logger().Debug(err)
return
}
// List buckets for organization
buckets, err := client.ListBuckets(context.Background(), "demo-org")
if err != nil {
app.Logger().Debug(err)
return
}
app.Logger().Debug("Buckets:", buckets)
// Delete bucket
if err := client.DeleteBucket(context.Background(), bucketID); err != nil {
app.Logger().Debug(err)
return
}
app.Logger().Debug("Bucket deleted successfully")
// Delete organization
if err := client.DeleteOrganization(context.Background(), orgID); err != nil {
app.Logger().Debug(err)
return
}
app.Logger().Debug("Organization deleted successfully")
// Start the server
app.Run()
}
```
================================================
FILE: docs/datasources/migrations/elasticsearch/page.md
================================================
# Elasticsearch Migrations
Elasticsearch migrations in **GoFr** let you manage index schemas, mappings, settings and data in a *version-controlled* manner.
This guide explains how to implement and operate these migrations without breaking production.
## Overview
Elasticsearch migrations help you:
- Create and manage indices with proper mappings
- Update index settings and configurations
- Seed initial data or migrate existing data
- Perform bulk operations efficiently
- Maintain schema consistency across environments
## Migration Tracking
GoFr automatically creates a `gofr_migrations` index in Elasticsearch to track applied migrations.
The index stores:
- Migration version (timestamp)
- Execution method (UP)
- Start time and duration
- Migration status
## Basic Migration Structure
```go
package main
import (
"context"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/elasticsearch"
"gofr.dev/pkg/gofr/migration"
)
func main() {
app := gofr.New()
// Configure Elasticsearch
esClient := elasticsearch.New(elasticsearch.Config{
Addresses: []string{"http://localhost:9200"},
})
app.AddElasticsearch(esClient)
// Define migrations
migrationsMap := map[int64]migration.Migrate{
1640995200: {
UP: func(d migration.Datasource) error {
// Migration logic here
return nil
},
},
}
// Register and run migrations
app.Migrate(migrationsMap)
app.Run()
}
```
## Available Operations
### Index Management
```go
// Create an index with mappings and settings
CreateIndex(ctx context.Context, index string, settings map[string]any) error
// Delete an index
DeleteIndex(ctx context.Context, index string) error
```
### Document Operations
```go
// Index a single document
IndexDocument(ctx context.Context, index, id string, document any) error
// Delete a document by ID
DeleteDocument(ctx context.Context, index, id string) error
// Bulk operations for multiple documents
Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error)
```
## Migration Examples
### 1. Creating an Index with Mappings
```go
1640995200: {
UP: func(d migration.Datasource) error {
settings := map[string]any{
"mappings": map[string]any{
"properties": map[string]any{
"title": map[string]any{
"type": "text",
"analyzer": "standard",
},
"price": map[string]any{
"type": "float",
},
"category": map[string]any{
"type": "keyword",
},
"created_at": map[string]any{
"type": "date",
},
"tags": map[string]any{
"type": "keyword",
},
},
},
"settings": map[string]any{
"number_of_shards": 1,
"number_of_replicas": 0,
"analysis": map[string]any{
"analyzer": map[string]any{
"custom_text_analyzer": map[string]any{
"type": "standard",
"stopwords": "_english_",
},
},
},
},
}
return d.Elasticsearch.CreateIndex(context.Background(), "products", settings)
},
},
```
### 2. Seeding Initial Data
```go
1640995300: {
UP: func(d migration.Datasource) error {
// Create sample products
products := []map[string]any{
{
"title": "Laptop",
"price": 999.99,
"category": "electronics",
"created_at": "2024-01-01T00:00:00Z",
"tags": []string{"computer", "portable"},
},
{
"title": "Coffee Mug",
"price": 12.99,
"category": "kitchen",
"created_at": "2024-01-01T00:00:00Z",
"tags": []string{"ceramic", "drink"},
},
}
ctx := context.Background()
for i, product := range products {
err := d.Elasticsearch.IndexDocument(
ctx,
"products",
fmt.Sprintf("%d", i+1),
product,
)
if err != nil {
return fmt.Errorf("failed to index product %d: %w", i+1, err)
}
}
return nil
},
},
```
### 3. Bulk Operations Migration
```go
1640995400: {
UP: func(d migration.Datasource) error {
// Bulk index multiple documents efficiently
operations := []map[string]any{
// Index operation metadata
{
"index": map[string]any{
"_index": "products",
"_id": "bulk_1",
},
},
// Document data
{
"title": "Bulk Product 1",
"price": 19.99,
"category": "bulk",
},
// Another index operation
{
"index": map[string]any{
"_index": "products",
"_id": "bulk_2",
},
},
// Document data
{
"title": "Bulk Product 2",
"price": 29.99,
"category": "bulk",
},
// Delete operation
{
"delete": map[string]any{
"_index": "products",
"_id": "old_product",
},
},
}
ctx := context.Background()
result, err := d.Elasticsearch.Bulk(ctx, operations)
if err != nil {
return fmt.Errorf("bulk operation failed: %w", err)
}
// Check for errors in bulk response
if errors, ok := result["errors"].(bool); ok && errors {
return fmt.Errorf("bulk operation had errors: %v", result)
}
return nil
},
},
```
### 4. Index Settings Update
```go
1640995500: {
UP: func(d migration.Datasource) error {
// Create a new index with updated settings
settings := map[string]any{
"mappings": map[string]any{
"properties": map[string]any{
"title": map[string]any{
"type": "text",
"analyzer": "custom_text_analyzer",
},
"description": map[string]any{
"type": "text",
"analyzer": "standard",
},
"price": map[string]any{
"type": "float",
},
},
},
"settings": map[string]any{
"number_of_shards": 2, // Increased shards
"number_of_replicas": 1, // Added replica
"refresh_interval": "30s",
},
}
return d.Elasticsearch.CreateIndex(context.Background(), "products_v2", settings)
},
},
```
### 5. Data Migration Between Indices
```go
1640995600: {
UP: func(d migration.Datasource) error {
ctx := context.Background()
// This would typically involve:
// 1. Reading data from old index (using Search - not shown in interface yet)
// 2. Transforming data if needed
// 3. Bulk indexing to new index
// 4. Deleting old index
// For now, we'll create the new index structure
newSettings := map[string]any{
"mappings": map[string]any{
"properties": map[string]any{
"product_name": map[string]any{ // Renamed from 'title'
"type": "text",
},
"product_price": map[string]any{ // Renamed from 'price'
"type": "float",
},
"product_category": map[string]any{ // Renamed from 'category'
"type": "keyword",
},
},
},
}
err := d.Elasticsearch.CreateIndex(ctx, "products_new_schema", newSettings)
if err != nil {
return fmt.Errorf("failed to create new schema index: %w", err)
}
// Clean up old index
return d.Elasticsearch.DeleteIndex(ctx, "products_old")
},
},
```
## Bulk Operations Format
### Index Operation
```go
{
"index": map[string]any{
"_index": "index_name",
"_id": "document_id",
},
}
// Followed by document data
{
"field1": "value1",
"field2": "value2",
}
```
### Update Operation
```go
{
"update": map[string]any{
"_index": "index_name",
"_id": "document_id",
},
}
// Followed by update data
{
"doc": map[string]any{
"field1": "new_value1",
},
}
```
### Delete Operation
```go
{
"delete": map[string]any{
"_index": "index_name",
"_id": "document_id",
},
}
// No document data needed for delete
```
## Best Practices
### 1. Index Naming
- Use descriptive names: `users`, `products`, `orders`
- Consider versioning: `products_v1`, `products_v2`
- Use consistent naming conventions
### 2. Mapping Design
- Define explicit mappings rather than relying on dynamic mapping
- Choose appropriate field types
- Consider analyzer requirements for text fields
- Plan for future field additions
### 3. Settings Configuration
- Set appropriate shard and replica counts
- Configure refresh intervals based on use case
- Set up custom analyzers if needed
### 4. Migration Safety
- Test migrations on non-production data first
- Use bulk operations for large data sets
- Implement proper error handling
- Consider index aliases for zero-downtime migrations
### 5. Performance Considerations
- Use bulk operations for multiple documents
- Batch operations appropriately (1 000 – 5 000 docs per batch)
- Monitor cluster health during migrations
- Consider disabling replicas during large data migrations
## Error Handling
```go
UP: func(d migration.Datasource) error {
ctx := context.Background()
// Check if index already exists (idempotent migration)
settings := map[string]any{
"mappings": map[string]any{
"properties": map[string]any{
"name": map[string]any{"type": "text"},
},
},
}
err := d.Elasticsearch.CreateIndex(ctx, "users", settings)
if err != nil {
// Handle specific Elasticsearch errors
if strings.Contains(err.Error(), "resource_already_exists_exception") {
// Index already exists, this is okay
return nil
}
return fmt.Errorf("failed to create users index: %w", err)
}
return nil
},
```
## Monitoring Migration Logs
```plaintext
INFO [15:09:13] running migration 1640995200
DEBU [15:09:13] CREATE INDEX products ELASTIC 215759µs products {"mappings":{"properties":{"price":{"type":"float"},"title":{"type":"text"}}},"settings":{"number_of_replicas":0,"number_of_shards":1}}
DEBU [15:09:13] INDEX DOCUMENT products/1 ELASTIC 87374µs 1 {"price":19.99,"title":"Sample Product"}
```
The logs show:
- **Operation type** – CREATE INDEX, INDEX DOCUMENT, BULK, etc.
- **Execution time** – In microseconds
- **Target** – Index name, document ID
- **Query/Data** – Full JSON of the operation (no base64 encoding)
## Complete Example
```go
package main
import (
"context"
"fmt"
"os"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/elasticsearch"
"gofr.dev/pkg/gofr/migration"
)
func main() {
app := gofr.New()
// Configure Elasticsearch
esURL := os.Getenv("ELASTICSEARCH_URL")
if esURL == "" {
esURL = "http://localhost:9200"
}
esClient := elasticsearch.New(elasticsearch.Config{
Addresses: []string{esURL},
})
app.AddElasticsearch(esClient)
// Define migrations
migrationsMap := map[int64]migration.Migrate{
// Create users index
1640995200: {
UP: func(d migration.Datasource) error {
settings := map[string]any{
"mappings": map[string]any{
"properties": map[string]any{
"name": map[string]any{"type": "keyword"},
"email": map[string]any{"type": "keyword"},
"age": map[string]any{"type": "integer"},
},
},
}
return d.Elasticsearch.CreateIndex(context.Background(), "users", settings)
},
},
// Seed initial users
1640995300: {
UP: func(d migration.Datasource) error {
users := []map[string]any{
{"name": "Alice", "email": "alice@example.com", "age": 30},
{"name": "Bob", "email": "bob@example.com", "age": 25},
}
ctx := context.Background()
for i, user := range users {
err := d.Elasticsearch.IndexDocument(
ctx, "users", fmt.Sprintf("%d", i+1), user,
)
if err != nil {
return err
}
}
return nil
},
},
// Bulk add more users
1640995400: {
UP: func(d migration.Datasource) error {
operations := []map[string]any{
{"index": map[string]any{"_index": "users", "_id": "3"}},
{"name": "Carol", "email": "carol@example.com", "age": 28},
{"index": map[string]any{"_index": "users", "_id": "4"}},
{"name": "David", "email": "david@example.com", "age": 35},
}
_, err := d.Elasticsearch.Bulk(context.Background(), operations)
return err
},
},
}
// Run migrations
app.Migrate(migrationsMap)
// Add API endpoints
app.GET("/users", getUsersHandler)
app.Run()
}
func getUsersHandler(ctx *gofr.Context) (any, error) {
query := map[string]any{
"query": map[string]any{"match_all": map[string]any{}},
"size": 10,
}
result, err := ctx.Container.Elasticsearch.Search(
ctx.Context, []string{"users"}, query,
)
if err != nil {
return nil, err
}
return result, nil
}
```
**Enjoy consistent, version-controlled Elasticsearch migrations with GoFr!**
================================================
FILE: docs/datasources/mongodb/page.md
================================================
# MongoDB
## Configuration
To connect to `MongoDB`, you need to provide the following environment variables:
- `URI`: Mongodb server URL that the client connects to.
- `DATABASE`: The name of the database to connect to.
- `CONNECTIONTIMEOUT`: The maximum time the client will wait while trying to establish a connection.
## Setup
GoFr supports injecting MongoDB that supports the following interface. Any driver that implements the interface can be added
using `app.AddMongo()` method, and users can use MongoDB across application with `gofr.Context`.
```go
type Mongo interface {
Find(ctx context.Context, collection string, filter any, results any) error
FindOne(ctx context.Context, collection string, filter any, result any) error
InsertOne(ctx context.Context, collection string, document any) (any, error)
InsertMany(ctx context.Context, collection string, documents []any) ([]any, error)
DeleteOne(ctx context.Context, collection string, filter any) (int64, error)
DeleteMany(ctx context.Context, collection string, filter any) (int64, error)
UpdateByID(ctx context.Context, collection string, id any, update any) (int64, error)
UpdateOne(ctx context.Context, collection string, filter any, update any) error
UpdateMany(ctx context.Context, collection string, filter any, update any) (int64, error)
CountDocuments(ctx context.Context, collection string, filter any) (int64, error)
Drop(ctx context.Context, collection string) error
}
```
Users can easily inject a driver that supports this interface; this provides usability without compromising the extensibility to use multiple databases.
Import the gofr's external driver for MongoDB:
```shell
go get gofr.dev/pkg/gofr/datasource/mongo@latest
```
### Example
```go
package main
import (
"time"
"go.mongodb.org/mongo-driver/bson"
"gofr.dev/pkg/gofr/datasource/mongo"
"gofr.dev/pkg/gofr"
)
type Person struct {
Name string `bson:"name" json:"name"`
Age int `bson:"age" json:"age"`
City string `bson:"city" json:"city"`
}
func main() {
app := gofr.New()
db := mongo.New(mongo.Config{URI: app.Config.Get("URI"), Database: app.Config.Get("DATABASE"), ConnectionTimeout: app.Config.Get("CONNECTIONTIMEOUT")})
// inject the mongo into gofr to use mongoDB across the application
// using gofr context
app.AddMongo(db)
app.POST("/mongo", Insert)
app.GET("/mongo/{name}", Get)
app.Run()
}
func Insert(ctx *gofr.Context) (any, error) {
var p Person
err := ctx.Bind(&p)
if err != nil {
return nil, err
}
res, err := ctx.Mongo.InsertOne(ctx, "collection", p)
if err != nil {
return nil, err
}
return res, nil
}
func Get(ctx *gofr.Context) (any, error) {
var result Person
p := ctx.PathParam("name")
err := ctx.Mongo.FindOne(ctx, "collection", bson.D{{"name", p}} /* valid filter */, &result)
if err != nil {
return nil, err
}
return result, nil
}
```
================================================
FILE: docs/datasources/opentsdb/page.md
================================================
# OpenTSDB
## Configuration
To connect to `OpenTSDB`, you need to provide the following environment variables:
- `HOSTS`: The hostname or IP address of your OpenTSDB server.
- `MAXCONTENTLENGTH`: Max length of the request body in bytes.
- `MAXPUTPOINTSNUM`: Max number of data points that can be sent in a single `PUT` request.
- `DETECTDELTANUM`: The number of data points that OpenTSDB looks at to spot unusual time gaps.
## Setup
GoFr supports injecting OpenTSDB to facilitate interaction with OpenTSDB's REST APIs.
Implementations adhering to the `OpenTSDB` interface can be registered with `app.AddOpenTSDB()`,
enabling applications to leverage OpenTSDB for time-series data management through `gofr.Context`.
```go
// OpenTSDB provides methods for GoFr applications to communicate with OpenTSDB
// through its REST APIs.
type OpenTSDB interface {
// HealthChecker verifies if the OpenTSDB server is reachable.
// Returns an error if the server is unreachable, otherwise nil.
HealthChecker
// PutDataPoints sends data to store metrics in OpenTSDB.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - data: A slice of DataPoint objects; must contain at least one entry.
// - queryParam: Specifies the response format:
// - client.PutRespWithSummary: Requests a summary response.
// - client.PutRespWithDetails: Requests detailed response information.
// - Empty string (""): No additional response details.
//
// - res: A pointer to PutResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
PutDataPoints(ctx context.Context, data any, queryParam string, res any) error
// QueryDataPoints retrieves data based on the specified parameters.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - param: An instance of QueryParam with query parameters for filtering data.
// - res: A pointer to QueryResponse, where the server's response will be stored.
QueryDataPoints(ctx context.Context, param any, res any) error
// QueryLatestDataPoints fetches the latest data point(s).
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - param: An instance of QueryLastParam with query parameters for the latest data point.
// - res: A pointer to QueryLastResponse, where the server's response will be stored.
QueryLatestDataPoints(ctx context.Context, param any, res any) error
// GetAggregators retrieves available aggregation functions.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - res: A pointer to AggregatorsResponse, where the server's response will be stored.
GetAggregators(ctx context.Context, res any) error
// QueryAnnotation retrieves a single annotation.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - queryAnnoParam: A map of parameters for the annotation query, such as client.AnQueryStartTime, client.AnQueryTSUid.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error
// PostAnnotation creates or updates an annotation.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be created or updated.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
PostAnnotation(ctx context.Context, annotation any, res any) error
// PutAnnotation creates or replaces an annotation.
// Fields not included in the request will be reset to default values.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be created or replaced.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
PutAnnotation(ctx context.Context, annotation any, res any) error
// DeleteAnnotation removes an annotation.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be deleted.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
DeleteAnnotation(ctx context.Context, annotation any, res any) error
}
```
Import the gofr's external driver for OpenTSDB:
```go
go get gofr.dev/pkg/gofr/datasource/opentsdb
```
The following example demonstrates injecting an OpenTSDB instance into a GoFr application
and using it to perform a health check on the OpenTSDB server.
```go
package main
import (
"context"
"fmt"
"math/rand/v2"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/opentsdb"
)
func main() {
app := gofr.New()
// Initialize OpenTSDB connection
app.AddOpenTSDB(opentsdb.New(opentsdb.Config{
Host: app.Config.Get("HOST"),
MaxContentLength: app.Config.Get("MAXCONTENTLENGTH"),
MaxPutPointsNum: app.Config.Get("MAXPUTPOINTSNUM"),
DetectDeltaNum: app.Config.Get("DETECTDELTANUM"),
}))
// Register routes
app.GET("/health", opentsdbHealthCheck)
app.POST("/write", writeDataPoints)
app.GET("/query", queryDataPoints)
// Run the app
app.Run()
}
// Health check for OpenTSDB
func opentsdbHealthCheck(c *gofr.Context) (any, error) {
res, err := c.OpenTSDB.HealthCheck(context.Background())
if err != nil {
return nil, err
}
return res, nil
}
// Write Data Points to OpenTSDB
func writeDataPoints(c *gofr.Context) (any, error) {
PutDataPointNum := 4
name := []string{"cpu", "disk", "net", "mem"}
cpuDatas := make([]opentsdb.DataPoint, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for i := 0; i < PutDataPointNum; i++ {
data := opentsdb.DataPoint{
Metric: name[i%len(name)],
Timestamp: time.Now().Unix(),
Value: rand.Float64() * 100,
Tags: tags,
}
cpuDatas = append(cpuDatas, data)
}
resp := opentsdb.PutResponse{}
err := c.OpenTSDB.PutDataPoints(context.Background(), cpuDatas, "details", &resp)
if err != nil {
return resp.Errors, err
}
return fmt.Sprintf("%v Data points written successfully", resp.Success), nil
}
// Query Data Points from OpenTSDB
func queryDataPoints(c *gofr.Context) (any, error) {
st1 := time.Now().Unix() - 3600
st2 := time.Now().Unix()
queryParam := opentsdb.QueryParam{
Start: st1,
End: st2,
}
name := []string{"cpu", "disk", "net", "mem"}
subqueries := make([]opentsdb.SubQuery, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for _, metric := range name {
subQuery := opentsdb.SubQuery{
Aggregator: "sum",
Metric: metric,
Tags: tags,
}
subqueries = append(subqueries, subQuery)
}
queryParam.Queries = subqueries
queryResp := &opentsdb.QueryResponse{}
err := c.OpenTSDB.QueryDataPoints(c, &queryParam, queryResp)
if err != nil {
return nil, err
}
return queryResp.QueryRespCnts, nil
}
```
================================================
FILE: docs/datasources/oracle/page.md
================================================
# OracleDB
## Configuration
To connect to `OracleDB`, you need to provide the following environment variables:
- `HOST`: The hostname or IP address of your OracleDB server.
- `PORT`: The port number.
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
- `SERVICE`: The specific Oracle database instance or service on the server that the client should connect to.
## Setup
GoFr supports injecting OracleDB as a relational datasource through a clean, extensible interface. Any driver that implements the following interface can be added using the `app.AddOracle()` method, and users can access OracleDB throughout their application via `gofr.Context`.
```go
type Oracle interface {
Exec(ctx context.Context, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
}
```
This approach allows users to easily inject any compatible Oracle driver, providing both usability and the flexibility to use multiple databases in a GoFr application.
## ⚠️ Important: Oracle Database Must Exist
**Before running your GoFr application, you must ensure that the Oracle database and the required schema (such as the `users` table) are already created.**
- Oracle does not allow creating a database (PDB or CDB) via a simple SQL query from a standard client connection.
- You must use Oracle tools (like DBCA, SQL\*Plus as SYSDBA, or Docker container initialization) to create the database and pluggable database (PDB) before connecting your app.
- Your application can create tables within an existing schema, but the database itself must be provisioned in advance.
## Setting Up OracleDB with Docker
To help new users, the following steps outline how to quickly set up an OracleDB instance using Docker.
### 1. Prerequisites
- **Docker** installed on your system.
- An **Oracle account** (free) with access to the Oracle Container Registry.
### 2. Create Your Oracle Account
Visit the Oracle Container Registry and create or sign in to your account:
👉 [https://container-registry.oracle.com/ords/f?p=113:10:14574461221664:::::](https://container-registry.oracle.com/ords/f?p=113:10:14574461221664:::::)
### 3. Pull the Oracle Free Database Docker Image
In your terminal:
1. Log in to the Oracle Container Registry using your Oracle account credentials:
```sh
docker login container-registry.oracle.com
```
2. After login, pull the Oracle Free Database image:
```sh
docker pull container-registry.oracle.com/database/free:latest
```
### 4. Run the Oracle Database Container
You can now run the OracleDB container (replace `YourPasswordHere` with a suitable strong password):
```sh
docker run -d --name oracle-free -p 1521:1521 -e ORACLE_PWD=YourPasswordHere container-registry.oracle.com/database/free:latest
```
- The database will be available on port **1521**
- The default Pluggable Database (PDB) is **FREEPDB1**
- The `system` user password is your `ORACLE_PWD`
- The service name for connecting is `FREEPDB1`
You can verify the container is running:
```sh
docker ps
```
### 5. Connect to the Oracle Database
Option 1: Direct SQL\*Plus session from within the container:
```sh
docker exec -it oracle-free sqlplus system/YourPasswordHere@localhost:1521/FREEPDB1
```
Option 2: Open bash shell inside the container and use SQL\*Plus from there:
```sh
docker exec -it oracle-free bash
sqlplus system/YourPasswordHere@localhost:1521/FREEPDB1
```
### 6. Create the `users` Table
Based on the Go struct:
```go
type User struct {
Id string `db:"ID"`
Name string `db:"NAME"`
Age int `db:"AGE"`
}
```
Run the following SQL command in SQL\*Plus:
```sql
CREATE TABLE users (
id VARCHAR2(36) PRIMARY KEY,
name VARCHAR2(100),
age NUMBER
);
```
This will create the required table for the GoFr application to interact with.
### 7. Sample OracleDB Config for GoFr
| Setting | Value |
| :---------- | :----------------- |
| host | `localhost` |
| port | `1521` |
| username | `system` |
| password | `YourPasswordHere` |
| service/SID | `FREEPDB1` |
## Import the GoFr External Driver for OracleDB
```bash
go get gofr.dev/pkg/gofr/datasource/oracle@latest
```
## Example
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/oracle"
)
type User struct {
Id string `db:"ID"`
Name string `db:"NAME"`
Age int `db:"AGE"`
}
func main() {
app := gofr.New()
app.AddOracle(oracle.New(oracle.Config{
Host: app.Config.Get("HOST"),
Port: app.Config.Get("PORT"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD")
Service: app.Config.Get("SERVICE"),
}))
app.POST("/user", Post)
app.GET("/user", Get)
app.Run()
}
func Post(ctx *gofr.Context) (any, error) {
err := ctx.Oracle.Exec(ctx, "INSERT INTO users (id, name, age) VALUES (:1, :2, :3)",
"8f165e2d-feef-416c-95f6-913ce3172e15", "aryan", 10)
if err != nil {
return nil, err
}
return "successfully inserted", nil
}
func Get(ctx *gofr.Context) (any, error) {
var users []map[string]any
err := ctx.Oracle.Select(ctx, &users, "SELECT id, name, age FROM users")
if err != nil {
return nil, err
}
return users, nil
}
```
## Example API Usage
You can create a user and get users using the following commands on the command prompt:
- **Create a user:**
```sh
curl -X POST http://localhost:8000/user
```
- **Get all users:**
```sh
curl http://localhost:8000/user
```
================================================
FILE: docs/datasources/scylladb/page.md
================================================
# ScyllaDB
## Configuration
To connect to `ScyllaDB`, you need to provide the following environment variables:
- `HOST`: The hostname or IP address of your ScyllaDB server.
- `KEYSPACE`: The top level namespace.
- `PORT`: The port number.
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
## Setup
GoFr supports pluggable ScyllaDB drivers. It defines an interface that specifies the required methods for interacting
with ScyllaDB. Any driver implementation that adheres to this interface can be integrated into GoFr using the
`app.AddScyllaDB()` method.
```go
type ScyllaDB interface {
// Query executes a CQL (Cassandra Query Language) query on the ScyllaDB cluster
// and stores the result in the provided destination variable `dest`.
// Accepts pointer to struct or slice as dest parameter for single and multiple
Query(dest any, stmt string, values ...any) error
// QueryWithCtx executes the query with a context and binds the result into dest parameter.
// Accepts pointer to struct or slice as dest parameter for single and multiple rows retrieval respectively.
QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error
// Exec executes a CQL statement (e.g., INSERT, UPDATE, DELETE) on the ScyllaDB cluster without returning any result.
Exec(stmt string, values ...any) error
// ExecWithCtx executes a CQL statement with the provided context and without returning any result.
ExecWithCtx(ctx context.Context, stmt string, values ...any) error
// ExecCAS executes a lightweight transaction (i.e. an UPDATE or INSERT statement containing an IF clause).
// If the transaction fails because the existing values did not match, the previous values will be stored in dest.
// Returns true if the query is applied otherwise false.
// Returns false and error if any error occur while executing the query.
// Accepts only pointer to struct and built-in types as the dest parameter.
ExecCAS(dest any, stmt string, values ...any) (bool, error)
// NewBatch initializes a new batch operation with the specified name and batch type.
NewBatch(name string, batchType int) error
// NewBatchWithCtx takes context,name and batchtype and return error.
NewBatchWithCtx(_ context.Context, name string, batchType int) error
// BatchQuery executes a batch query in the ScyllaDB cluster with the specified name, statement, and values.
BatchQuery(name, stmt string, values ...any) error
// BatchQueryWithCtx executes a batch query with the provided context.
BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error
// ExecuteBatchWithCtx executes a batch with context and name returns error.
ExecuteBatchWithCtx(ctx context.Context, name string) error
// HealthChecker defines the HealthChecker interface.
HealthChecker
}
```
Import the gofr's external driver for ScyllaDB:
```shell
go get gofr.dev/pkg/gofr/datasource/scylladb
```
```go
package main
import (
"github.com/gocql/gocql"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/scylladb"
"gofr.dev/pkg/gofr/http"
)
type User struct {
ID gocql.UUID `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func main() {
app := gofr.New()
client := scylladb.New(scylladb.Config{
Host: app.Config.Get("HOST"),
Keyspace: app.Config.Get("KEYSPACE"),
Port: app.Config.Get("PORT"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD"),
})
app.AddScyllaDB(client)
app.GET("/users/{id}", getUser)
app.POST("/users", addUser)
app.Run()
}
func addUser(c *gofr.Context) (any, error) {
var newUser User
err := c.Bind(&newUser)
if err != nil {
return nil, err
}
_ = c.ScyllaDB.ExecWithCtx(c, `INSERT INTO users (user_id, username, email) VALUES (?, ?, ?)`, newUser.ID, newUser.Name, newUser.Email)
return newUser, nil
}
func getUser(c *gofr.Context) (any, error) {
var user User
id := c.PathParam("id")
userID, err := gocql.ParseUUID(id)
if err != nil {
c.Logger.Error("Invalid UUID format:", err)
return nil, err
}
err = c.ScyllaDB.QueryWithCtx(c, &user, "SELECT id, name, email FROM users WHERE id = ?", userID)
if err != nil {
c.Logger.Error("Error querying user:", err)
return nil, err
}
return user, nil
}
```
================================================
FILE: docs/datasources/solr/page.md
================================================
# Solr
## Configuration
To connect to `Solr` DB, you need to provide the following environment variables:
- `HOST`: The hostname or IP address of your Solr DB server.
- `PORT`: The port number.
## Setup
GoFr supports injecting Solr database that supports the following interface. Any driver that implements the interface can be added
using `app.AddSolr()` method, and user's can use Solr DB across application with `gofr.Context`.
```go
type Solr interface {
Search(ctx context.Context, collection string, params map[string]any) (any, error)
Create(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Update(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Delete(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Retrieve(ctx context.Context, collection string, params map[string]any) (any, error)
ListFields(ctx context.Context, collection string, params map[string]any) (any, error)
AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
}
```
User's can easily inject a driver that supports this interface, this provides usability
without compromising the extensibility to use multiple databases.
Import the gofr's external driver for Solr:
```shell
go get gofr.dev/pkg/gofr/datasource/solr@latest
```
Note : This datasource package requires the user to create the collection before performing any operations.
While testing the below code create a collection using :
`curl --location 'http://localhost:2020/solr/admin/collections?action=CREATE&name=test&numShards=2&replicationFactor=1&wt=xml'`
```go
package main
import (
"bytes"
"encoding/json"
"errors"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/solr"
)
func main() {
app := gofr.New()
app.AddSolr(solr.New(solr.Config{
Host: app.Config.Get("HOST"),
Port: app.Config.Get("PORT"),
}))
app.POST("/solr", post)
app.GET("/solr", get)
app.Run()
}
type Person struct {
Name string
Age int
}
func post(c *gofr.Context) (any, error) {
p := []Person{{Name: "Srijan", Age: 24}}
body, _ := json.Marshal(p)
resp, err := c.Solr.Create(c, "test", bytes.NewBuffer(body), nil)
if err != nil {
return nil, err
}
return resp, nil
}
func get(c *gofr.Context) (any, error) {
resp, err := c.Solr.Search(c, "test", nil)
if err != nil {
return nil, err
}
res, ok := resp.(solr.Response)
if !ok {
return nil, errors.New("invalid response type")
}
b, _ := json.Marshal(res.Data)
err = json.Unmarshal(b, &Person{})
if err != nil {
return nil, err
}
return resp, nil
}
```
================================================
FILE: docs/datasources/surrealdb/page.md
================================================
# SurrealDB
## Configuration
To connect to `SurrealDB`, you need to provide the following environment variables:
- `HOST`: The hostname or IP address of your SurrealDB server.
- `PORT`: The port number.
- `USERNAME`: The username for connecting to the database.
- `PASSWORD`: The password for the specified user.
- `NAMESPACE`: Top level container in SurrealDB that groups databases.
- `DATABASE`: The name of the database to connect to.
- `TLSENABLED`: TLS mode (e.g., disable, require)
## Setup
GoFr supports injecting SurrealDB database that supports the following interface. Any driver that implements the interface can be added
using `app.AddSurrealDB()` method, and users can use Surreal DB across application through the `gofr.Context`.
```go
// SurrealDB defines an interface representing a SurrealDB client with common database operations.
type SurrealDB interface {
// Query executes a Surreal query with the provided variables and returns the query results as a slice of interfaces{}.
// It returns an error if the query execution fails.
Query(ctx context.Context, query string, vars map[string]any) ([]any, error)
// Create inserts a new record into the specified table and returns the created record as a map.
// It returns an error if the operation fails.
Create(ctx context.Context, table string, data any) (map[string]any, error)
// Update modifies an existing record in the specified table by its ID with the provided data.
// It returns the updated record as an interface and an error if the operation fails.
Update(ctx context.Context, table string, id string, data any) (any, error)
// Delete removes a record from the specified table by its ID.
// It returns the result of the delete operation as an interface and an error if the operation fails.
Delete(ctx context.Context, table string, id string) (any, error)
// Select retrieves all records from the specified table.
// It returns a slice of maps representing the records and an error if the operation fails.
Select(ctx context.Context, table string) ([]map[string]any, error)
HealthChecker
}
// SurrealDBProvider is an interface that extends SurrealDB with additional methods for logging, metrics, or connection management.
// It is typically used for initializing and managing SurrealDB-based data sources.
type SurrealDBProvider interface {
SurrealDB
provider
}
```
Import the gofr's external driver for SurrealDB:
```shell
go get gofr.dev/pkg/gofr/datasource/surrealdb
```
The following example demonstrates injecting an SurrealDB instance into a GoFr application.
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/surrealdb"
)
type Person struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}
type ErrorResponse struct {
Message string `json:"message"`
}
func main() {
app := gofr.New()
client := surrealdb.New(&surrealdb.Config{
Host: app.Config.Get("HOST"),
Port: app.Config.Get("PORT"),
Username: app.Config.Get("USERNAME"),
Password: app.Config.Get("PASSWORD"),
Namespace: app.Config.Get("NAMESPACE"),
Database: app.Config.Get("DATABASE"),
TLSEnabled: app.Config.Get("TLSENABLED"),
})
app.AddSurrealDB(client)
// GET request to fetch person by ID
app.GET("/person/{id}", func(ctx *gofr.Context) (any, error) {
id := ctx.PathParam("id")
query := "SELECT * FROM type::thing('person', $id)"
vars := map[string]any{
"id": id,
}
result, err := ctx.SurrealDB.Query(ctx, query, vars)
if err != nil {
return nil, err
}
return result, nil
})
// POST request to create a new person
app.POST("/person", func(ctx *gofr.Context) (any, error) {
var person Person
if err := ctx.Bind(&person); err != nil {
return ErrorResponse{Message: "Invalid request body"}, nil
}
result, err := ctx.SurrealDB.Create(ctx, "person", map[string]any{
"name": person.Name,
"age": person.Age,
"email": person.Email,
})
if err != nil {
return nil, err
}
return result, nil
})
app.Run()
}
```
================================================
FILE: docs/events.json
================================================
[
{
"date": "23-24th November, 2024",
"title": "GoFr Hackathon",
"description": "GoFr Hackathon was a major success with participants building innovative solutions using the GoFr framework. It was an exciting two-day event where developers and tech enthusiasts came together to collaborate and create.",
"imageSrc": "/events/hackathon.jpg"
},
{
"date": "23-24th October, 2024",
"title": "Open Source India",
"description": "At Open Source India, we had an amazing experience connecting with the open-source community. The event featured various sessions, demos, and showcases on GoFr, highlighting its role in simplifying backend development and its microservice approach.",
"imageSrc": "/events/OpenSourceIndia.jpeg"
},
{
"date": "18-19th October, 2024",
"title": "GopherCon Africa",
"description": "At GopherCon Africa, GoFr stood out with its cutting-edge tools and integration features, making backend development easier. We showcased GoFr's capabilities in working with databases, observability tools, and its seamless HTTP/gRPC support.",
"imageSrc": "/events/gopherconAfrica.jpeg"
},
{
"date": "September 2024",
"title": "GoFr Workshops",
"description": "GoFr workshops are held regularly to engage with developers and tech enthusiasts. We’ve conducted workshops across multiple cities such as IIT BHU, IIIT Sri City, Thapar University, JIIT Noida, NIT Jalandhar, and NIT Nagpur offering hands-on experiences and deep dives into GoFr’s powerful features.",
"imageSrc": "/events/WorkShop.jpeg"
}
]
================================================
FILE: docs/navigation.js
================================================
export const navigation = [
{
title: 'Quick Start Guide',
desc: "Get started with GoFR through our Quick Start Guide. Learn to build scalable applications with easy-to-follow instructions on server setup, database connections, configuration management, and more. Boost your productivity and streamline your development process.",
links: [
{
title: 'Hello Server',
href: '/docs/quick-start/introduction',
desc: "Getting started with how to write a server using GoFR with basic examples and explanations. Boost your productivity with efficient coding practices and learn to build scalable applications quickly."
},
{
title: 'Configuration',
href: '/docs/quick-start/configuration',
desc: "Set up environment variables, manage settings, and streamline your development process."
},
{
title: 'Connecting Redis',
href: '/docs/quick-start/connecting-redis',
desc: "Discover how to connect your GoFR application to Redis for fast in-memory data storage."
},
{
title: 'Connecting MySQL',
href: '/docs/quick-start/connecting-mysql',
desc: "Step-by-step guide on integrating MySQL with your GoFR application. With managed database connections and new methods for increasing your productivity."
},
{
title: 'Observability',
href: '/docs/quick-start/observability',
desc: "Inbuilt logging, tracing, and metrics to enhance reliability and performance."
},
{
title: 'Adding REST Handlers',
href: '/docs/quick-start/add-rest-handlers',
desc: "Fastest way to create CRUD APIs by just providing the entity."
}
],
},
{
title: 'Advanced Guide',
links: [
{
title: "Scheduling Cron Jobs",
href: "/docs/advanced-guide/using-cron",
desc: "Learn how to schedule and manage cron jobs in your application for automated tasks and background processes with GoFr's CRON job management."
},
{
title: 'Overriding Default',
href: '/docs/advanced-guide/overriding-default',
desc: "Understand how to override default configurations and behaviors in GoFr to tailor framework to your specific needs."
},
{
title: 'Remote Log Level Change',
href: '/docs/advanced-guide/remote-log-level-change',
desc: "Discover how to dynamically change log levels remotely, enabling you to adjust logging verbosity without redeploying your application."
},
{
title: 'Publishing Custom Metrics',
href: '/docs/advanced-guide/publishing-custom-metrics',
desc: "Explore methods for publishing custom metrics to monitor your application's performance and gain valuable insights."
},
{
title: 'Custom Headers in Response',
href: '/docs/advanced-guide/setting-custom-response-headers',
desc: "Learn how to include custom headers in HTTP responses to provide additional context and control to your API clients."
},
{
title: 'Custom Spans in Tracing',
href: '/docs/advanced-guide/custom-spans-in-tracing',
desc: "Learn to create custom spans for tracing to enhance observability and analyze the performance of your services."
},
{
title: 'Adding Custom Middleware',
href: '/docs/advanced-guide/middlewares',
desc: "Learn how to add custom middleware to your GoFr application for enhanced functionality and request processing."
},
{
title: 'HTTP Communication',
href: '/docs/advanced-guide/http-communication',
desc: "Get familiar with making HTTP requests and handling responses within your GoFr application to facilitate seamless communication."
},
{
title: 'Authentication',
href: '/docs/advanced-guide/authentication',
desc: "Implement various authentication methods to secure your GoFR application and protect sensitive endpoints across HTTP and gRPC."
},
{
title: 'Role-Based Access Control (RBAC)',
href: '/docs/advanced-guide/rbac',
desc: "Implement comprehensive Role-Based Access Control with support for roles, permissions, hierarchy, JWT integration, and fine-grained permission-based authorization."
},
{
title: 'Circuit Breaker Support',
href: '/docs/advanced-guide/circuit-breaker',
desc: "Understand how to implement circuit breaker patterns to enhance the resilience of your services against failures."
},
{
title: 'Monitoring Service Health',
href: '/docs/advanced-guide/monitoring-service-health',
desc: "Learn to monitor the health of your services effectively, ensuring optimal performance and quick issue resolution."
},
{
title: 'Handling Data Migrations',
href: '/docs/advanced-guide/handling-data-migrations',
desc: "Explore strategies for managing data migrations within your GoFr application to ensure smooth transitions and data integrity."
},
{
title: 'Writing gRPC Server/Client',
href: '/docs/advanced-guide/grpc',
desc: "Step-by-step guide on writing a gRPC server in GoFr to facilitate efficient communication between services."
},
{
title: 'gRPC Streaming',
href: '/docs/advanced-guide/grpc-streaming',
desc: "Learn how to implement server-side, client-side, and bidirectional streaming in GoFr with built-in observability and error handling."
},
{
title: 'Using Pub/Sub',
href: '/docs/advanced-guide/using-publisher-subscriber',
desc: "Discover how to GoFr seamlessly allows to integrate different Pub/Sub systems in your application for effective messaging and event-driven architectures."
},
{
title: 'Key Value Store',
href: '/docs/advanced-guide/key-value-store',
desc: "Explore how to implement and manage a key-value store in your GoFr application for fast and efficient data retrieval. Supports BadgerDB, NATS-KV, and DynamoDB."
},
{
title: 'Dealing with SQL',
href: '/docs/advanced-guide/dealing-with-sql',
desc: "Get insights into best practices for working with SQL databases in GoFr, including query optimization and error handling."
},
{
title: 'Automatic SwaggerUI Rendering',
href: '/docs/advanced-guide/swagger-documentation',
desc: "Learn how to automatically render SwaggerUI documentation for your GoFr APIs, improving discoverability and usability."
},
{
title: 'Adding Synchronous Startup Hooks',
href: '/docs/advanced-guide/startup-hooks',
desc: "Learn how to seed a database, warm up a cache, or perform other critical setup procedures, synchronously before starting your application."
},
{
title: 'Error Handling',
href: '/docs/advanced-guide/gofr-errors',
desc: "Understand error handling mechanisms in GoFr to ensure robust applications and improved user experience."
},
{
title: 'Handling File',
href: '/docs/advanced-guide/handling-file',
desc: "Explore how GoFr enables efficient file handling by abstracting remote and local filestore providers in your Go application. Learn to manage file uploads, downloads, and storage seamlessly, enhancing your application's capability to work with diverse data sources."
},
{
title: 'WebSockets',
href: '/docs/advanced-guide/websocket',
desc: "Explore how GoFr eases the process of WebSocket communication in your Golang application for real-time data exchange."
},
{
title: 'GraphQL',
href: '/docs/advanced-guide/graphql',
desc: 'Learn how to build native GraphQL APIs in GoFr using a schema-first approach with a ./configs/schema.graphqls file and an interactive playground.'
},
{
title: 'Serving-Static Files',
href: '/docs/advanced-guide/serving-static-files',
desc: "Know how GoFr automatically serves static content from a static folder in the application directory."
},
{
title: 'Profiling in GoFr Applications',
href: '/docs/advanced-guide/debugging',
desc: "Discover GoFr auto-enables pprof profiling by leveraging its built-in configurations."
},
{
title: 'Building CLI Applications',
href: '/docs/advanced-guide/building-cli-applications',
desc: "Learn to build powerful command-line interface (CLI) applications using GoFr's app.NewCMD(), offering a robust framework for command-line tools."
},
],
},
{
title: 'Datasources',
links: [
{
title: "Getting Started",
href: "/docs/datasources/getting-started",
desc: "Learn how to connect to and interact with multiple databases in GoFr."
},
{
title: "ArangoDB",
href: "/docs/datasources/arangodb",
desc: "Learn how to connect to and interact with arango database in GoFr."
},
{
title: "Cassandra",
href: "/docs/datasources/cassandra",
desc: "Learn how to connect to and interact with cassandra database in GoFr."
},
{
title: "ClickHouse",
href: "/docs/datasources/clickhouse",
desc: "Learn how to connect to and interact with clickhouse database in GoFr."
},
{
title: "CockroachDB",
href: "/docs/datasources/cockroachdb",
desc: "Learn how to connect to and interact with CockroachDB in GoFr."
},
{
title: "Couchbase",
href: "/docs/datasources/couchbase",
desc: "Learn how to connect to and interact with couchbase database in GoFr."
},
{
title: "DGraph",
href: "/docs/datasources/dgraph",
desc: "Learn how to connect to and interact with dgraph database in GoFr."
},
{
title: "MongoDB",
href: "/docs/datasources/mongodb",
desc: "Learn how to connect to and interact with mongo database in GoFr."
},
{
title: "OpenTSDB",
href: "/docs/datasources/opentsdb",
desc: "Learn how to connect to and interact with opentsdb database in GoFr."
},
{
title: "OracleDB",
href: "/docs/datasources/oracle",
desc: "Learn how to connect to and interact with oracle database in GoFr."
},
{
title: "ScyllaDB",
href: "/docs/datasources/scylladb",
desc: "Learn how to connect to and interact with scylla database in GoFr."
},
{
title: "Solr",
href: "/docs/datasources/solr",
desc: "Learn how to connect to and interact with solr database in GoFr."
},
{
title: "SurrealDB",
href: "/docs/datasources/surrealdb",
desc: "Learn how to connect to and interact with surreal database in GoFr."
},
{
title: "Elasticsearch",
href: "/docs/datasources/elasticsearch",
desc: "Learn how to connect to and interact with elasticsearch in GoFr."
},
{
title: "InfluxDB",
href: "/docs/datasources/influxdb",
desc: "Learn how to connect to and interact with influxdb in GoFr."
},
],
},
{
title: 'References',
links: [
{
title: 'Context',
href: '/docs/references/context',
desc: "Discover the GoFR context, an injected object that simplifies request-specific data handling for HTTP, gRPC, and Pub/Sub calls. Learn how it extends Go's context, providing easy access to dependencies like databases, loggers, and HTTP clients. Explore features for reading HTTP requests, binding data, and accessing query and path parameters efficiently, all while reducing application complexity."
},
{
title: 'Configs',
href: '/docs/references/configs',
desc: "Learn how to manage configuration settings in your GoFR applications, including default values for environment variables. This section provides a comprehensive list of all available configurations to streamline your setup."
},
{
title: 'Testing',
href: '/docs/references/testing',
desc: "GoFr provides a centralized collection of mocks to facilitate writing effective unit tests. Explore testing strategies and tools for GoFr applications, ensuring the code is robust, reliable, and maintainable."
},
{
title: 'GoFr CLI',
href: '/docs/references/gofrcli',
desc: "GoFr CLI is the command line tool for initializing projects and performing tasks in accordance with GoFr framework."
}
],
},
]
================================================
FILE: docs/page.md
================================================
# Getting started
GoFr is an opinionated web framework written in Go (Golang). It helps in building robust and scalable applications. This framework is designed to offer a user-friendly and familiar abstraction for all the developers. We prioritize simplicity over complexity.
In this section, we will walk through what GoFr is, the problems it solves, and how it can help you build your project.
{% quick-links %}
{% quick-link title="Quick Start" icon="installation" href="/docs/quick-start/introduction" description="Step-by-step guides to setting up your system and installing the library." /%}
{% quick-link title="Examples" icon="plugins" href="https://github.com/gofr-dev/gofr/tree/main/examples" description="Our guides break down how to perform common tasks in GoFr." /%}
{% /quick-links %}
## Key Features
- Logging
- Support for various response types such as JSON, FILE.
- Health check and Readiness monitoring for checking continuous service availability.
- Metrics exposure for monitoring and analysis using Prometheus.
- Tracing capability to track user request progress with traceable spans.
- Level-based logging support for effective debugging and monitoring.
## Principles
- Promote simple and clean code.
- Favor compile-time checked code over dynamic code.
- Create a solid foundation for the integration of application modules.
- Encourage a more functional way of programming.
- Avoid code duplication.
- Log and store data for analysis purposes.
================================================
FILE: docs/quick-start/add-rest-handlers/page.md
================================================
# Add REST Handlers
GoFr simplifies the process of implementing CRUD (Create, Read, Update, Delete) operations by enabling the automatic generation of handlers directly from Go structs.
This feature eliminates the need for writing repetitive boilerplate code, allowing developers to focus on application logic.
## Default Behavior
If the custom handlers ain't implemented on the struct, GoFr provides default handlers for each CRUD operation. These handlers handle basic database interactions:
- **Create**: `/entity` Inserts a new record based on data provided in a JSON request body.
- **Read**:
- **GET**: `/entity` Retrieves all entities of the type specified by the struct.
- **GET**: `/entity/{id}` Retrieves a specific entity identified by the {id} path parameter.
- **Update**: `/entity/{id}` Updates an existing record identified by the {id} path parameter, based on data provided in a JSON request body.
- **Delete** `/entity/{id}` Deletes an existing record identified by the {id} path parameter.
> [!NOTE]
> The registered routes will have the same name as the given struct, but if we want to change route name, we can implement `RestPath` method in the struct:
```go
type userEntity struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
IsEmployed bool `json:"isEmployed"`
}
func (u *userEntity) RestPath() string {
return "users"
}
```
## Overriding Default Handlers
While the default handlers provide basic functionality, user might want to customize their behavior for specific use cases.
The AddRESTHandlers feature allows user to override these handlers by implementing methods within the struct itself.
## Database Table Name
By default, GoFr assumes the struct name in snake-case matches the database table name for querying data. For example, `UserEntity` struct matches `user_entity` database table, `cardConfig` struct matches `card_config` database table, etc.
To change table name, you need to implement `TableName` method in the struct:
```go
type userEntity struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
IsEmployed bool `json:"isEmployed"`
}
func (u *userEntity) TableName() string {
return "user"
}
```
## Adding Database Constraints
By default, GoFr assumes to have manual insertion of id for a given struct, but to support SQL constraints like `auto-increment`,
`not-null` user can use the `sql` tag while declaring the struct fields.
```go
type user struct {
ID int `json:"id" sql:"auto_increment"`
Name string `json:"name" sql:"not_null"`
Age int `json:"age"`
IsEmployed bool `json:"isEmployed"`
}
```
Now when posting data for the user struct, the `Id` we be auto-incremented and the `Name` will be a not-null field in table.
## Benefits of Adding REST Handlers of GoFr
1. Reduced Boilerplate Code: Eliminate repetitive code for CRUD operations, freeing user to focus on core application logic.
2. Consistency: Ensures consistency in CRUD operations across different entities by using a standardized approach.
3. Flexibility: Allows developers to customize CRUD behavior as per application requirements, providing flexibility and extensibility.
## Example
```go
package main
import (
"gofr.dev/examples/using-crud-from-struct/migrations"
"gofr.dev/pkg/gofr"
)
type user struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
IsEmployed bool `json:"isEmployed"`
}
// GetAll : User can overwrite the specific handlers by implementing them like this
func (u *user) GetAll(c *gofr.Context) (any, error) {
return "user GetAll called", nil
}
func main() {
// Create a new application
a := gofr.New()
// Add migrations to run
a.Migrate(migrations.All())
// AddRESTHandlers creates CRUD handles for the given entity
err := a.AddRESTHandlers(&user{})
if err != nil {
return
}
// Run the application
a.Run()
}
```
In this example, we define a user struct representing a database entity. The `GetAll` method in the provided code demonstrates how to override the default behavior for retrieving all entities.
This method can be used to implement custom logic for filtering, sorting, or retrieving additional data along with the entities.
## Few Points to Consider:
**1. Passing Struct by Reference**
The struct should always be passed by reference in the method `AddRESTHandlers`.
**2. Field Naming Convention**
GoFr assumes that struct fields in snake_case match the database column names.
* For example, the `IsEmployed` field in the struct matches the `is_employed` column in the database.
* Similarly, the `Age` field matches the `age` column.
**3. Primary Key**
The first field of the struct is typically used as the primary key for data operations. However, this behavior can be customized using GoFr's features.
**4. Datatype Conversions**
| Go Type | SQL Type | Description |
|---|---|---|
| `uuid.UUID` (from `github.com/google/uuid` or `github.com/satori/go.uuid`) | `CHAR(36)` or `VARCHAR(36)` | UUIDs are typically stored as 36-character strings in SQL databases. |
| `string` | `VARCHAR(n)` or `TEXT` | Use `VARCHAR(n)` for fixed-length strings, while `TEXT` is for longer, variable-length strings. |
| `int`, `int32`, `int64`, `uint`, `uint32`, `uint64` | `INT`, `BIGINT`, `SMALLINT`, `TINYINT`, `INTEGER` | Use `INT` for general integer values, `BIGINT` for large values, and `SMALLINT` or `TINYINT` for smaller ranges. |
| `bool` | `BOOLEAN` or `TINYINT(1)` | Use `BOOLEAN` (supported by most SQL databases like PostgreSQL, MySQL) or `TINYINT(1)` in MySQL (where `0` is false, and `1` is true). |
| `float32`, `float64` | `FLOAT`, `DOUBLE`, `DECIMAL` | Use `DECIMAL` for precise decimal numbers (e.g., financial data), `FLOAT` or `DOUBLE` for approximate floating-point numbers. |
| `time.Time` | `DATE`, `TIME`, `DATETIME`, `TIMESTAMP` | Use `DATE` for just the date, `TIME` for the time of day, and `DATETIME` or `TIMESTAMP` for both date and time. |
> #### Check out the example on how to add REST Handlers in GoFr: [Visit GitHub](https://github.com/gofr-dev/gofr/tree/main/examples/using-add-rest-handlers)
================================================
FILE: docs/quick-start/cli/page.md
================================================
================================================
FILE: docs/quick-start/configuration/page.md
================================================
# Configurations
GoFr simplifies configuration management by reading configuration via environment variables.
Application code is decoupled from how configuration is managed as per the {%new-tab-link title="12-factor" href="https://12factor.net/config" %}.
Configs in GoFr can be used to initialize datasources, tracing, setting log levels, changing default HTTP or metrics port.
This abstraction provides a user-friendly interface for configuring user's application without modifying the code itself.
To set configs create a `configs` directory in the project's root and add `.env` file.
Follow this directory structure within the GoFr project:
```dotenv
my-gofr-app/
├── configs/
│ ├── .local.env
│ ├── .dev.env
│ ├── .staging.env
│ └── .prod.env
├── main.go
└── ...
```
By default, GoFr starts HTTP server at port 8000, in order to change that we can add the config `HTTP_PORT`
Similarly to Set the app-name user can add `APP_NAME`. For example:
```dotenv
# configs/.env
APP_NAME=test-service
HTTP_PORT=9000
```
## Configuring Environments in GoFr
GoFr uses an environment variable, `APP_ENV`, to determine the application's current environment. This variable also guides GoFr to load the corresponding environment file.
### Example:
If `APP_ENV` is set to `dev`, GoFr will attempt to load the `.dev.env` file from the configs directory. If this file is not found, GoFr will default to loading the `.env` file.
In the absence of the `APP_ENV` variable, GoFr will first attempt to load the `.local.env` file. If this file is not found, it will default to loading the `.env` file.
_For example, to run the application in the `dev` environment, use the following command:_
```bash
APP_ENV=dev go run main.go
```
This approach ensures that the correct configurations are used for each environment, providing flexibility and control over the application's behavior in different contexts.
================================================
FILE: docs/quick-start/connecting-mysql/page.md
================================================
# Connecting to MySQL
Just like Redis, GoFr supports connection to various SQL-compatible databases (MySQL, MariaDB, PostgreSQL, and Supabase) based on configuration variables.
## MySQL/MariaDB
### Setup
Users can run MySQL/MariaDB and create a database locally using the following Docker command:
```bash
docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=root123 -e MYSQL_DATABASE=test_db -p 3306:3306 -d mysql:8.0.30
```
Access the `test_db` database and create a table customer with columns `id` and `name`. Change MySQL to MariaDB as needed:
```bash
docker exec -it gofr-mysql mysql -uroot -proot123 test_db -e "CREATE TABLE customers (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL);"
```
Now that the database with the table is ready, we can connect our GoFr server to MySQL/MariaDB.
### Configuration & Usage
After adding MySQL/MariaDB configs `.env` will be updated to the following. Use ```DB_DIALECT=mysql``` for both MySQL and MariaDB.
```dotenv
# configs/.env
APP_NAME=test-service
HTTP_PORT=9000
REDIS_HOST=localhost
REDIS_PORT=6379
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=root123
DB_NAME=test_db
DB_PORT=3306
DB_DIALECT=mysql
DB_CHARSET=utf8 #(optional)
```
### TLS/SSL Configuration
GoFr supports secure TLS connections to MySQL/MariaDB databases. Configure TLS by setting the `DB_SSL_MODE` environment variable and optionally providing certificate paths for enhanced security.
#### Available SSL Modes
| SSL Mode | Description |
|----------|-------------|
| `disable` | No TLS encryption (default) |
| `preferred` | Attempts TLS, falls back to plain connection if unavailable |
| `require` | Enforces TLS but skips certificate validation |
| `skip-verify` | Enforces TLS without validating server certificate |
| `verify-ca` | Enforces TLS and validates server certificate against CA |
| `verify-full` | Enforces TLS with full certificate validation (including hostname) |
#### TLS Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| `DB_SSL_MODE` | No | TLS mode (defaults to `disable`) |
| `DB_TLS_CA_CERT` | Conditional | Path to CA certificate (required for `verify-ca`/`verify-full`) |
| `DB_TLS_CLIENT_CERT` | No | Path to client certificate (for mutual TLS) |
| `DB_TLS_CLIENT_KEY` | No | Path to client private key (for mutual TLS) |
#### Example Configuration
```dotenv
# configs/.env
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=root123
DB_NAME=test_db
DB_PORT=3306
DB_DIALECT=mysql
# Basic TLS (no certificate validation)
DB_SSL_MODE=require
# OR with CA certificate validation (production)
DB_SSL_MODE=verify-ca
DB_TLS_CA_CERT=/path/to/ca-cert.pem
# OR with mutual TLS (enhanced security)
DB_SSL_MODE=verify-full
DB_TLS_CA_CERT=/path/to/ca-cert.pem
DB_TLS_CLIENT_CERT=/path/to/client-cert.pem
DB_TLS_CLIENT_KEY=/path/to/client-key.pem
```
## PostgreSQL
### Setup
Users can run PostgreSQL and create a database locally using the following Docker command:
```bash
docker run --name gofr-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=test_db -p 5432:5432 -d postgres:14
```
Access `test_db` database and create a table customer with columns `id` and `name`:
```bash
docker exec -it gofr-postgres psql -U postgres test_db -c "CREATE TABLE customers (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL);"
```
### Configuration & Usage
After adding PostgreSQL configs, `.env` will be updated to the following:
```dotenv
# configs/.env
APP_NAME=test-service
HTTP_PORT=9000
REDIS_HOST=localhost
REDIS_PORT=6379
DB_HOST=localhost
DB_USER=postgres
DB_PASSWORD=postgres
DB_NAME=test_db
DB_PORT=5432
DB_DIALECT=postgres
DB_SSL_MODE=disable #(optional, defaults to disable)
```
## Supabase
[Supabase](https://supabase.com) is an open-source Firebase alternative that provides a PostgreSQL database with additional features. GoFr supports connecting to Supabase databases with specialized configuration.
### Setup
To use Supabase with GoFr:
1. Sign up for a [Supabase account](https://supabase.com)
2. Create a new project
3. Get your connection information from the Supabase dashboard:
- Project Reference ID
- Database Password
- Region (for pooled connections)
### Configuration & Usage
GoFr provides three connection types for Supabase:
1. **Direct Connection**: Standard connection to the database
2. **Session Pooler**: Connection via Supabase's connection pooler (maintains session variables)
3. **Transaction Pooler**: Connection via Supabase's transaction pooler (resets session variables)
Add Supabase configuration to your `.env` file:
```dotenv
# configs/.env
APP_NAME=test-service
HTTP_PORT=9000
# Supabase configuration
DB_DIALECT=supabase
DB_USER=postgres
DB_PASSWORD=your_database_password
DB_NAME=postgres
DB_PORT=5432 # Optional, defaults based on connection type
DB_SSL_MODE=require # Optional, always forced to "require" for Supabase
# Supabase-specific configs
SUPABASE_PROJECT_REF=your_project_ref_id
SUPABASE_CONNECTION_TYPE=direct # Options: direct, session, transaction
SUPABASE_REGION=us-east-1 # Required for pooled connections
```
Alternatively, you can provide a full connection string:
```dotenv
DB_DIALECT=supabase
DB_URL=postgresql://postgres:your_password@db.your_project_ref.supabase.co:5432/postgres
```
#### Connection Types
- **Direct** (`SUPABASE_CONNECTION_TYPE=direct`): Connects directly to your database at `db.[PROJECT_REF].supabase.co:5432`
- **Session Pooler** (`SUPABASE_CONNECTION_TYPE=session`): Uses Supabase's connection pooler at `aws-0-[REGION].pooler.supabase.co:5432`
- **Transaction Pooler** (`SUPABASE_CONNECTION_TYPE=transaction`): Uses Supabase's transaction pooler at `aws-0-[REGION].pooler.supabase.co:6543`
**Note:** For pooled connections, the `SUPABASE_REGION` parameter is required.
## Database Usage Example
For all supported SQL databases, GoFr provides a consistent API to interact with your data.
Now, in the following example, we'll store customer data using **POST** `/customer` and then use **GET** `/customer` to retrieve the same.
We will be storing the customer data with `id` and `name`.
After adding code to add and retrieve data from the SQL datastore, `main.go` will be updated to the following.
```go
package main
import (
"errors"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr"
)
type Customer struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
// initialize gofr object
app := gofr.New()
app.GET("/redis", func(ctx *gofr.Context) (any, error) {
// Get the value using the Redis instance
val, err := ctx.Redis.Get(ctx.Context, "test").Result()
if err != nil && !errors.Is(err, redis.Nil) {
// If the key is not found, we are not considering this an error and returning ""
return nil, err
}
return val, nil
})
app.POST("/customer/{name}", func(ctx *gofr.Context) (any, error) {
name := ctx.PathParam("name")
// Inserting a customer row in database using SQL
_, err := ctx.SQL.ExecContext(ctx, "INSERT INTO customers (name) VALUES (?)", name)
return nil, err
})
app.GET("/customer", func(ctx *gofr.Context) (any, error) {
var customers []Customer
// Getting the customer from the database using SQL
rows, err := ctx.SQL.QueryContext(ctx, "SELECT * FROM customers")
if err != nil {
return nil, err
}
for rows.Next() {
var customer Customer
if err := rows.Scan(&customer.ID, &customer.Name); err != nil {
return nil, err
}
customers = append(customers, customer)
}
// return the customer
return customers, nil
})
app.Run()
}
```
To update the database with the customer data access, use this curl command through the terminal
```bash
# here abc and xyz after /customer are the path parameters
curl --location --request POST 'http://localhost:9000/customer/abc'
curl --location --request POST 'http://localhost:9000/customer/xyz'
```
Now when we access {% new-tab-link title="http://localhost:9000/customer" href="http://localhost:9000/customer" /%} we should see the following output:
```json
{
"data": [
{
"id": 1,
"name": "abc"
},
{
"id": 2,
"name": "xyz"
}
]
}
```
**Note:** When using PostgreSQL or Supabase, you may need to use `$1` instead of `?` in SQL queries, depending on your driver configuration.
## Enabling Read/Write Splitting in MySQL (DBResolver)
GoFr provides built-in support for read/write splitting using its `DBRESOLVER` module for **MySQL**.
This feature automatically routes requests to the **primary database** or **read replicas** based on:
- **HTTP Method**:
- Write operations (`POST`, `PUT`, `PATCH`, `DELETE`) → Primary
- Read operations (`GET`, `HEAD`, `OPTIONS`) → Replicas
- **Route Configuration**: Force specific routes to always use the primary database for strong consistency
### Installation
Import the GoFr's dbresolver for MySQL:
```shell
go get gofr.dev/pkg/gofr/datasource/dbresolver@latest
```
### Configuration
**1. Environment Variables**
Configure the primary database in your .env file:
```editorconfig
# Primary database
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=root123
DB_NAME=test_db
DB_DIALECT=mysql
```
**2. Initialize DBResolver**
After importing the package, you can configure the DBResolver in your GoFr application using the `AddDBResolver` method.
You can choose the load balancing strategy and enable fallback to primary:
```go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/dbresolver"
)
type Customer struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
a := gofr.New()
// Initialize DB resolver with default settings
err := dbresolver.InitDBResolver(a, &dbresolver.Config{
Strategy: dbresolver.StrategyRoundRobin, // use round-robin strategy or random strategy
ReadFallback: true, // allow reads on primary if all replicas are down
MaxFailures: 3, // number of allowed failures before marking a replica as down
TimeoutSec: 30, // timeout for marking a replica as down
PrimaryRoutes: []string{"/admin", "/api/payments/*"},
Replicas: []dbresolver.ReplicaCredential{
{
Host: "localhost:3307",
User: "replica_user1",
Password: "pass1",
},
{
Host: "replica2.example.com:3308",
User: "replica_user2",
Password: "pass2",
},
{
Host: "replica3.example.com:3309",
User: "replica_user3",
Password: "pass3",
},
},// routes that should go to primary
})
if err != nil {
a.Logger().Errorf("failed to initialize db resolver: %v", err)
}
// Read endpoint - goes to replica
a.GET("/customers", func(c *gofr.Context) (interface{}, error) {
var customers []Customer
c.SQL.Select(c, &customers, "SELECT id, name FROM customers")
return customers, err
})
// Write endpoint - goes to primary
a.POST("/customers", func(c *gofr.Context) (interface{}, error) {
var customer Customer
c.Bind(&customer)
_, err := c.SQL.Exec("INSERT INTO customers (name) VALUES (?)", customer.Name)
return customer, err
})
// Admin endpoint - forced to primary
a.GET("/admin/customers", func(c *gofr.Context) (interface{}, error) {
var customers []Customer
c.SQL.Select(c, &customers, "SELECT id, name FROM customers")
return customers, err
})
a.Run()
}
```
**3. Connection Pool Tuning (Optional)**
By default, replica pools are auto-scaled based on primary settings:
```editorconfig
# Defaults (automatically calculated)
DB_MAX_IDLE_CONNECTION=2 → Replicas: 8 (2 × 4)
DB_MAX_OPEN_CONNECTION=20 → Replicas: 40 (20 × 2)
```
Override with:
```editorconfig
DB_REPLICA_MAX_IDLE_CAP=100
DB_REPLICA_MIN_IDLE=5
DB_REPLICA_DEFAULT_IDLE=15
DB_REPLICA_MAX_OPEN_CAP=500
DB_REPLICA_MIN_OPEN=20
DB_REPLICA_DEFAULT_OPEN=150
```
**Benefits**
- Performance: Offloads read traffic from the primary, reducing latency.
- Scalability: Easily scale reads by adding more replicas.
- Resilience: Ensures high availability through automatic fallback.
================================================
FILE: docs/quick-start/connecting-redis/page.md
================================================
# Connecting to Redis
GoFr simplifies the process of connecting to Redis.
## Setup:
Ensure we have Redis installed on our system.
Optionally, we can use Docker to set up a development environment with password authentication as described below.
```bash
docker run --name gofr-redis -p 2002:6379 -d \
-e REDIS_PASSWORD=password \
redis:7.0.5 --requirepass password
```
We can set a sample key `greeting` using the following command:
```bash
docker exec -it gofr-redis bash -c 'redis-cli SET greeting "Hello from Redis."'
```
## Configuration & Usage:
GoFr applications rely on environment variables to configure and connect to a Redis server.
These variables are stored in a `.env` file located within the `configs` directory at your project root.
### Required Environment Variables:
{% table %}
- Key
- Description
---
- REDIS_HOST
- Hostname or IP address of your Redis server
---
- REDIS_PORT
- Port number your Redis server listens on (default: `6379`)
---
- REDIS_USER
- Redis username; multiple users with ACLs can be configured. [See official docs](https://redis.io/docs/latest/operate/oss_and_stack/management/security/acl/)
---
- REDIS_PASSWORD
- Redis password (required only if authentication is enabled)
---
- REDIS_DB
- Redis database number (default: `0`)
---
{% /table %}
## TLS Support (Optional):
{% table %}
- Key
- Description
---
- REDIS_TLS_ENABLED
- Set to `"true"` to enable TLS
---
- REDIS_TLS_CA_CERT_PATH
- File path to the CA certificate used to verify the Redis server
---
- REDIS_TLS_CERT_PATH
- File path to the client certificate (for mTLS)
---
- REDIS_TLS_KEY_PATH
- File path to the client private key (for mTLS)
---
{% /table %}
## ✅ Example `.env` File
```env
REDIS_HOST=redis.example.com
REDIS_PORT=6379
REDIS_USER=appuser
REDIS_PASSWORD=securepassword
REDIS_DB=0
# TLS settings (optional)
REDIS_TLS_ENABLED=true
REDIS_TLS_CA_CERT_PATH=./configs/certs/ca.pem
REDIS_TLS_CERT_PATH=./configs/certs/client.crt
REDIS_TLS_KEY_PATH=./configs/certs/client.key
```
The following code snippet demonstrates how to retrieve data from a Redis key named "greeting":
```go
package main
import (
"errors"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr"
)
func main() {
// Initialize GoFr object
app := gofr.New()
app.GET("/redis", func(ctx *gofr.Context) (any, error) {
// Get the value using the Redis instance
val, err := ctx.Redis.Get(ctx.Context, "greeting").Result()
if err != nil && !errors.Is(err, redis.Nil) {
// If the key is not found, we are not considering this an error and returning ""
return nil, err
}
return val, nil
})
// Run the application
app.Run()
}
```
================================================
FILE: docs/quick-start/introduction/page.md
================================================
# Prerequisite
- Go 1.24 or above.
To check the Go version, use the following command `go version`.
- Prior familiarity with Golang syntax is essential. {% new-tab-link title="Golang Tour" href="https://tour.golang.org/" /%} is highly recommended as it has an excellent guided tour.
## Write your first GoFr API
Let's start by initializing the {% new-tab-link title="go module" href="https://go.dev/ref/mod" /%} by using the following command.
```bash
go mod init github.com/example
```
Add {% new-tab-link title="gofr" href="https://github.com/gofr-dev/gofr" /%} package to the project using the following command.
```bash
go get gofr.dev
```
This code snippet showcases the creation of a simple GoFr application that defines a route and serves a response.
You can add this code to your main.go file.
```go
package main
import "gofr.dev/pkg/gofr"
func main() {
// initialize gofr object
app := gofr.New()
// register route greet
app.GET("/greet", func(ctx *gofr.Context) (any, error) {
return "Hello World!", nil
})
// Runs the server, it will listen on the default port 8000.
// it can be over-ridden through configs
app.Run()
}
```
Before starting the server, run the following command in your terminal to ensure you have downloaded and synchronized all required dependencies for your project.
`go mod tidy`
Once the dependencies are synchronized, start the GoFr server using the following command:
`go run main.go`
This would start the server at 8000 port, `/greet` endpoint can be accessed from your browser at {% new-tab-link title="http://localhost:8000/greet" href="http://localhost:8000/greet" /%}, you would be able to see the output as following with _Status Code 200_ as per REST Standard.
```json
{ "data": "Hello World!" }
```
## Understanding the example
The `hello-world` server involves three essential steps:
1. **Creating GoFr Server:**
When `gofr.New()` is called, it initializes the framework and handles various setup tasks like initializing logger, metrics, datasources, etc., based on the configs.
_This single line is a standard part of all GoFr servers._
2. **Attaching a Handler to a Path:**
In this step, the server is instructed to associate an HTTP request with a specific handler function. This is achieved through `app.GET("/greet", HandlerFunction)`, where _GET /greet_ maps to HandlerFunction. Likewise, `app.POST("/todo", ToDoCreationHandler)` links a _POST_ request to the `/todo` endpoint with _ToDoCreationHandler_.
**Good To Know**
> In Go, functions are first-class citizens, allowing easy handler definition and reference.
> HTTP Handler functions should follow the `func(ctx *gofr.Context) (any, error)` signature.
> They take a context as input, returning two values: the response data and an error (set to `nil` when there is no error).
GoFr {% new-tab-link newtab=false title="context" href="/docs/references/context" /%} `ctx *gofr.Context` serves as a wrapper for requests, responses, and dependencies, providing various functionalities.
3. **Starting the server**
When `app.Run()` is called, it configures, initiates, and runs the HTTP server, middlewares. It manages essential features such as routes for health check endpoints, metrics server, favicon etc. It starts the server on the default port 8000.
================================================
FILE: docs/quick-start/observability/page.md
================================================
# Observability
GoFr, by default, manages observability in different ways once the server starts:
## Logs
Logs offer real-time information, providing valuable insights and immediate visibility into the ongoing state and activities of the system.
It helps in identifying errors, debugging and troubleshooting, monitor performance, analyzing application usage, communications etc.
GoFr logger allows customizing the log level, which provides flexibility to adjust logs based on specific needs.
Logs are generated only for events equal to or above the specified log level; by default, GoFr logs at _INFO_ level.
Log Level can be changed by setting the environment variable `LOG_LEVEL` value to _DEBUG, INFO, NOTICE, WARN, ERROR or FATAL_.
When the GoFr server runs, it prints a log for reading configs, database connection, requests, database queries, missing configs, etc.
They contain information such as request's correlation ID, status codes, request time, etc.
### Log Levels
#### DEBUG
This is the lowest priority level. It represents the most detailed/granular information.
**Note:** `DEBUG` logs should be enabled only in development or controlled troubleshooting scenarios.They are typically disabled in production environments due to performance overhead and security risks.
**Example**
```Go
ctx.Debug("Calc trace - Price:", 150, "Discount:", 0.2, "Tax Multiplier:", 1.05)
```
---
#### INFO
`INFO` Represents normal operational events during application execution and acts as the default logging level, ensuring baseline observability without excessive verbosity.
**Example**
```Go
ctx.Info("Application configuration loaded", "Source", "env")
```
---
#### NOTICE
A level higher than `INFO` but lower than `WARN`. It shares the same visual prominence as a Warning but implies a "normal" condition rather than a problem. In simple words, it's used for events that are normal but rare and significant.
**Example**
```Go
ctx.Notice("Configuration hot-reload triggered by system admin")
```
---
#### WARN
`WARN` should represent abnormal runtime conditions that indicate instability or degraded operation (retries, fallbacks, transient failures), not long-term code hygiene issues like deprecated API usage. If something would show up repeatedly in a healthy system, it shouldn’t be a `WARN`, otherwise the signal gets diluted and operators start ignoring it.
**Example**
```Go
ctx.Warn("Database connection timeout. Retrying...", "attempt", 1, "retry_after", "2s")
```
---
#### ERROR
Indicates a failure event. This level routes logs to `stderr` (Standard Error), ensuring visibility to error tracking tools.
**Example**
```Go
ctx.Error("DB Query Timeout: Analytics fetch failed.", "error", errors.New("query execution exceeded 3000ms"))
```
---
#### FATAL
The highest priority level. `FATAL` represents a critical system failures where the application cannot function.
**Note:** `FATAL` terminates the process immediately and is intended only for startup-time failures, not runtime request handling.
**Example**
```Go
app.Logger().Fatal("Startup Failure: Mandatory SSL certificate missing.", "path", "/etc/certs/server.crt")
```
---
> **Note:** Performance & Log Volume.
>1. Early Exit Optimization: The logger implements an "Early Exit" strategy. If the incoming log level is lower than the configured `LOG_LEVEL`, the function returns immediately before performing any formatting or allocation.
>2. Locking Overhead: The terminal output utilizes a mutex lock to ensure thread safety.
---
{% figure src="/quick-start-logs.png" alt="Pretty Printed Logs" /%}
Logs are well-structured, they are of type JSON when exported to a file, such that they can be pushed to logging systems such as {% new-tab-link title="Loki" href="https://grafana.com/oss/loki/" /%}, Elasticsearch, etc.
## Metrics
Metrics enable performance monitoring by providing insights into response times, latency, throughput, resource utilization, tracking CPU, memory, and disk I/O consumption across services, facilitating capacity planning and scalability efforts.
Metrics play a pivotal role in fault detection and troubleshooting, offering visibility into system behavior.
They are instrumental in measuring and meeting service-level agreements (SLAs) to ensure expected performance and reliability.
GoFr publishes metrics to port: _2121_ on _/metrics_ endpoint in Prometheus format.
### Default Metrics
{% table %}
- Name
- Type
- Description
---
- app_go_numGC
- gauge
- Number of completed Garbage Collector cycles
---
- app_go_routines
- gauge
- Number of Go routines running
---
- app_go_sys
- gauge
- Number of total bytes of memory
---
- app_sys_memory_alloc
- gauge
- Number of bytes allocated for heap objects
---
- app_sys_total_alloc
- gauge
- Number of cumulative bytes allocated for heap objects
---
- app_info
- gauge
- Number of instances running with info of app and framework
---
- app_http_response
- histogram
- Response time of HTTP requests in seconds
---
- app_http_service_response
- histogram
- Response time of HTTP service requests in seconds
---
- app_sql_open_connections
- gauge
- Number of open SQL connections
---
- app_sql_inUse_connections
- gauge
- Number of inUse SQL connections
---
- app_sql_stats
- histogram
- Response time of SQL queries in milliseconds
---
- app_redis_stats
- histogram
- Response time of Redis commands in milliseconds
---
- app_pubsub_publish_total_count
- counter
- Number of total publish operations
---
- app_pubsub_publish_success_count
- counter
- Number of successful publish operations
---
- app_pubsub_subscribe_total_count
- counter
- Number of total subscribe operations
---
- app_pubsub_subscribe_success_count
- counter
- Number of successful subscribe operations
---
- app_http_retry_count
- counter
- Total number of retry events
---
- app_http_circuit_breaker_state
- gauge
- Current state of the circuit breaker (0 for Closed, 1 for Open). Used for historical timeline visualization.
---
- app_graphql_operations_total
- counter
- Total number of GraphQL operations received. Labels: `operation_name`, `type`.
---
- app_graphql_error_total
- counter
- Total number of GraphQL operations that returned an error. Labels: `operation_name`, `type`.
---
- app_graphql_request_duration
- histogram
- Response time of GraphQL requests in seconds. Labels: `operation_name`, `type`, `status`.
---
- app_cron_job_total
- counter
- Total number of cron job executions. Label: `job`.
---
- app_cron_job_success
- counter
- Number of successful cron job executions. Label: `job`.
---
- app_cron_job_failures
- counter
- Number of failed cron job executions. Label: `job`.
---
- app_cron_job_duration
- histogram
- Duration of cron job execution in seconds. Label: `job`.
{% /table %}
For example: When running the application locally, we can access the /metrics endpoint on port 2121 from: {% new-tab-link title="http://localhost:2121/metrics" href="http://localhost:2121/metrics" /%}
GoFr also supports creating {% new-tab-link newtab=false title="custom metrics" href="/docs/advanced-guide/publishing-custom-metrics" /%}.
### Disabling the Metrics Server
To disable the metrics server entirely, set the `METRICS_PORT` environment variable to `0`:
```dotenv
METRICS_PORT=0
```
### Example Dashboard
These metrics can be easily consumed by monitoring systems like {% new-tab-link title="Prometheus" href="https://prometheus.io/" /%}
and visualized in dashboards using tools like {% new-tab-link title="Grafana" href="https://grafana.com/" /%}.
You can find the dashboard source in the {% new-tab-link title="GoFr repository" href="https://github.com/gofr-dev/gofr/tree/main/examples/http-server/docker/provisioning/dashboards/gofr-dashboard" /%}.
{% figure src="/metrics-dashboard.png" alt="Grafana Dashboard showing GoFr metrics including HTTP request rates,
response times, etc." caption="Example monitoring dashboard using GoFr's built-in metrics" /%}
## Tracing
{% new-tab-link title="Tracing" href="https://opentelemetry.io/docs/concepts/signals/#traces" /%} is a powerful tool for gaining insights into your application's behavior, identifying bottlenecks, and improving
system performance. A trace is a tree of spans. It is a collective of observable signals showing the path of work
through a system. A trace on its own is distinguishable by a `TraceID`.
In complex distributed systems, understanding how requests flow through the system is crucial for troubleshooting performance
issues and identifying bottlenecks. Traditional logging approaches often fall short, providing limited visibility into
the intricate interactions between components.
### Automated Tracing in GoFr
GoFr automatically exports traces for all requests and responses. GoFr uses
{% new-tab-link title="OpenTelemetry" href="https://opentelemetry.io/docs/concepts/what-is-opentelemetry/" /%} , a popular tracing framework, to
automatically add traces to all requests and responses.
**Automatic Correlation ID Propagation:**
When a request enters your GoFr application, GoFr automatically generates a correlation-ID `X-Correlation-ID` and adds it
to the response headers. This correlation ID is then propagated to all downstream requests. This means that user can track
a request as it travels through your distributed system by simply looking at the correlation ID in the request headers.
### Configuration & Usage:
GoFr has support for following trace-exporters:
#### 1. [Zipkin](https://zipkin.io/):
To see the traces install zipkin image using the following Docker command:
```bash
docker run --name gofr-zipkin -p 2005:9411 -d openzipkin/zipkin:latest
```
Add Tracer configs in `.env` file, your .env will be updated to
```dotenv
APP_NAME=test-service
HTTP_PORT=9000
REDIS_HOST=localhost
REDIS_PORT=6379
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=root123
DB_NAME=test_db
DB_PORT=3306
# tracing configs
TRACE_EXPORTER=zipkin
TRACER_URL=http://localhost:2005/api/v2/spans
TRACER_RATIO=0.1
LOG_LEVEL=DEBUG
```
> [!NOTE]
> If the value of `TRACER_PORT` is not provided, GoFr uses port `9411` by default.
Open {% new-tab-link title="zipkin" href="http://localhost:2005/zipkin/" /%} and search by TraceID (correlationID) to see the trace.
{% figure src="/quick-start-trace.png" alt="Zipkin traces" /%}
#### 2. [Jaeger](https://www.jaegertracing.io/):
To see the traces, install Jaeger image using the following Docker command:
```bash
docker run -d --name jaeger \
-e COLLECTOR_OTLP_ENABLED=true \
-p 16686:16686 \
-p 14317:4317 \
-p 14318:4318 \
jaegertracing/all-in-one:1.41
```
Add Jaeger Tracer configs in `.env` file, your .env will be updated to
```dotenv
# ... no change in other env variables
# tracing configs
TRACE_EXPORTER=jaeger
TRACER_URL=localhost:14317
TRACER_RATIO=0.1
```
Open {% new-tab-link title="jaeger" href="http://localhost:16686/trace/" /%} and search by TraceID (correlationID) to see the trace.
{% figure src="/jaeger-traces.png" alt="Jaeger traces" /%}
#### 3. [OpenTelemetry Protocol](https://opentelemetry.io/docs/specs/otlp/):
The OpenTelemetry Protocol (OTLP) underlying gRPC is one of general-purpose telemetry data delivery protocol designed in the scope of the OpenTelemetry project.
Add OTLP configs in `.env` file, your .env will be updated to
```dotenv
# ... no change in other env variables
# tracing configs
TRACE_EXPORTER=otlp
TRACER_URL=localhost:4317
TRACER_RATIO=0.1
```
#### 4. [GoFr Tracer](https://tracer.gofr.dev/):
GoFr tracer is GoFr's own custom trace exporter as well as collector. Users can search a trace by its TraceID (correlationID)
in GoFr's own tracer service, available anywhere, anytime.
Add GoFr Tracer configs in `.env` file, your .env will be updated to
```dotenv
# ... no change in other env variables
# tracing configs
TRACE_EXPORTER=gofr
TRACER_RATIO=0.1
```
> [!NOTE]
> `TRACER_RATIO` refers to the proportion of traces that are exported through sampling. It ranges between 0 and 1. By default, this ratio is set to 1, meaning all traces are exported.
>
> Open {% new-tab-link title="gofr-tracer" href="https://tracer.gofr.dev/" /%} and search by TraceID (correlationID) to see the trace.
### Custom Authentication Headers
Many observability platforms require custom headers for authentication. GoFr supports this through the `TRACER_HEADERS` configuration, which accepts comma-separated `key=value` pairs following the OpenTelemetry standard format.
#### Usage Examples
**Single Header:**
```dotenv
# Honeycomb
TRACER_HEADERS="X-Honeycomb-Team=your_api_key"
```
**Multiple Headers:**
```dotenv
# Grafana Cloud with multiple headers
TRACER_HEADERS="Authorization=Basic base64encodedcreds,X-Scope-OrgID=tenant-1"
```
```dotenv
# API key with special characters
TRACER_HEADERS="X-Api-Key=secret123,Authorization=Bearer token"
```
#### Configuration Example
Here's an example for sending traces to Grafana Cloud with authentication:
```dotenv
APP_NAME=my-service
# Grafana Cloud OTLP endpoint with authentication
TRACE_EXPORTER=otlp
TRACER_URL=otlp-gateway-prod-us-east-0.grafana.net:443
TRACER_HEADERS="Authorization=Basic dXNlcm5hbWU6cGFzc3dvcmQ=,X-Scope-OrgID=123456"
TRACER_RATIO=1.0
```
================================================
FILE: docs/references/configs/page.md
================================================
# GoFr Configuration Options
This document lists all the configuration options supported by the GoFr framework. The configurations are grouped by category for better organization.
## App
{% table %}
- Name
- Description
- Default Value
---
- APP_NAME
- Name of the application
- gofr-app
---
- APP_ENV
- Name of the environment file to use (e.g., stage.env, prod.env, or local.env).
---
- APP_VERSION
- Application version
- dev
---
- LOG_LEVEL
- Level of verbosity for application logs. Supported values are **DEBUG, INFO, NOTICE, WARN, ERROR, FATAL**
- INFO
---
- REMOTE_LOG_URL
- URL to remotely change the log level
---
- REMOTE_LOG_FETCH_INTERVAL
- Time interval (in seconds) to check for remote log level updates
- 15
---
- METRICS_PORT
- Port on which the application exposes metrics
- 2121
---
- HTTP_PORT
- Port on which the HTTP server listens
- 8000
---
- GRPC_PORT
- Port on which the gRPC server listens
- 9000
---
- TRACE_EXPORTER
- Tracing exporter to use. Supported values: gofr, zipkin, jaeger, otlp.
---
- TRACER_HOST
- Hostname of the tracing collector. Required if TRACE_EXPORTER is set to zipkin or jaeger.
- **DEPRECATED**
---
- TRACER_PORT
- Port of the tracing collector. Required if TRACE_EXPORTER is set to zipkin or jaeger.
- 9411
- **DEPRECATED**
---
- TRACER_URL
- URL of the trace collector. Required if TRACE_EXPORTER is set to zipkin or jaeger.
---
- TRACER_RATIO
- Refers to the proportion of traces that are exported through sampling. It is optional configuration. By default, this ratio is set to 1.
---
- TRACER_AUTH_KEY
- Authorization header for trace exporter requests. Supported for zipkin, jaeger, otlp.
---
- TRACER_HEADERS
- Custom authentication headers for trace exporter requests in comma-separated key=value format (e.g., "X-Api-Key=secret,Authorization=Bearer token"). Supported for zipkin, jaeger, otlp. Takes priority over TRACER_AUTH_KEY.
---
- CMD_LOGS_FILE
- File to save the logs in case of a CMD application
---
- SHUTDOWN_GRACE_PERIOD
- Timeout duration for server shutdown process
- 30s
---
- GOFR_TELEMETRY
- Enable telemetry for GoFr framework usage
- true
---
- LOG_DISABLE_PROBES
- Disable log probes for health checks
- false
---
- GRPC_ENABLE_REFLECTION
- Enable gRPC server reflection
- false
{% /table %}
## HTTP
{% table %}
- Name
- Description
---
- REQUEST_TIMEOUT
- Set the request timeouts (in seconds) for HTTP server.
---
- CERT_FILE
- Set the path to your PEM certificate file for the HTTPS server to establish a secure connection.
---
- KEY_FILE
- Set the path to your PEM key file for the HTTPS server to establish a secure connection.
{% /table %}
## Datasource
### SQL
{% table %}
- Name
- Description
- Default Value
---
- DB_DIALECT
- Database dialect. Supported values: mysql, postgres, supabase
---
- DB_HOST
- Hostname of the database server.
---
- DB_PORT
- Port of the database server.
- 3306
---
- DB_USER
- Username for the database.
---
- DB_PASSWORD
- Password for the database.
---
- DB_NAME
- Name of the database to use.
---
- DB_MAX_IDLE_CONNECTION
- Number of maximum idle connection.
- 2
---
- DB_MAX_OPEN_CONNECTION
- Number of maximum connections which can be used with database.
- 0 (unlimited)
---
- DB_SSL_MODE
- TLS/SSL mode for database connections. Supported modes: **disable** (no TLS), **preferred** (attempts TLS, falls back to plain), **require** (enforces TLS, skips validation), **skip-verify** (enforces TLS, no certificate validation), **verify-ca** (enforces TLS, validates certificate against CA), **verify-full** (enforces TLS with full validation including hostname). Currently supported for MySQL/MariaDB and PostgreSQL.
- disable
---
- DB_TLS_CA_CERT
- Path to CA certificate file for TLS connections. Required for **verify-ca** and **verify-full** SSL modes.
- None
---
- DB_TLS_CLIENT_CERT
- Path to client certificate file for mutual TLS authentication.
- None
---
- DB_TLS_CLIENT_KEY
- Path to client private key file for mutual TLS authentication.
- None
---
- DB_REPLICA_HOSTS
- Comma-separated list of replica database hosts. Used for read replicas.
- None
---
- DB_REPLICA_PORTS
- Comma-separated list of replica database ports. Used for read replicas.
- None
---
- DB_REPLICA_USERS
- Comma-separated list of replica database users. Used for read replicas.
- None
---
- DB_REPLICA_PASSWORDS_
- Comma-separated list of replica database passwords. Used for read replicas.
- None
---
- DB_REPLICA_MAX_IDLE_CONNECTIONS
- Maximum idle connections allowed for a replica
- 50
---
- DB_REPLICA_MIN_IDLE_CONNECTIONS
- Minimum idle connections for a replica
- 10
---
- DB_REPLICA_DEFAULT_IDLE_CONNECTIONS
- Idle connections used if no primary setting is provided
- 10
---
- DB_REPLICA_MAX_OPEN_CONNECTIONS
- Maximum open connections allowed for a replica
- 200
---
- DB_REPLICA_MIN_OPEN_CONNECTIONS
- Minimum open connections for a replica
- 50
---
- DB_REPLICA_DEFAULT_OPEN_CONNECTIONS
- Open connections used if no primary setting is provided
- 100
---
- DB_CHARSET
- The character set for database connection
- utf8
---
- SUPABASE_CONNECTION_TYPE
- Connection type to Supabase. Supported values: direct, session, transaction
- direct
---
- SUPABASE_PROJECT_REF
- Supabase project reference ID
---
- SUPABASE_REGION
- Supabase region for pooled connections
---
- DB_URL
- Full PostgreSQL connection string for Supabase (alternative to separate config parameters)
{% /table %}
### Redis
{% table %}
- Name
- Description
- Default Value
---
- REDIS_HOST
- Hostname of the Redis server.
- localhost
---
- REDIS_PORT
- Port of the Redis server.
- 6379
---
- REDIS_USER
- Username for the Redis server (optional).
- ""
---
- REDIS_PASSWORD
- Password for the Redis server (optional).
- ""
---
- REDIS_DB
- Database number to use for the Redis server.
- 0
---
- REDIS_TLS_ENABLED
- Enable TLS for Redis connections.
- false
---
- REDIS_TLS_CA_CERT
- Path to the TLS CA certificate file for Redis (or PEM-encoded string).
- ""
---
- REDIS_TLS_CERT
- Path to the TLS certificate file for Redis (or PEM-encoded string).
- ""
---
- REDIS_TLS_KEY
- Path to the TLS key file for Redis (or PEM-encoded string).
- ""
{% /table %}
**Redis PubSub Configuration:**
{% table %}
- Name
- Description
- Default Value
---
- REDIS_PUBSUB_DB
- Redis database number to use only for Redis Pub/Sub (when `PUBSUB_BACKEND=REDIS`). Use a different DB than `REDIS_DB` when running GoFr migrations with Redis Streams mode to avoid `gofr_migrations` key-type collisions.
- Default: `15` (highest default Redis database, 0-15)
---
- REDIS_PUBSUB_MODE
- Operation mode: `pubsub` or `streams`.
- streams
---
- REDIS_STREAMS_CONSUMER_GROUP
- Consumer group name (required for streams mode).
- ""
---
- REDIS_STREAMS_CONSUMER_NAME
- Unique consumer name (optional, auto-generated if empty).
- ""
---
- REDIS_STREAMS_BLOCK_TIMEOUT
- Blocking duration for reading new messages using Redis `XREADGROUP`. Lower values (1s-2s) provide faster detection but increase CPU usage. Higher values (10s-30s) reduce CPU usage, ideal for batch processing.
- 5s
---
- REDIS_STREAMS_PEL_RATIO
- Ratio of PEL (pending) messages to read vs new messages (0.0-1.0). Controls balance between retry and fresh messages. 0.7 = 70% PEL, 30% new.
- 0.7
---
- REDIS_STREAMS_MAXLEN
- Maximum length of the stream (approximate). Prevents streams from growing indefinitely. Set to `0` for unlimited.
- 0 (unlimited)
{% /table %}
> **Note**: When using GoFr migrations with Streams mode, keep `REDIS_DB` and `REDIS_PUBSUB_DB` separate (defaults: 0 and 15). For `REDIS_STREAMS_BLOCK_TIMEOUT`: use 1s-2s for real-time or 10s-30s for batch processing.
### Pub/Sub
{% table %}
- Name
- Description
- Default Value
---
- PUBSUB_BACKEND
- Pub/Sub message broker backend
- kafka, google, mqtt, nats, redis
{% /table %}
**Kafka**
{% table %}
- Name
- Description
- Default Value
---
- PUBSUB_BROKER
- Comma-separated list of broker addresses
- localhost:9092
---
- PARTITION_SIZE
- Size of each message partition (in bytes)
- 0
---
- PUBSUB_OFFSET
- Offset to start consuming messages from. -1 for earliest, 0 for latest.
- -1
---
- KAFKA_BATCH_SIZE
- Number of messages to batch before sending to Kafka
- 1
---
- KAFKA_BATCH_BYTES
- Number of bytes to batch before sending to Kafka
- 1048576
---
- KAFKA_BATCH_TIMEOUT
- Time to wait before sending a batch to Kafka
- 100ms
---
- CONSUMER_ID
- Unique identifier for this consumer
- gofr-consumer
---
---
- KAFKA_SECURITY_PROTOCOL
- Security protocol used to communicate with Kafka (e.g., PLAINTEXT, SSL, SASL_PLAINTEXT, SASL_SSL)
- PLAINTEXT
---
- KAFKA_SASL_MECHANISM
- SASL mechanism for authentication (e.g. PLAIN, SCRAM-SHA-256, SCRAM-SHA-512)
- None
---
- KAFKA_SASL_USERNAME
- Username for SASL authentication
- None
---
- KAFKA_SASL_PASSWORD
- Password for SASL authentication
- None
---
- KAFKA_TLS_CERT_FILE
- Path to the TLS certificate file
- None
---
- KAFKA_TLS_KEY_FILE
- Path to the TLS key file
- None
---
- KAFKA_TLS_CA_CERT_FILE
- Path to the TLS CA certificate file
- None
---
- KAFKA_TLS_INSECURE_SKIP_VERIFY
- Skip TLS certificate verification
- false
{% /table %}
**Google**
{% table %}
- Name
- Description
---
- GOOGLE_PROJECT_ID
- ID of the Google Cloud project. Required for Google Pub/Sub.
---
- GOOGLE_SUBSCRIPTION_NAME
- Name of the Google Pub/Sub subscription. Required for Google Pub/Sub.
{% /table %}
**MQTT**
{% table %}
- Name
- Description
- Default Value
---
- MQTT_PORT
- Port of the MQTT broker
- 1883
---
- MQTT_MESSAGE_ORDER
- Enable guaranteed message order
- false
---
- MQTT_PROTOCOL
- Communication protocol. Supported values: tcp, ssl.
- tcp
---
- MQTT_HOST
- Hostname of the MQTT broker
- localhost
---
- MQTT_USER
- Username for the MQTT broker
---
- MQTT_PASSWORD
- Password for the MQTT broker
---
- MQTT_CLIENT_ID_SUFFIX
- Suffix appended to the client ID
---
- MQTT_QOS
- Quality of Service Level
---
- MQTT_KEEP_ALIVE
- Sends regular messages to check the link is active. May not work as expected if handling func is blocking execution
- MQTT_RETRIEVE_RETAINED
- Retrieve retained messages on subscription
{% /table %}
**NATS JetStream**
{% table %}
- Name
- Description
- Default Value
---
- NATS_SERVER
- URL of the NATS server
- nats://localhost:4222
---
- NATS_CREDS_FILE
- File containing the NATS credentials
- creds.json
{% /table %}
================================================
FILE: docs/references/context/page.md
================================================
# GoFr Context
GoFr context is an object injected by the GoFr handler. It contains all the request-specific data, for each
request-response cycle a new context is created. The request can be either an HTTP request, gRPC call or
a message from Pub-Sub.
GoFr Context also embeds the **_container_** which maintains all the dependencies like databases, logger, HTTP service clients,
metrics manager, etc. This reduces the complexity of the application as users don't have to maintain and keep track of
all the dependencies by themselves.
GoFr context is an extension of the Go context, providing a wrapper around the request and response providing
user access to dependencies.
# Usage
## Reading HTTP requests
`ctx.Request` can be used to access the underlying request which provides the following methods to access different
parts of the request.
- `Context()` - to access the context associated with the incoming request
```go
ctx.Request.Context()
```
- `Param(string)` - to access the query parameters present in the request, it returns the value of the key provided
```go
// Example: Request is /configs?key1=value1&key2=value2
value := ctx.Request.Param("key1")
// value = "value1"
```
- `PathParam(string)` - to retrieve the path parameters
```go
// Consider the path to be /employee/{id}
id := ctx.Request.PathParam("id")
```
- `Bind(any)` - to access a decoded format of the request body, the body is mapped to the interface provided
```go
// incoming request body is
// {
// "name" : "trident",
// "category" : "snacks"
// }
type product struct{
Name string `json:"name"`
Category string `json:"category"`
}
var p product
ctx.Bind(&p)
// the Bind() method will map the incoming request to variable p
```
- `Binding multipart-form data / urlencoded form data `
- To bind multipart-form data or url-encoded form, we 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 {
Name string `form:"name"`
Compressed file.Zip `file:"upload"`
FileHeader *multipart.FileHeader `file:"file_upload"`
}
```
- The `form` tag is used to bind non-file fields.
- The `file` tag is used to bind file fields. If the tag is not present, the field name is used as the key.
- `HostName()` - to access the host name for the incoming request
```go
// for example if request is made from xyz.com
host := ctx.Request.HostName()
// the host would be http://xyz.com
// Note: the protocol if not provided in the headers will be set to http by default
```
- `Params(string)` - to access all query parameters for a given key returning slice of strings.
```go
// Example: Request is /search?category=books,electronics&category=tech
values := ctx.Request.Params("category")
// values = []string{"books", "electronics", "tech"}
```
## Accessing Authentication Information
GoFr provides a helper method to access authentication details from the context.
These values are populated when the respective authentication middleware is enabled (see [HTTP Auth Middleware](https://github.com/gofr-dev/gofr/blob/0845d19181d2cc55e12c557fc9ad51adb4ab44fd/examples/using-http-auth-middleware/ReadMe.md) section).
```go
info := ctx.GetAuthInfo()
```
### Methods
* **`GetClaims()`** – Returns the JWT claims containing standard fields such as:
* `Issuer` – identifies who issued the token.
* `Subject` – identifies the principal that is the subject of the token.
* `Audience` – identifies the intended recipients of the token.
* `NotBefore` – time before which the token is not valid.
* `IssuedAt` – time at which the token was issued.
* `ExpirationTime` – time after which the token expires.
**Requires:** OAuth middleware (`EnableOAuth`)
* **`GetUsername()`** – Returns the authenticated username when using Basic Authentication.
**Requires:** Basic Auth middleware (`EnableBasicAuthWithValidator`)
* **`GetAPIKey()`** – Returns the API key used for authentication.
**Requires:** API Key middleware (`EnableAPIKeyAuthWithValidator`)
> Note: These values will be available only if the respective authentication middleware is enabled in the application.
## Accessing dependencies
GoFr context embeds the container object which provides access to
all the injected dependencies by the users. Users can access the fields and methods provided
by the **_container_**.
================================================
FILE: docs/references/gofrcli/page.md
================================================
# GoFR Command Line Interface
Managing repetitive tasks and maintaining consistency across large-scale applications is challenging!
**GoFr CLI provides the following:**
* All-in-one command-line tool designed specifically for GoFr applications
* Simplifies **database migrations** management
* **Store Layer Generator** for type-safe data access code from YAML configurations
* Abstracts **tracing**, **metrics** and structured **logging** for GoFr's gRPC server/client
* Enforces standard **GoFr conventions** in new projects
## Prerequisites
- Go 1.22 or above. To check Go version use the following command:
```bash
go version
```
## **Installation**
To get started with GoFr CLI, use the below commands
```bash
go install gofr.dev/cli/gofr@latest
```
To check the installation:
```bash
gofr version
```
---
## Usage
The CLI can be run directly from the terminal after installation. Here’s the general syntax:
```bash
gofr [flags]=[arguments]
```
---
## **Commands**
## 1. ***`init`***
The init command initializes a new GoFr project. It sets up the foundational structure for the project and generates a basic "Hello World!" program as a starting point. This allows developers to quickly dive into building their application with a ready-made structure.
### Command Usage
```bash
gofr init
```
---
## 2. ***`migrate create`***
The migrate create command generates a migration template file with pre-defined structure in your migrations directory.
This boilerplate code helps you maintain consistent patterns when writing database schema modifications across your project.
### Command Usage
```bash
gofr migrate create -name=
```
### Example Usage
```bash
gofr migrate create -name=create_employee_table
```
This command generates a migration directory which has the below files:
1. A new migration file with timestamp prefix (e.g., `20250127152047_create_employee_table.go`) containing:
```go
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func create_employee_table() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
// write your migrations here
return nil
},
}
}
```
2. An auto-generated all.go file that maintains a registry of all migrations:
```go
// This is auto-generated file using 'gofr migrate' tool. DO NOT EDIT.
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate {
20250127152047: create_employee_table(),
}
}
```
> **💡 Best Practice:** Learn about [organizing migrations by feature](../../advanced-guide/handling-data-migrations#organizing-migrations-by-feature) to avoid creating one migration per table or operation.
For detailed instructions on handling database migrations, see the [handling-data-migrations documentation](../../advanced-guide/handling-data-migrations)
For more examples, see the [using-migrations](https://github.com/gofr-dev/gofr/tree/main/examples/using-migrations)
---
## 3. ***`wrap grpc`***
* The gofr wrap grpc command streamlines gRPC integration in a GoFr project by generating GoFr's context-aware structures.
* It simplifies setting up gRPC handlers with minimal steps, and accessing datasources, adding tracing as well as custom metrics. Based on the proto file it creates the handler/client with GoFr's context.
For detailed instructions on using grpc with GoFr see the [gRPC documentation](../../advanced-guide/grpc)
### Command Usage
**gRPC Server**
```bash
gofr wrap grpc server --proto=
```
### Generated Files
**Server**
- ```{serviceName}_gofr.go (auto-generated; do not modify)```
- ```{serviceName}_server.go (example structure below)```
### Example Usage
**gRPC Server**
The command generates a server implementation template similar to this:
```go
package server
import (
"gofr.dev/pkg/gofr"
)
// Register the gRPC service in your app using the following code in your main.go:
//
// service.Register{ServiceName}ServerWithGofr(app, &server.{ServiceName}Server{})
//
// {ServiceName}Server defines the gRPC server implementation.
// Customize the struct with required dependencies and fields as needed.
type {ServiceName}Server struct {
}
// Example method (actual methods will depend on your proto file)
func (s *MyServiceServer) MethodName(ctx *gofr.Context) (any, error) {
// Replace with actual logic if needed
return &ServiceResponse{
}, nil
}
```
For detailed instruction on setting up a gRPC server with GoFr see the [gRPC Server Documentation](https://gofr.dev/docs/advanced-guide/grpc#generating-g-rpc-server-handler-template-using)
**gRPC Client**
```bash
gofr wrap grpc client --proto=
```
**Client**
- ```{serviceName}_client.go (example structure below)```
### Example Usage:
Assuming the service is named hello, after generating the hello_client.go file, you can seamlessly register and access the gRPC service using the following steps:
```go
type GreetHandler struct {
helloGRPCClient client.HelloGoFrClient
}
func NewGreetHandler(helloClient client.HelloGoFrClient) *GreetHandler {
return &GreetHandler{
helloGRPCClient: helloClient,
}
}
func (g GreetHandler) Hello(ctx *gofr.Context) (any, error) {
userName := ctx.Param("name")
helloResponse, err := g.helloGRPCClient.SayHello(ctx, &client.HelloRequest{Name: userName})
if err != nil {
return nil, err
}
return helloResponse, nil
}
func main() {
app := gofr.New()
// Create a gRPC client for the Hello service
helloGRPCClient, err := client.NewHelloGoFrClient(app.Config.Get("GRPC_SERVER_HOST"), app.Metrics())
if err != nil {
app.Logger().Errorf("Failed to create Hello gRPC client: %v", err)
return
}
greetHandler := NewGreetHandler(helloGRPCClient)
// Register HTTP endpoint for Hello service
app.GET("/hello", greetHandler.Hello)
// Run the application
app.Run()
}
```
For detailed instruction on setting up a gRPC server with GoFr see the [gRPC Client Documentation](https://gofr.dev/docs/advanced-guide/grpc#generating-tracing-enabled-g-rpc-client-using)
For more examples refer [gRPC Examples](https://github.com/gofr-dev/gofr/tree/main/examples/grpc)
---
## 4. ***`store`***
> **Available since:** `gofr-cli` **v0.8.1**
The `gofr store` command is a code generator that creates type-safe data access layers from YAML configuration files. It eliminates boilerplate code while maintaining GoFr's best practices for observability and context management.
### **Features**
* **YAML-Driven Configuration**: Define your data models and queries in a simple, declarative format.
* **Type-Safe Code Generation**: Generates Go interfaces and implementation boilerplates.
* **GoFr Context Integration**: Generated methods work with `*gofr.Context` for built-in observability.
* **Multiple Stores**: Define all stores in a single YAML file — each gets its own directory.
* **Store Registry**: Centralized factory management of all generated stores via `stores/all.go`.
### **Commands**
#### **Initialize Store Configuration**
Create a new store directory and a `store.yaml` configuration template. **The `-name` flag is required.**
```bash
gofr store init -name=
```
**Example:**
```bash
gofr store init -name=user
```
This creates the following structure:
- `stores/store.yaml` — Configuration file template (shared across all stores).
- `stores/all.go` — Store registry factory (auto-generated, DO NOT EDIT).
- `stores/user/interface.go` — Initial interface stub (DO NOT EDIT — regenerated by `generate`).
- `stores/user/user.go` — Initial implementation stub (editable — add your SQL logic here).
#### **Generate Store Code**
Generate or update Go code from your store configuration file.
```bash
gofr store generate
```
> **💡 Note:** By default, this command looks for the configuration at **`stores/store.yaml`**. To use a different path, use the `-config` flag:
> ```bash
> gofr store generate -config=path/to/store.yaml
> ```
---
### **Quick Start Example**
**Step 1: Initialize Configuration**
```bash
gofr store init -name=user
```
**Step 2: Define Your Store in `stores/store.yaml`**
```yaml
version: "1.0"
stores:
- name: "user"
package: "user"
output_dir: "stores/user"
interface: "UserStore"
implementation: "userStore"
queries:
- name: "GetUserByID"
sql: "SELECT id, name, email FROM users WHERE id = ?"
type: "select"
model: "User"
returns: "single"
params:
- name: "id"
type: "int64"
description: "Retrieves a user by their ID"
- name: "GetAllUsers"
sql: "SELECT id, name, email FROM users"
type: "select"
model: "User"
returns: "multiple"
description: "Retrieves all users"
models:
- name: "User"
fields:
- name: "ID"
type: "int64"
tag: 'db:"id" json:"id"'
- name: "Name"
type: "string"
tag: 'db:"name" json:"name"'
- name: "Email"
type: "string"
tag: 'db:"email" json:"email"'
```
**Step 3: Generate Store Code**
```bash
gofr store generate
```
This generates:
```text
stores/
├── store.yaml # Central Configuration
├── all.go # Store registry factory (auto-generated)
└── user/
├── interface.go # UserStore interface definition
├── userStore.go # userStore implementation boilerplate
└── user.go # User model struct
```
**Step 4: Use in Your Application**
```go
package main
import (
"gofr.dev/pkg/gofr"
"your-project/stores/user"
)
func main() {
app := gofr.New()
userStore := user.NewUserStore()
app.GET("/users/{id}", func(ctx *gofr.Context) (interface{}, error) {
id, _ := strconv.ParseInt(ctx.PathParam("id"), 10, 64)
return userStore.GetUserByID(ctx, id)
})
app.GET("/users", func(ctx *gofr.Context) (interface{}, error) {
return userStore.GetAllUsers(ctx)
})
app.Run()
}
```
---
### **Multiple Stores in One File**
You can define all stores in a single YAML file. Each store gets its own output directory and all are registered into the same `stores/all.go` registry.
```yaml
version: "1.0"
stores:
- name: "user"
package: "user"
output_dir: "stores/user"
interface: "UserStore"
implementation: "userStore"
queries: [...]
- name: "product"
package: "product"
output_dir: "stores/product"
interface: "ProductStore"
implementation: "productStore"
queries: [...]
models:
- name: "User"
fields: [...]
- name: "Product"
fields: [...]
```
**Generated structure:**
```text
stores/
├── all.go
├── user/
│ ├── interface.go
│ ├── userStore.go
│ └── user.go
└── product/
├── interface.go
├── productStore.go
└── product.go
```
**Using the registry with multiple stores:**
```go
import (
"your-project/stores"
"your-project/stores/user"
"your-project/stores/product"
)
// stores.GetStore returns a factory-created instance
userStore := stores.GetStore("user").(user.UserStore)
productStore := stores.GetStore("product").(product.ProductStore)
```
> **💡 Note:** `stores.All()` returns a `map[string]func() any` — a map of **factory functions**, not active instances. `stores.GetStore(name)` calls the factory for you and returns the instance.
---
### **Configuration Reference**
#### **Store Configuration**
| Field | Description | Required |
|-------|-------------|----------|
| `name` | Store identifier used in the registry key. | **Yes** |
| `package` | Go package name for generated code. | **Yes** |
| `output_dir` | Directory path where files will be generated. | Optional (defaults to `stores/`) |
| `interface` | Interface name — **recommended: `Store`** (e.g., `UserStore`). | Optional (defaults to `Store`) |
| `implementation` | Private struct name for the implementation (e.g., `userStore`). | Optional (defaults to `Store`) |
| `queries` | List of database queries. | Optional |
> **⚠️ Naming Convention:** The registry (`stores/all.go`) uses a hardcoded `Store` pattern when generating constructor calls (e.g., `NewUserStore()`). Always name your interface as `Store` to avoid compilation errors.
#### **Query Types**
* **`select`** — SELECT queries.
* **`insert`** — INSERT queries.
* **`update`** — UPDATE queries.
* **`delete`** — DELETE queries.
#### **Return Types**
* **`single`** — Returns `(Model, error)`.
* **`multiple`** — Returns `([]Model, error)`.
* **`count`** — Returns `(int64, error)`.
* **`custom`** — Returns `(any, error)`.
#### **Query Parameters**
```yaml
params:
- name: "id"
type: "int64"
- name: "email"
type: "string"
```
Supported parameter types include all Go primitive types, `time.Time`, and pointer types (e.g., `*int64`).
---
### **Model Generation**
#### **Generate New Models**
```yaml
models:
- name: "User"
fields:
- name: "ID"
type: "int64"
tag: 'db:"id" json:"id"'
- name: "Name"
type: "string"
tag: 'db:"name" json:"name"'
- name: "CreatedAt"
type: "time.Time"
tag: 'db:"created_at" json:"created_at"'
```
This generates:
```go
type User struct {
ID int64 `db:"id" json:"id"`
Name string `db:"name" json:"name"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
}
func (User) TableName() string {
return "user"
}
```
#### **Reference Existing Models**
If you already have models defined elsewhere:
```yaml
models:
- name: "User"
path: "../models/user.go"
package: "your-project/models"
```
---
### **Generated Code Structure**
#### **Interface (`interface.go`)**
```go
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
package user
import "gofr.dev/pkg/gofr"
type UserStore interface {
GetUserByID(ctx *gofr.Context, id int64) (User, error)
GetAllUsers(ctx *gofr.Context) ([]User, error)
}
```
#### **Implementation (`userStore.go`)**
```go
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
package user
type userStore struct{}
func NewUserStore() UserStore {
return &userStore{}
}
func (s *userStore) GetUserByID(ctx *gofr.Context, id int64) (User, error) {
// TODO: Implement using ctx.SQL()
var result User
// err := ctx.SQL().QueryRowContext(ctx, sql, id).Scan(&result.ID, ...)
return result, nil
}
func (s *userStore) GetAllUsers(ctx *gofr.Context) ([]User, error) {
// TODO: Implement using ctx.SQL()
return []User{}, nil
}
```
---
### **Best Practices**
1. **Implement the TODOs**: The generator creates method **signatures and boilerplate only**. You must fill in the `// TODO: Implement` sections with actual SQL execution using `ctx.SQL()` methods.
2. **Use `Store` Interface Names**: The registry assumes this convention. E.g., `interface: "UserStore"` results in the constructor `NewUserStore()` and type assertion `.(user.UserStore)`.
3. **One YAML, Many Stores**: Define all your stores in a single `store.yaml` to keep your data access layer centrally configured.
4. **Know Which Files Are Auto-Generated**: Only `interface.go` and `all.go` are marked `DO NOT EDIT` and are overwritten on every `gofr store generate`. The implementation stub (`.go`) created by `gofr store init` is editable — this is where you add your SQL logic. The `userStore.go` generated by `gofr store generate` is also editable boilerplate.
5. **Version Control**: Always commit your `store.yaml`. Re-run `gofr store generate` after any configuration change to sync the generated interfaces.
---
### **Complete Example**
For a complete working example of the store generator, see the [store example](https://github.com/gofr-dev/gofr-cli/tree/main/store/example.yaml) in the gofr-cli repository.
For detailed configuration options and advanced usage, refer to the [Store Generator README](https://github.com/gofr-dev/gofr-cli/blob/main/store/README.md).
================================================
FILE: docs/references/testing/page.md
================================================
# Testing REST APIs with GoFr
Testing REST APIs ensures that your endpoints function correctly under various conditions. This guide demonstrates how to write tests for GoFr-based REST APIs.
## Mocking Databases in GoFr
Mocking databases allows for isolated testing by simulating various scenarios. GoFr's built-in mock container supports, not only SQL databases, but also extends to other data stores, including Redis, Cassandra, Key-Value stores, MongoDB, and ClickHouse.
## Example of Unit Testing a REST API Using GoFr
Below is an example of how to test, say the `Add` method of a handler that interacts with a SQL database.
Here’s an `Add` function for adding a book to the database using GoFr:
```go
// main.go
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http"
)
type Book struct {
Id int `json:"id"`
ISBN int `json:"isbn"`
Title string `json:"title"`
}
func Add(ctx *gofr.Context) (any, error) {
var book Book
if err := ctx.Bind(&book); err != nil {
ctx.Logger.Errorf("error in binding: %v", err)
return nil, http.ErrorInvalidParam{Params: []string{"body"}}
}
// we assume the `id` column in the database is set to auto-increment.
res, err := ctx.SQL.ExecContext(ctx, `INSERT INTO books (title, isbn) VALUES (?, ?)`, book.Title, book.ISBN)
if err != nil {
return nil, err
}
id, err := res.LastInsertId()
if err != nil {
return nil, err
}
return id, nil
}
func main() {
// initialize gofr object
app := gofr.New()
app.POST("/book", Add)
// Run the application
app.Run()
}
```
Here’s how to write tests using GoFr:
```go
// main_test.go
package main
import (
"bytes"
"context"
"database/sql"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
gofrHttp "gofr.dev/pkg/gofr/http"
)
func TestAdd(t *testing.T) {
type gofrResponse struct {
result any
err error
}
// NewMockContainer provides mock implementations for various databases including:
// Redis, SQL, ClickHouse, Cassandra, MongoDB, and KVStore.
// These mock can be used to define database expectations in unit tests,
// similar to the SQL example demonstrated here.
mockContainer, mock := container.NewMockContainer(t)
ctx := &gofr.Context{
Context: context.Background(),
Request: nil,
Container: mockContainer,
}
tests := []struct {
name string
requestBody string
mockExpect func()
expectedResponse any
}{
{
name: "Error while Binding",
requestBody: `title":"Book Title","isbn":12345}`,
mockExpect: func() {
},
expectedResponse: gofrResponse{
nil,
gofrHttp.ErrorInvalidParam{Params: []string{"body"}}},
},
{
name: "Successful Insertion",
requestBody: `{"title":"Book Title","isbn":12345}`,
mockExpect: func() {
mock.SQL.
ExpectExec(`INSERT INTO books (title, isbn) VALUES (?, ?)`).
WithArgs("Book Title", 12345).
WillReturnResult(sqlmock.NewResult(12, 1))
},
expectedResponse: gofrResponse{
int64(12),
nil,
},
},
{
name: "Error on Insertion",
requestBody: `{"title":"Book Title","isbn":12345}`,
mockExpect: func() {
mock.SQL.
ExpectExec(`INSERT INTO books (title, isbn) VALUES (?, ?)`).
WithArgs("Book Title", 12345).
WillReturnError(sql.ErrConnDone)
},
expectedResponse: gofrResponse{
nil,
sql.ErrConnDone},
},
{
name: "Error while fetching LastInsertId",
requestBody: `{"title":"Book Title","isbn":12345}`,
mockExpect: func() {
mock.SQL.
ExpectExec(`INSERT INTO books (title, isbn) VALUES (?, ?)`).
WithArgs("Book Title", 12345).
WillReturnError(errors.New("mocked result error"))
},
expectedResponse: gofrResponse{
nil,
errors.New("mocked result error")},
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockExpect()
var req *http.Request
req = httptest.NewRequest(
http.MethodPost,
"/book",
bytes.NewBuffer([]byte(tt.requestBody)),
)
req.Header.Set("Content-Type", "application/json")
request := gofrHttp.NewRequest(req)
ctx.Request = request
val, err := Add(ctx)
response := gofrResponse{val, err}
assert.Equal(t, tt.expectedResponse, response, "TEST[%d], Failed.\n%s", i, tt.name)
})
}
}
```
## Testing HTTP Handlers with Mock Services
When you register multiple services with `WithMockHTTPService`, each service gets its own separate mock instance. This allows you to set different expectations for each service using the `mocks.HTTPServices` map. Use table-driven tests to cover multiple scenarios:
### Important Notes
- **Context Matching**: Always use the exact context from your `gofr.Context` (`ctx.Context`) in expectations. gomock compares contexts by reference, not value, so using `t.Context()` or `context.Background()` will fail.
- **Service Registration**: `WithMockHTTPService("serviceName")` registers the service with the specified name. Each service gets its own separate mock instance.
- **Multiple Services**: Use `mocks.HTTPServices["serviceName"]` to access and set different expectations for each service. Each service has its own mock instance, so expectations are independent.
- **Tests will fail** if the mocked HTTPService is not called as expected or if the context doesn't match.
```go
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
gofrHttp "gofr.dev/pkg/gofr/http"
)
// Handler that calls multiple HTTP services
// This handler demonstrates calling two different services (paymentService and shippingService)
// to fetch order details from different parts of the system.
func OrderDetailsHandler(ctx *gofr.Context) (any, error) {
orderID := ctx.PathParam("id")
if orderID == "" {
return nil, errors.New("order ID is required")
}
// First HTTP service call: Get payment details from paymentService
paymentService := ctx.GetHTTPService("paymentService")
paymentResp, err := paymentService.Get(ctx.Context, "/payments/"+orderID, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch payment details: %w", err)
}
defer paymentResp.Body.Close()
var paymentData struct {
Status string `json:"status"`
Amount int `json:"amount"`
}
paymentBody, err := io.ReadAll(paymentResp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read payment response: %w", err)
}
if err := json.Unmarshal(paymentBody, &paymentData); err != nil {
return nil, fmt.Errorf("failed to parse payment response: %w", err)
}
// Second HTTP service call: Get shipping details from shippingService
shippingService := ctx.GetHTTPService("shippingService")
shippingResp, err := shippingService.Get(ctx.Context, "/shipping/"+orderID, nil)
if err != nil {
return nil, fmt.Errorf("failed to fetch shipping details: %w", err)
}
defer shippingResp.Body.Close()
var shippingData struct {
Status string `json:"status"`
Tracking string `json:"tracking"`
EstimatedDelivery string `json:"estimated_delivery"`
}
shippingBody, err := io.ReadAll(shippingResp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read shipping response: %w", err)
}
if err := json.Unmarshal(shippingBody, &shippingData); err != nil {
return nil, fmt.Errorf("failed to parse shipping response: %w", err)
}
// Combine results from both services
return map[string]any{
"order_id": orderID,
"payment_status": paymentData.Status,
"payment_amount": paymentData.Amount,
"shipping_status": shippingData.Status,
"tracking_number": shippingData.Tracking,
"estimated_delivery": shippingData.EstimatedDelivery,
}, nil
}
func TestOrderDetailsHandler(t *testing.T) {
// Helper function to create test context with path parameters
createTestContext := func(path string, container *container.Container) *gofr.Context {
req := httptest.NewRequest(http.MethodGet, path, nil)
// Set path parameters using mux.SetURLVars (required for ctx.PathParam to work)
if strings.Contains(path, "/orders/") {
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) >= 2 && parts[1] != "" {
req = mux.SetURLVars(req, map[string]string{"id": parts[1]})
}
}
return &gofr.Context{
Context: req.Context(),
Request: gofrHttp.NewRequest(req),
Container: container,
}
}
const testOrderID = "12345" // Reusable order ID for tests
tests := []struct {
name string
setupMocks func(*container.Mocks, *gofr.Context)
requestPath string
wantErr bool
wantErrMsg string
validateResult func(*testing.T, any)
}{
{
name: "successful order details retrieval",
setupMocks: func(mocks *container.Mocks, ctx *gofr.Context) {
// Set up expectation for paymentService - this is the first HTTP call in the handler
paymentResp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"status":"completed","amount":1500}`)),
}
mocks.HTTPServices["paymentService"].EXPECT().Get(
ctx.Context,
"/payments/"+testOrderID,
nil,
).Return(paymentResp, nil)
// Set up expectation for shippingService - this is the second HTTP call in the handler
// Note: Each service has its own independent mock instance
shippingResp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"status":"in_transit","tracking":"TRACK123","estimated_delivery":"2024-12-25"}`)),
}
mocks.HTTPServices["shippingService"].EXPECT().Get(
ctx.Context,
"/shipping/"+testOrderID,
nil,
).Return(shippingResp, nil)
},
requestPath: "/orders/" + testOrderID,
wantErr: false,
validateResult: func(t *testing.T, result any) {
resultMap := result.(map[string]any)
assert.Equal(t, testOrderID, resultMap["order_id"])
assert.Equal(t, "completed", resultMap["payment_status"])
assert.Equal(t, 1500, resultMap["payment_amount"])
assert.Equal(t, "in_transit", resultMap["shipping_status"])
assert.Equal(t, "TRACK123", resultMap["tracking_number"])
assert.Equal(t, "2024-12-25", resultMap["estimated_delivery"])
},
},
{
name: "payment service error",
setupMocks: func(mocks *container.Mocks, ctx *gofr.Context) {
// Payment service returns an error - handler should fail before calling shipping service
mocks.HTTPServices["paymentService"].EXPECT().Get(
ctx.Context,
"/payments/"+testOrderID,
nil,
).Return(nil, errors.New("payment service unavailable"))
// Shipping service should NOT be called when payment service fails
// No expectation set for shippingService - test will fail if it's called
},
requestPath: "/orders/" + testOrderID,
wantErr: true,
wantErrMsg: "failed to fetch payment details",
},
{
name: "shipping service error",
setupMocks: func(mocks *container.Mocks, ctx *gofr.Context) {
// Payment service succeeds
paymentResp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"status":"completed","amount":1500}`)),
}
mocks.HTTPServices["paymentService"].EXPECT().Get(
ctx.Context,
"/payments/"+testOrderID,
nil,
).Return(paymentResp, nil)
// Shipping service returns an error - this is the second HTTP call
mocks.HTTPServices["shippingService"].EXPECT().Get(
ctx.Context,
"/shipping/"+testOrderID,
nil,
).Return(nil, errors.New("shipping service unavailable"))
},
requestPath: "/orders/" + testOrderID,
wantErr: true,
wantErrMsg: "failed to fetch shipping details",
},
{
name: "missing order ID",
setupMocks: func(mocks *container.Mocks, ctx *gofr.Context) {
// No service calls should be made when order ID is missing
},
requestPath: "/orders/",
wantErr: true,
wantErrMsg: "order ID is required",
},
}
// Register HTTP services once - each service gets its own separate mock instance
// Since all test cases use the same services, we can create the mock container outside the loop
mockContainer, mocks := container.NewMockContainer(t,
container.WithMockHTTPService("paymentService", "shippingService"),
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create test context using helper function
ctx := createTestContext(tt.requestPath, mockContainer)
// Set up mock expectations BEFORE calling the handler
// Each service's expectations are independent
tt.setupMocks(mocks, ctx)
// Call the handler
result, err := OrderDetailsHandler(ctx)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErrMsg)
assert.Nil(t, result)
} else {
require.NoError(t, err)
if tt.validateResult != nil {
tt.validateResult(t, result)
}
}
})
}
}
```
**Key Points**:
- Each service registered via `WithMockHTTPService` gets its own separate mock instance
- Always use `mocks.HTTPServices["serviceName"]` to access and set expectations for a specific service
- Always create the `gofr.Context` with the exact request context (`req.Context()`) that will be used in the handler
- Set expectations on the mock services before calling the handler
- Test both success and error scenarios to ensure your handlers handle all cases correctly
### Summary
- **Mocking Database Interactions**: Use GoFr mock container to simulate database interactions.
- **Mocking HTTP Services**: Use `WithMockHTTPService("serviceName")` to register and mock HTTP services.
- **Context Matching**: Always use `ctx.Context` from your `gofr.Context` in mock expectations, not `t.Context()` or `context.Background()`.
- **Define Test Cases**: Create table-driven tests to handle various scenarios.
- **Run and Validate**: Ensure that your tests check for expected results, and handle errors correctly.
This approach guarantees that your database and HTTP service interactions are tested independently, allowing you to simulate different responses and errors hassle-free.
================================================
FILE: docs/testimonials.json
================================================
[
{
"quote": "Beyond the technical aspects, the GoFr community has been a highlight. The core team's responsiveness to issues and feature requests demonstrates their dedication to continuous improvement. Reviewing merge requests has been consistently positive; the team embraces feedback and actively explores alternative approaches, fostering a collaborative and productive development environment.",
"author": "Christophe Colombier",
"role": "Senior Architect | Open Source Enthusiast",
"profile": "/reviewers/christophe_colombier.webp"
},
{
"quote": "I've used GoFr for both personal and professional projects and found it to be quite effective. While I haven't explored all its features yet, the ones I have used—such as REST API, observability (metrics and logging), and middleware—work seamlessly.",
"author": "Aditya Joshi",
"role": "Senior Software Engineer, Walmart",
"profile": "/reviewers/aditya_joshi.webp"
},
{
"quote": "We want to express our appreciation for the excellent framework you have developed. Integrating NATS into this package has significantly streamlined our application development process and accelerated implementation timelines.",
"author": "Manosh Malai",
"role": "CTO, Mydbops",
"profile": "/reviewers/manosh_malai.webp"
},
{
"quote": "What I like about GoFr is that it plays well with the standard CI/CD infrastructure, deployment environment, and every additional tool in between. Also, it solves for the 80% cases, thus, ensuring we only focus on our core business.",
"author": "Praveen Kumar",
"role": "Founder, apnerve labs",
"profile": "/reviewers/praveen_kumar.webp"
},
{
"quote": "GoFr has helped me monitor my applications so easily for which I would have written more than 100 lines of code, and integrating everything together would have been a nightmare.",
"author": "Shridhar Vijay Kumar",
"role": "Software Engineer, Doceree",
"profile": "/reviewers/shridhar_vijay.webp"
},
{
"quote": "The strong opinions embedded in GoFr.dev make it incredibly efficient and straightforward to work with. It's like having a trusted expert guiding your every move. The framework's Golang foundation ensures exceptional performance and reliability. If you're serious about microservices, this is a must-try",
"author": "Vineet Dwivedi",
"role": "Founder & CEO, LaunchX",
"profile": "/reviewers/vineet_dwivedi.webp"
},
{
"quote": "Productivity has skyrocketed since adopting this framework. The simplicity of GoLang compared to other frameworks makes it incredibly easy to learn and use, allowing us to onboard new developers quickly. Additionally, the framework's design encourages writing clean, maintainable code, making long-term scalability a breeze.",
"author": "Aayush Mishra",
"role": "Data Analyst, American Express",
"profile": "/reviewers/ayush_mishra.webp"
},
{
"quote": "Using the GoFr framework has been a fantastic experience. The environment-based configuration is straightforward and efficient, significantly reducing development time. One of the standout features is how it eliminates boilerplate code, making the codebase much cleaner and more maintainable. Additionally, it enforces a well-organized architecture, which greatly accelerates microservice development. Highly recommend it!",
"author": "Vignesh Palanichamy",
"role": "Lead Consultant at Thoughtworks",
"profile": "/reviewers/vignesh.webp"
}
]
================================================
FILE: examples/grpc/grpc-streaming-client/README.md
================================================
# gRPC Streaming Client Example
This GoFr example demonstrates a simple gRPC streaming server that communicates with another gRPC service hosted on a different machine. It serves as a client for another gRPC example included in this examples folder.
Refer to the documentation to setup
### Steps to Run the Example
1. First, start the corresponding `grpc-streaming-server` example, which is located at the relative path: `../grpc-streaming-server`.
Use the following command to start it:
```console
go run main.go
```
2. Once the `grpc-streaming-server` is running, start this server using a similar command:
```console
go run main.go
```
================================================
FILE: examples/grpc/grpc-streaming-client/client/chat.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v5.29.3
// source: chat.proto
package client
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *Request) Reset() {
*x = Request{}
if protoimpl.UnsafeEnabled {
mi := &file_chat_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_chat_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_chat_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type Response struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *Response) Reset() {
*x = Response{}
if protoimpl.UnsafeEnabled {
mi := &file_chat_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_chat_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_chat_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_chat_proto protoreflect.FileDescriptor
var file_chat_proto_rawDesc = []byte{
0x0a, 0x0a, 0x63, 0x68, 0x61, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x23, 0x0a, 0x07,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x22, 0x24, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x82, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x25,
0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x08,
0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x25, 0x0a, 0x0a, 0x42, 0x69, 0x44, 0x69, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x35, 0x5a, 0x33,
0x67, 0x6f, 0x66, 0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x69, 0x6e, 0x67, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6c, 0x69,
0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_chat_proto_rawDescOnce sync.Once
file_chat_proto_rawDescData = file_chat_proto_rawDesc
)
func file_chat_proto_rawDescGZIP() []byte {
file_chat_proto_rawDescOnce.Do(func() {
file_chat_proto_rawDescData = protoimpl.X.CompressGZIP(file_chat_proto_rawDescData)
})
return file_chat_proto_rawDescData
}
var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_chat_proto_goTypes = []interface{}{
(*Request)(nil), // 0: Request
(*Response)(nil), // 1: Response
}
var file_chat_proto_depIdxs = []int32{
0, // 0: ChatService.ServerStream:input_type -> Request
0, // 1: ChatService.ClientStream:input_type -> Request
0, // 2: ChatService.BiDiStream:input_type -> Request
1, // 3: ChatService.ServerStream:output_type -> Response
1, // 4: ChatService.ClientStream:output_type -> Response
1, // 5: ChatService.BiDiStream:output_type -> Response
3, // [3:6] is the sub-list for method output_type
0, // [0:3] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_chat_proto_init() }
func file_chat_proto_init() {
if File_chat_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_chat_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Request); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_chat_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Response); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_chat_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_chat_proto_goTypes,
DependencyIndexes: file_chat_proto_depIdxs,
MessageInfos: file_chat_proto_msgTypes,
}.Build()
File_chat_proto = out.File
file_chat_proto_rawDesc = nil
file_chat_proto_goTypes = nil
file_chat_proto_depIdxs = nil
}
================================================
FILE: examples/grpc/grpc-streaming-client/client/chat.proto
================================================
syntax = "proto3";
option go_package = "gofr.dev/examples/grpc/grpc-streaming-client/client";
message Request {
string message = 1;
}
message Response {
string message = 1;
}
service ChatService {
rpc ServerStream(Request) returns (stream Response);
rpc ClientStream(stream Request) returns (Response);
rpc BiDiStream(stream Request) returns (stream Response);
}
================================================
FILE: examples/grpc/grpc-streaming-client/client/chat_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v5.29.3
// source: chat.proto
package client
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ChatServiceClient is the client API for ChatService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ChatServiceClient interface {
ServerStream(ctx context.Context, in *Request, opts ...grpc.CallOption) (ChatService_ServerStreamClient, error)
ClientStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_ClientStreamClient, error)
BiDiStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_BiDiStreamClient, error)
}
type chatServiceClient struct {
cc grpc.ClientConnInterface
}
func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient {
return &chatServiceClient{cc}
}
func (c *chatServiceClient) ServerStream(ctx context.Context, in *Request, opts ...grpc.CallOption) (ChatService_ServerStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[0], "/ChatService/ServerStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceServerStreamClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type ChatService_ServerStreamClient interface {
Recv() (*Response, error)
grpc.ClientStream
}
type chatServiceServerStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceServerStreamClient) Recv() (*Response, error) {
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *chatServiceClient) ClientStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_ClientStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[1], "/ChatService/ClientStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceClientStreamClient{stream}
return x, nil
}
type ChatService_ClientStreamClient interface {
Send(*Request) error
CloseAndRecv() (*Response, error)
grpc.ClientStream
}
type chatServiceClientStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceClientStreamClient) Send(m *Request) error {
return x.ClientStream.SendMsg(m)
}
func (x *chatServiceClientStreamClient) CloseAndRecv() (*Response, error) {
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *chatServiceClient) BiDiStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_BiDiStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[2], "/ChatService/BiDiStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceBiDiStreamClient{stream}
return x, nil
}
type ChatService_BiDiStreamClient interface {
Send(*Request) error
Recv() (*Response, error)
grpc.ClientStream
}
type chatServiceBiDiStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceBiDiStreamClient) Send(m *Request) error {
return x.ClientStream.SendMsg(m)
}
func (x *chatServiceBiDiStreamClient) Recv() (*Response, error) {
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ChatServiceServer is the server API for ChatService service.
// All implementations must embed UnimplementedChatServiceServer
// for forward compatibility
type ChatServiceServer interface {
ServerStream(*Request, ChatService_ServerStreamServer) error
ClientStream(ChatService_ClientStreamServer) error
BiDiStream(ChatService_BiDiStreamServer) error
mustEmbedUnimplementedChatServiceServer()
}
// UnimplementedChatServiceServer must be embedded to have forward compatible implementations.
type UnimplementedChatServiceServer struct {
}
func (UnimplementedChatServiceServer) ServerStream(*Request, ChatService_ServerStreamServer) error {
return status.Errorf(codes.Unimplemented, "method ServerStream not implemented")
}
func (UnimplementedChatServiceServer) ClientStream(ChatService_ClientStreamServer) error {
return status.Errorf(codes.Unimplemented, "method ClientStream not implemented")
}
func (UnimplementedChatServiceServer) BiDiStream(ChatService_BiDiStreamServer) error {
return status.Errorf(codes.Unimplemented, "method BiDiStream not implemented")
}
func (UnimplementedChatServiceServer) mustEmbedUnimplementedChatServiceServer() {}
// UnsafeChatServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ChatServiceServer will
// result in compilation errors.
type UnsafeChatServiceServer interface {
mustEmbedUnimplementedChatServiceServer()
}
func RegisterChatServiceServer(s grpc.ServiceRegistrar, srv ChatServiceServer) {
s.RegisterService(&ChatService_ServiceDesc, srv)
}
func _ChatService_ServerStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Request)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ChatServiceServer).ServerStream(m, &chatServiceServerStreamServer{stream})
}
type ChatService_ServerStreamServer interface {
Send(*Response) error
grpc.ServerStream
}
type chatServiceServerStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceServerStreamServer) Send(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func _ChatService_ClientStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(ChatServiceServer).ClientStream(&chatServiceClientStreamServer{stream})
}
type ChatService_ClientStreamServer interface {
SendAndClose(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
type chatServiceClientStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceClientStreamServer) SendAndClose(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func (x *chatServiceClientStreamServer) Recv() (*Request, error) {
m := new(Request)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _ChatService_BiDiStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(ChatServiceServer).BiDiStream(&chatServiceBiDiStreamServer{stream})
}
type ChatService_BiDiStreamServer interface {
Send(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
type chatServiceBiDiStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceBiDiStreamServer) Send(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func (x *chatServiceBiDiStreamServer) Recv() (*Request, error) {
m := new(Request)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ChatService_ServiceDesc is the grpc.ServiceDesc for ChatService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ChatService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ChatService",
HandlerType: (*ChatServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "ServerStream",
Handler: _ChatService_ServerStream_Handler,
ServerStreams: true,
},
{
StreamName: "ClientStream",
Handler: _ChatService_ClientStream_Handler,
ClientStreams: true,
},
{
StreamName: "BiDiStream",
Handler: _ChatService_BiDiStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "chat.proto",
}
================================================
FILE: examples/grpc/grpc-streaming-client/client/chatservice_client.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: chat.proto
package client
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/metrics"
"google.golang.org/grpc"
)
type ChatServiceGoFrClient interface {
ServerStream(ctx *gofr.Context, req *Request, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Response], error)
ClientStream(ctx *gofr.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Request, Response], error)
BiDiStream(ctx *gofr.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Request, Response], error)
HealthClient
}
type ChatServiceClientWrapper struct {
client ChatServiceClient
HealthClient
}
func NewChatServiceGoFrClient(host string, metrics metrics.Manager, dialOptions ...grpc.DialOption) (ChatServiceGoFrClient, error) {
conn, err := createGRPCConn(host, "ChatService", dialOptions...)
if err != nil {
return &ChatServiceClientWrapper{
client: nil,
HealthClient: &HealthClientWrapper{client: nil},
}, err
}
metricsOnce.Do(func() {
metrics.NewHistogram("app_gRPC-Client_stats", "Response time of gRPC client in milliseconds.", gRPCBuckets...)
metrics.NewHistogram("app_gRPC-Client-Stream_stats", "Response time of gRPC streaming client in milliseconds.", gRPCBuckets...)
})
res := NewChatServiceClient(conn)
healthClient := NewHealthClient(conn)
return &ChatServiceClientWrapper{
client: res,
HealthClient: healthClient,
}, nil
}
func (h *ChatServiceClientWrapper) ServerStream(ctx *gofr.Context, req *Request, opts ...grpc.CallOption) (grpc.ServerStreamingClient[Response], error) {
result, err := invokeRPC(ctx, "/ChatService/ServerStream", func() (interface{}, error) {
return h.client.ServerStream(ctx.Context, req, opts...)
}, "app_gRPC-Client-Stream_stats")
if err != nil {
return nil, err
}
return result.(grpc.ServerStreamingClient[Response]), nil
}
func (h *ChatServiceClientWrapper) ClientStream(ctx *gofr.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[Request, Response], error) {
result, err := invokeRPC(ctx, "/ChatService/ClientStream", func() (interface{}, error) {
return h.client.ClientStream(ctx.Context, opts...)
}, "app_gRPC-Client-Stream_stats")
if err != nil {
return nil, err
}
return result.(grpc.ClientStreamingClient[Request, Response]), nil
}
func (h *ChatServiceClientWrapper) BiDiStream(ctx *gofr.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[Request, Response], error) {
result, err := invokeRPC(ctx, "/ChatService/BiDiStream", func() (interface{}, error) {
return h.client.BiDiStream(ctx.Context, opts...)
}, "app_gRPC-Client-Stream_stats")
if err != nil {
return nil, err
}
return result.(grpc.BidiStreamingClient[Request, Response]), nil
}
================================================
FILE: examples/grpc/grpc-streaming-client/client/health_client.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.6.0
// gofr.dev v1.37.0
// source: chat.proto
package client
import (
"fmt"
"sync"
"time"
"gofr.dev/pkg/gofr"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
gofrgRPC "gofr.dev/pkg/gofr/grpc"
)
var (
metricsOnce sync.Once
gRPCBuckets = []float64{0.005, 0.01, .05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
)
type HealthClient interface {
Check(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error)
Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (
grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse], error)
}
type HealthClientWrapper struct {
client grpc_health_v1.HealthClient
}
func NewHealthClient(conn *grpc.ClientConn) HealthClient {
return &HealthClientWrapper{
client: grpc_health_v1.NewHealthClient(conn),
}
}
func createGRPCConn(host string, serviceName string, dialOptions ...grpc.DialOption) (*grpc.ClientConn, error) {
serviceConfig := `{"loadBalancingPolicy": "round_robin"}`
defaultOpts := []grpc.DialOption{
grpc.WithDefaultServiceConfig(serviceConfig),
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
// Developer Note: If the user provides custom DialOptions, they will override the default options due to
// the ordering of dialOptions. This behavior is intentional to ensure the gRPC client connection is properly
// configured even when the user does not specify any DialOptions.
dialOptions = append(defaultOpts, dialOptions...)
conn, err := grpc.NewClient(host, dialOptions...)
if err != nil {
return nil, err
}
return conn, nil
}
func invokeRPC(ctx *gofr.Context, rpcName string, rpcFunc func() (interface{}, error), metricName string) (interface{}, error) {
span := ctx.Trace("gRPC-srv-call: " + rpcName)
defer span.End()
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
md := metadata.Pairs("x-gofr-traceid", traceID, "x-gofr-spanid", spanID)
ctx.Context = metadata.NewOutgoingContext(ctx.Context, md)
transactionStartTime := time.Now()
res, err := rpcFunc()
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), transactionStartTime, err, rpcName, metricName)
return res, err
}
func (h *HealthClientWrapper) Check(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest,
opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error) {
result, err := invokeRPC(ctx, fmt.Sprintf("/grpc.health.v1.Health/Check Service: %q", in.Service), func() (interface{}, error) {
return h.client.Check(ctx, in, opts...)
}, "app_gRPC-Client_stats")
if err != nil {
return nil, err
}
return result.(*grpc_health_v1.HealthCheckResponse), nil
}
func (h *HealthClientWrapper) Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest,
opts ...grpc.CallOption) (grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse], error) {
result, err := invokeRPC(ctx, fmt.Sprintf("/grpc.health.v1.Health/Watch Service: %q", in.Service), func() (interface{}, error) {
return h.client.Watch(ctx, in, opts...)
}, "app_gRPC-Client-Stream_stats")
if err != nil {
return nil, err
}
return result.(grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse]), nil
}
================================================
FILE: examples/grpc/grpc-streaming-client/main.go
================================================
package main
import (
"errors"
"fmt"
"io"
"time"
"gofr.dev/examples/grpc/grpc-streaming-client/client"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Create a gRPC client for the Chat Streaming service
chatClient, err := client.NewChatServiceGoFrClient(app.Config.Get("GRPC_SERVER_HOST"), app.Metrics())
if err != nil {
app.Logger().Errorf("Failed to create Chat client: %v", err)
}
chat := NewChatHandler(chatClient)
app.GET("/chat/server-stream", chat.ServerStreamHandler)
app.POST("/chat/client-stream", chat.ClientStreamHandler)
app.GET("/chat/bidi-stream", chat.BiDiStreamHandler)
app.Run()
}
type ChatHandler struct {
chatClient client.ChatServiceGoFrClient
}
func NewChatHandler(chatClient client.ChatServiceGoFrClient) *ChatHandler {
return &ChatHandler{chatClient: chatClient}
}
type StreamResponse struct {
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
Direction string `json:"direction"` // "received" or "sent"
}
// ServerStreamHandler handles server-side streaming with detailed response tracking
func (c *ChatHandler) ServerStreamHandler(ctx *gofr.Context) (any, error) {
startTime := time.Now()
var responses []StreamResponse
// Record initial request
responses = append(responses, StreamResponse{
Message: "initiating server stream request",
Timestamp: startTime,
Direction: "sent",
})
stream, err := c.chatClient.ServerStream(ctx, &client.Request{Message: "stream request"})
if err != nil {
return nil, fmt.Errorf("failed to initiate server stream: %v", err)
}
// Handle server streaming
for {
res, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return nil, fmt.Errorf("stream receive error: %v", err)
}
// Record received message
response := StreamResponse{
Message: res.Message,
Timestamp: time.Now(),
Direction: "received",
}
responses = append(responses, response)
ctx.Logger.Infof("Received server stream message: %s at %v", res.Message, response.Timestamp)
}
// Return detailed stream information
return map[string]any{
"status": "server stream completed",
"start_time": startTime,
"end_time": time.Now(),
"duration_sec": time.Since(startTime).Seconds(),
"stream_messages": responses,
}, nil
}
// ClientStreamHandler handles client-side streaming with detailed tracking
func (c *ChatHandler) ClientStreamHandler(ctx *gofr.Context) (any, error) {
startTime := time.Now()
var streamLog []StreamResponse
// Get client streaming interface
stream, err := c.chatClient.ClientStream(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initiate client stream: %v", err)
}
// Example: Read multiple messages from request body
var requests []*client.Request
if err := ctx.Bind(&requests); err != nil {
return nil, fmt.Errorf("failed to bind requests: %v", err)
}
// Send multiple messages to server and log each one
for i, req := range requests {
sendTime := time.Now()
if err := stream.Send(req); err != nil {
return nil, fmt.Errorf("failed to send request %d: %v", i+1, err)
}
streamLog = append(streamLog, StreamResponse{
Message: req.Message,
Timestamp: sendTime,
Direction: "sent",
})
ctx.Logger.Infof("Sent client stream message %d: %s at %v", i+1, req.Message, sendTime)
}
// Close the stream and get final response
response, err := stream.CloseAndRecv()
if err != nil {
return nil, fmt.Errorf("failed to receive final response: %v", err)
}
// Record final response
streamLog = append(streamLog, StreamResponse{
Message: response.Message,
Timestamp: time.Now(),
Direction: "received",
})
return map[string]any{
"final_response": response.Message,
"start_time": startTime,
"end_time": time.Now(),
"duration_sec": time.Since(startTime).Seconds(),
"stream_log": streamLog,
}, nil
}
// BiDiStreamHandler handles bidirectional streaming with detailed tracking
func (c *ChatHandler) BiDiStreamHandler(ctx *gofr.Context) (any, error) {
startTime := time.Now()
streamLog := make([]StreamResponse, 0)
stream, err := c.chatClient.BiDiStream(ctx)
if err != nil {
return nil, fmt.Errorf("failed to initiate bidirectional stream: %v", err)
}
respChan, errChan := make(chan StreamResponse), make(chan error)
go c.receiveBiDiResponses(ctx, stream, respChan, errChan)
sentMessages, err := c.sendBiDiMessages(ctx, stream, &streamLog)
if err != nil {
return nil, err
}
if err := stream.CloseSend(); err != nil {
return nil, fmt.Errorf("failed to close send: %v", err)
}
receivedMessages, err := c.collectBiDiResponses(respChan, errChan, &streamLog)
if err != nil {
return nil, err
}
return map[string]any{
"status": "bidirectional stream completed",
"start_time": startTime,
"end_time": time.Now(),
"duration_sec": time.Since(startTime).Seconds(),
"sent_messages": sentMessages,
"received_messages": receivedMessages,
"detailed_log": streamLog,
}, nil
}
// receiveBiDiResponses receives messages in a goroutine
func (c *ChatHandler) receiveBiDiResponses(ctx *gofr.Context, stream client.ChatService_BiDiStreamClient, respChan chan<- StreamResponse, errChan chan<- error) {
for {
res, err := stream.Recv()
if err != nil {
if errors.Is(err, io.EOF) {
errChan <- nil
} else {
errChan <- fmt.Errorf("receive error: %v", err)
}
return
}
timestamp := time.Now()
ctx.Logger.Infof("Received bidirectional message: %s at %v", res.Message, timestamp)
respChan <- StreamResponse{
Message: res.Message,
Timestamp: timestamp,
Direction: "received",
}
}
}
// sendBiDiMessages sends predefined messages
func (c *ChatHandler) sendBiDiMessages(ctx *gofr.Context, stream client.ChatService_BiDiStreamClient, streamLog *[]StreamResponse) ([]string, error) {
messages := []string{"message 1", "message 2", "message 3"}
for _, msg := range messages {
timestamp := time.Now()
if err := stream.Send(&client.Request{Message: msg}); err != nil {
return nil, fmt.Errorf("failed to send message %q: %v", msg, err)
}
ctx.Logger.Infof("Sent bidirectional message: %s at %v", msg, timestamp)
*streamLog = append(*streamLog, StreamResponse{
Message: msg,
Timestamp: timestamp,
Direction: "sent",
})
}
return messages, nil
}
// collectBiDiResponses waits and aggregates received responses
func (c *ChatHandler) collectBiDiResponses(respChan <-chan StreamResponse, errChan <-chan error, streamLog *[]StreamResponse) ([]string, error) {
var received []string
timeout := time.After(5 * time.Second)
for {
select {
case err := <-errChan:
return received, err
case resp := <-respChan:
received = append(received, resp.Message)
*streamLog = append(*streamLog, resp)
case <-timeout:
return nil, errors.New("bidirectional stream timeout")
}
}
}
================================================
FILE: examples/grpc/grpc-streaming-server/README.md
================================================
# GRPC Server Example
This GoFr example showcases a basic gRPC streaming server implementation. For detailed instructions on setting up gRPC handlers with GoFr’s context support—enabling built-in tracing and database integration within your gRPC handlers—please refer to our [official documentation](https://gofr.dev/docs/advanced-guide/grpc).
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/grpc/grpc-streaming-server/main.go
================================================
package main
import (
"gofr.dev/examples/grpc/grpc-streaming-server/server"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
server.RegisterChatServiceServerWithGofr(app, server.NewChatServiceGoFrServer())
app.Run()
}
================================================
FILE: examples/grpc/grpc-streaming-server/main_test.go
================================================
package main
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gofr.dev/examples/grpc/grpc-streaming-server/server"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
go main()
time.Sleep(300 * time.Millisecond) // wait for server to boot
m.Run()
}
func TestServerStream(t *testing.T) {
ctx := context.Background()
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.ServerStream(ctx, &server.Request{Message: "Hello"})
if err != nil {
t.Fatalf("ServerStream failed: %v", err)
}
count := 0
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
t.Fatalf("Error receiving: %v", err)
}
expected := fmt.Sprintf("Server stream %d: Hello", count)
if resp.GetMessage() != expected {
t.Errorf("Unexpected message: got %q, want %q", resp.GetMessage(), expected)
}
count++
}
if count != 5 {
t.Errorf("Expected 5 messages, got %d", count)
}
}
func TestClientStream(t *testing.T) {
ctx := context.Background()
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.ClientStream(ctx)
if err != nil {
t.Fatalf("ClientStream failed: %v", err)
}
messages := []string{"Hello", "from", "client"}
for _, msg := range messages {
if err := stream.Send(&server.Request{Message: msg}); err != nil {
t.Fatalf("Send failed: %v", err)
}
}
resp, err := stream.CloseAndRecv()
if err != nil {
t.Fatalf("CloseAndRecv failed: %v", err)
}
expected := "Received 3 messages. Final: Hello from client "
if resp.GetMessage() != expected {
t.Errorf("Unexpected response: got %q, want %q", resp.GetMessage(), expected)
}
}
func TestBiDiStream(t *testing.T) {
ctx := context.Background()
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.BiDiStream(ctx)
if err != nil {
t.Fatalf("BiDiStream failed: %v", err)
}
messages := []string{"msg1", "msg2", "msg3"}
go func() {
for _, msg := range messages {
_ = stream.Send(&server.Request{Message: msg})
time.Sleep(100 * time.Millisecond)
}
_ = stream.CloseSend()
}()
var responses []string
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("Recv failed: %v", err)
}
responses = append(responses, resp.GetMessage())
}
for i, msg := range messages {
expected := "Echo: " + msg
if strings.TrimSpace(responses[i]) != expected {
t.Errorf("Unexpected response: got %q, want %q", responses[i], expected)
}
}
}
// TestServerStream_ContextCancellation tests that server-side streaming
// properly handles context cancellation
func TestServerStream_ContextCancellation(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
// Create a context that will be canceled after a short delay
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := client.ServerStream(ctx, &server.Request{Message: "Hello"})
if err != nil {
t.Fatalf("ServerStream failed: %v", err)
}
// Cancel context after receiving first message
receivedFirst := false
var lastErr error
for {
resp, err := stream.Recv()
if err != nil {
lastErr = err
break
}
if !receivedFirst {
receivedFirst = true
// Cancel context to trigger cancellation handling
cancel()
// Give server time to detect cancellation
time.Sleep(200 * time.Millisecond)
}
_ = resp // Use response to avoid unused variable
}
// Verify that we got a cancellation error
if lastErr == nil {
t.Error("Expected error due to context cancellation, got nil")
} else {
s, ok := status.FromError(lastErr)
if !ok {
t.Errorf("Expected gRPC status error, got: %v", lastErr)
} else if s.Code() != codes.Canceled {
t.Errorf("Expected Canceled status code, got: %v", s.Code())
}
}
}
// TestClientStream_ContextCancellation tests that client-side streaming
// properly handles context cancellation
func TestClientStream_ContextCancellation(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
// Create a context that will be canceled
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := client.ClientStream(ctx)
if err != nil {
t.Fatalf("ClientStream failed: %v", err)
}
// Send one message then cancel
if err := stream.Send(&server.Request{Message: "Hello"}); err != nil {
t.Fatalf("Send failed: %v", err)
}
// Cancel context
cancel()
// Give server time to detect cancellation
time.Sleep(200 * time.Millisecond)
// Try to close and receive - should get cancellation error
_, err = stream.CloseAndRecv()
if err == nil {
t.Error("Expected error due to context cancellation, got nil")
} else {
s, ok := status.FromError(err)
if !ok {
t.Errorf("Expected gRPC status error, got: %v", err)
} else if s.Code() != codes.Canceled {
t.Errorf("Expected Canceled status code, got: %v", s.Code())
}
}
}
// TestClientStream_EOFHandling tests that client-side streaming
// properly handles EOF when client closes the stream
func TestClientStream_EOFHandling(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.ClientStream(context.Background())
if err != nil {
t.Fatalf("ClientStream failed: %v", err)
}
// Send messages
messages := []string{"msg1", "msg2", "msg3"}
for _, msg := range messages {
if err := stream.Send(&server.Request{Message: msg}); err != nil {
t.Fatalf("Send failed: %v", err)
}
}
// Close and receive - should succeed with EOF handled properly
resp, err := stream.CloseAndRecv()
if err != nil {
t.Fatalf("CloseAndRecv failed: %v", err)
}
// Verify response indicates all messages were received
expected := fmt.Sprintf("Received %d messages. Final: %s ", len(messages), strings.Join(messages, " "))
if !strings.Contains(resp.GetMessage(), expected) {
t.Errorf("Unexpected response: got %q, expected to contain %q", resp.GetMessage(), expected)
}
}
// TestBiDiStream_ContextCancellation tests that bidirectional streaming
// properly handles context cancellation
func TestBiDiStream_ContextCancellation(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
// Create a context that will be canceled
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
stream, err := client.BiDiStream(ctx)
if err != nil {
t.Fatalf("BiDiStream failed: %v", err)
}
// Send one message
if err := stream.Send(&server.Request{Message: "test"}); err != nil {
t.Fatalf("Send failed: %v", err)
}
// Receive one response
resp, err := stream.Recv()
if err != nil {
t.Fatalf("Recv failed: %v", err)
}
if resp.GetMessage() != "Echo: test" {
t.Errorf("Unexpected response: got %q, want %q", resp.GetMessage(), "Echo: test")
}
// Cancel context
cancel()
// Give server time to detect cancellation
time.Sleep(200 * time.Millisecond)
// Try to receive - should get cancellation error
_, err = stream.Recv()
if err == nil {
t.Error("Expected error due to context cancellation, got nil")
} else {
s, ok := status.FromError(err)
if !ok {
// EOF is also acceptable when stream closes normally
if !errors.Is(err, io.EOF) {
t.Errorf("Expected gRPC status error or EOF, got: %v", err)
}
} else if s.Code() != codes.Canceled {
// If it's a status error, it should be Canceled
t.Errorf("Expected Canceled status code, got: %v", s.Code())
}
}
}
// TestServerStream_ErrorHandling tests error handling in server-side streaming
func TestServerStream_ErrorHandling(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.ServerStream(context.Background(), &server.Request{Message: "test"})
if err != nil {
t.Fatalf("ServerStream failed: %v", err)
}
// Receive all messages and verify EOF handling
count := 0
for {
resp, err := stream.Recv()
if err == io.EOF {
// EOF indicates stream ended normally
break
}
if err != nil {
t.Fatalf("Unexpected error receiving: %v", err)
}
count++
_ = resp // Use response
}
// Verify we received all expected messages
if count != 5 {
t.Errorf("Expected 5 messages, got %d", count)
}
}
// TestBiDiStream_ErrorHandling tests error handling in bidirectional streaming
func TestBiDiStream_ErrorHandling(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Dial failed: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
stream, err := client.BiDiStream(context.Background())
if err != nil {
t.Fatalf("BiDiStream failed: %v", err)
}
// Send messages
messages := []string{"msg1", "msg2"}
for _, msg := range messages {
if err := stream.Send(&server.Request{Message: msg}); err != nil {
t.Fatalf("Send failed: %v", err)
}
}
// Close send side
if err := stream.CloseSend(); err != nil {
t.Fatalf("CloseSend failed: %v", err)
}
// Receive responses and verify EOF handling
var responses []string
for {
resp, err := stream.Recv()
if errors.Is(err, io.EOF) {
// EOF indicates stream ended normally
break
}
if err != nil {
t.Fatalf("Unexpected error receiving: %v", err)
}
responses = append(responses, resp.GetMessage())
}
// Verify we received responses for all sent messages
if len(responses) != len(messages) {
t.Errorf("Expected %d responses, got %d", len(messages), len(responses))
}
}
// TestServerStream_Timeout tests server-side streaming with timeout
func TestServerStream_Timeout(t *testing.T) {
conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial: %v", err)
}
defer conn.Close()
client := server.NewChatServiceClient(conn)
// Create a context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
stream, err := client.ServerStream(ctx, &server.Request{Message: "timeout test"})
if err != nil {
t.Fatalf("ServerStream failed: %v", err)
}
// Try to receive messages - timeout should occur
var lastErr error
count := 0
for {
resp, err := stream.Recv()
if err != nil {
lastErr = err
break
}
count++
_ = resp
}
// Should have received some messages before timeout
if count == 0 {
t.Error("Expected to receive at least one message before timeout")
}
// Verify timeout error
if lastErr == nil {
t.Error("Expected timeout error, got nil")
} else {
s, ok := status.FromError(lastErr)
if ok && s.Code() == codes.DeadlineExceeded {
// Deadline exceeded is expected for timeout
} else if !errors.Is(lastErr, context.DeadlineExceeded) {
// Context deadline exceeded is also acceptable
if !errors.Is(lastErr, io.EOF) {
t.Logf("Got error (may be expected): %v", lastErr)
}
}
}
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/chat.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v5.29.3
// source: chat.proto
package server
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Request struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *Request) Reset() {
*x = Request{}
if protoimpl.UnsafeEnabled {
mi := &file_chat_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_chat_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_chat_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
type Response struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *Response) Reset() {
*x = Response{}
if protoimpl.UnsafeEnabled {
mi := &file_chat_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_chat_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_chat_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_chat_proto protoreflect.FileDescriptor
var file_chat_proto_rawDesc = []byte{
0x0a, 0x0a, 0x63, 0x68, 0x61, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x23, 0x0a, 0x07,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61,
0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
0x65, 0x22, 0x24, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a,
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x82, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x74,
0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65,
0x72, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x25,
0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x08,
0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x25, 0x0a, 0x0a, 0x42, 0x69, 0x44, 0x69, 0x53, 0x74, 0x72,
0x65, 0x61, 0x6d, 0x12, 0x08, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x09, 0x2e,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x34, 0x5a, 0x32,
0x67, 0x6f, 0x66, 0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2d, 0x73, 0x74, 0x72, 0x65,
0x61, 0x6d, 0x69, 0x6e, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76,
0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_chat_proto_rawDescOnce sync.Once
file_chat_proto_rawDescData = file_chat_proto_rawDesc
)
func file_chat_proto_rawDescGZIP() []byte {
file_chat_proto_rawDescOnce.Do(func() {
file_chat_proto_rawDescData = protoimpl.X.CompressGZIP(file_chat_proto_rawDescData)
})
return file_chat_proto_rawDescData
}
var file_chat_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_chat_proto_goTypes = []interface{}{
(*Request)(nil), // 0: Request
(*Response)(nil), // 1: Response
}
var file_chat_proto_depIdxs = []int32{
0, // 0: ChatService.ServerStream:input_type -> Request
0, // 1: ChatService.ClientStream:input_type -> Request
0, // 2: ChatService.BiDiStream:input_type -> Request
1, // 3: ChatService.ServerStream:output_type -> Response
1, // 4: ChatService.ClientStream:output_type -> Response
1, // 5: ChatService.BiDiStream:output_type -> Response
3, // [3:6] is the sub-list for method output_type
0, // [0:3] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_chat_proto_init() }
func file_chat_proto_init() {
if File_chat_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_chat_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Request); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_chat_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Response); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_chat_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_chat_proto_goTypes,
DependencyIndexes: file_chat_proto_depIdxs,
MessageInfos: file_chat_proto_msgTypes,
}.Build()
File_chat_proto = out.File
file_chat_proto_rawDesc = nil
file_chat_proto_goTypes = nil
file_chat_proto_depIdxs = nil
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/chat.proto
================================================
syntax = "proto3";
option go_package = "gofr.dev/examples/grpc/grpc-streaming-server/server";
message Request {
string message = 1;
}
message Response {
string message = 1;
}
service ChatService {
rpc ServerStream(Request) returns (stream Response);
rpc ClientStream(stream Request) returns (Response);
rpc BiDiStream(stream Request) returns (stream Response);
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/chat_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v5.29.3
// source: chat.proto
package server
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// ChatServiceClient is the client API for ChatService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ChatServiceClient interface {
ServerStream(ctx context.Context, in *Request, opts ...grpc.CallOption) (ChatService_ServerStreamClient, error)
ClientStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_ClientStreamClient, error)
BiDiStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_BiDiStreamClient, error)
}
type chatServiceClient struct {
cc grpc.ClientConnInterface
}
func NewChatServiceClient(cc grpc.ClientConnInterface) ChatServiceClient {
return &chatServiceClient{cc}
}
func (c *chatServiceClient) ServerStream(ctx context.Context, in *Request, opts ...grpc.CallOption) (ChatService_ServerStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[0], "/ChatService/ServerStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceServerStreamClient{stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
type ChatService_ServerStreamClient interface {
Recv() (*Response, error)
grpc.ClientStream
}
type chatServiceServerStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceServerStreamClient) Recv() (*Response, error) {
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *chatServiceClient) ClientStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_ClientStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[1], "/ChatService/ClientStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceClientStreamClient{stream}
return x, nil
}
type ChatService_ClientStreamClient interface {
Send(*Request) error
CloseAndRecv() (*Response, error)
grpc.ClientStream
}
type chatServiceClientStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceClientStreamClient) Send(m *Request) error {
return x.ClientStream.SendMsg(m)
}
func (x *chatServiceClientStreamClient) CloseAndRecv() (*Response, error) {
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *chatServiceClient) BiDiStream(ctx context.Context, opts ...grpc.CallOption) (ChatService_BiDiStreamClient, error) {
stream, err := c.cc.NewStream(ctx, &ChatService_ServiceDesc.Streams[2], "/ChatService/BiDiStream", opts...)
if err != nil {
return nil, err
}
x := &chatServiceBiDiStreamClient{stream}
return x, nil
}
type ChatService_BiDiStreamClient interface {
Send(*Request) error
Recv() (*Response, error)
grpc.ClientStream
}
type chatServiceBiDiStreamClient struct {
grpc.ClientStream
}
func (x *chatServiceBiDiStreamClient) Send(m *Request) error {
return x.ClientStream.SendMsg(m)
}
func (x *chatServiceBiDiStreamClient) Recv() (*Response, error) {
m := new(Response)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ChatServiceServer is the server API for ChatService service.
// All implementations must embed UnimplementedChatServiceServer
// for forward compatibility
type ChatServiceServer interface {
ServerStream(*Request, ChatService_ServerStreamServer) error
ClientStream(ChatService_ClientStreamServer) error
BiDiStream(ChatService_BiDiStreamServer) error
mustEmbedUnimplementedChatServiceServer()
}
// UnimplementedChatServiceServer must be embedded to have forward compatible implementations.
type UnimplementedChatServiceServer struct {
}
func (UnimplementedChatServiceServer) ServerStream(*Request, ChatService_ServerStreamServer) error {
return status.Errorf(codes.Unimplemented, "method ServerStream not implemented")
}
func (UnimplementedChatServiceServer) ClientStream(ChatService_ClientStreamServer) error {
return status.Errorf(codes.Unimplemented, "method ClientStream not implemented")
}
func (UnimplementedChatServiceServer) BiDiStream(ChatService_BiDiStreamServer) error {
return status.Errorf(codes.Unimplemented, "method BiDiStream not implemented")
}
func (UnimplementedChatServiceServer) mustEmbedUnimplementedChatServiceServer() {}
// UnsafeChatServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ChatServiceServer will
// result in compilation errors.
type UnsafeChatServiceServer interface {
mustEmbedUnimplementedChatServiceServer()
}
func RegisterChatServiceServer(s grpc.ServiceRegistrar, srv ChatServiceServer) {
s.RegisterService(&ChatService_ServiceDesc, srv)
}
func _ChatService_ServerStream_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Request)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ChatServiceServer).ServerStream(m, &chatServiceServerStreamServer{stream})
}
type ChatService_ServerStreamServer interface {
Send(*Response) error
grpc.ServerStream
}
type chatServiceServerStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceServerStreamServer) Send(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func _ChatService_ClientStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(ChatServiceServer).ClientStream(&chatServiceClientStreamServer{stream})
}
type ChatService_ClientStreamServer interface {
SendAndClose(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
type chatServiceClientStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceClientStreamServer) SendAndClose(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func (x *chatServiceClientStreamServer) Recv() (*Request, error) {
m := new(Request)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _ChatService_BiDiStream_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(ChatServiceServer).BiDiStream(&chatServiceBiDiStreamServer{stream})
}
type ChatService_BiDiStreamServer interface {
Send(*Response) error
Recv() (*Request, error)
grpc.ServerStream
}
type chatServiceBiDiStreamServer struct {
grpc.ServerStream
}
func (x *chatServiceBiDiStreamServer) Send(m *Response) error {
return x.ServerStream.SendMsg(m)
}
func (x *chatServiceBiDiStreamServer) Recv() (*Request, error) {
m := new(Request)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// ChatService_ServiceDesc is the grpc.ServiceDesc for ChatService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var ChatService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "ChatService",
HandlerType: (*ChatServiceServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "ServerStream",
Handler: _ChatService_ServerStream_Handler,
ServerStreams: true,
},
{
StreamName: "ClientStream",
Handler: _ChatService_ClientStream_Handler,
ClientStreams: true,
},
{
StreamName: "BiDiStream",
Handler: _ChatService_BiDiStream_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "chat.proto",
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/chatservice_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: chat.proto
package server
import (
"context"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
gofrgRPC "gofr.dev/pkg/gofr/grpc"
"google.golang.org/grpc"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
// NewChatServiceGoFrServer creates a new instance of ChatServiceGoFrServer
func NewChatServiceGoFrServer() *ChatServiceGoFrServer {
return &ChatServiceGoFrServer{
health: getOrCreateHealthServer(), // Initialize the health server
}
}
// ChatServiceServerWithGofr is the interface for the server implementation
type ChatServiceServerWithGofr interface {
ServerStream(*gofr.Context, ChatService_ServerStreamServer) error
ClientStream(*gofr.Context, ChatService_ClientStreamServer) error
BiDiStream(*gofr.Context, ChatService_BiDiStreamServer) error
}
// ChatServiceServerWrapper wraps the server and handles request and response logic
type ChatServiceServerWrapper struct {
ChatServiceServer
*healthServer
Container *container.Container
server ChatServiceServerWithGofr
}
// Base instrumented stream
type instrumentedStream struct {
grpc.ServerStream
ctx *gofr.Context
method string
}
func (s *instrumentedStream) Context() context.Context {
return s.ctx
}
func (s *instrumentedStream) SendMsg(m interface{}) error {
start := time.Now()
span := s.ctx.Trace(s.method + "/SendMsg")
defer span.End()
err := s.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(s.ctx, s.ctx.Logger, s.ctx.Metrics(), start, err,
s.method+"/SendMsg", "app_gRPC-Stream_stats")
return err
}
func (s *instrumentedStream) RecvMsg(m interface{}) error {
start := time.Now()
span := s.ctx.Trace(s.method + "/RecvMsg")
defer span.End()
err := s.ServerStream.RecvMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(s.ctx, s.ctx.Logger, s.ctx.Metrics(), start, err,
s.method+"/RecvMsg", "app_gRPC-Stream_stats")
return err
}
// Server-side streaming specific wrapper
type serverStreamWrapperServerStream struct {
*instrumentedStream
}
func (w *serverStreamWrapperServerStream) Send(m *Response) error {
start := time.Now()
span := w.ctx.Trace(w.method + "/Send")
defer span.End()
err := w.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/Send", "app_gRPC-Stream_stats")
return err
}
// Client-side streaming specific wrapper
type clientStreamWrapperClientStream struct {
*instrumentedStream
}
func (w *clientStreamWrapperClientStream) SendAndClose(m *Response) error {
start := time.Now()
span := w.ctx.Trace(w.method + "/SendAndClose")
defer span.End()
err := w.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/SendAndClose", "app_gRPC-Stream_stats")
return err
}
func (w *clientStreamWrapperClientStream) Recv() (*Request, error) {
start := time.Now()
span := w.ctx.Trace(w.method + "/Recv")
defer span.End()
var req Request
err := w.ServerStream.RecvMsg(&req)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/Recv", "app_gRPC-Stream_stats")
return &req, err
}
// Client-side streaming specific wrapper
type clientStreamWrapperBiDiStream struct {
*instrumentedStream
}
func (w *clientStreamWrapperBiDiStream) SendAndClose(m *Response) error {
start := time.Now()
span := w.ctx.Trace(w.method + "/SendAndClose")
defer span.End()
err := w.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/SendAndClose", "app_gRPC-Stream_stats")
return err
}
func (w *clientStreamWrapperBiDiStream) Recv() (*Request, error) {
start := time.Now()
span := w.ctx.Trace(w.method + "/Recv")
defer span.End()
var req Request
err := w.ServerStream.RecvMsg(&req)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/Recv", "app_gRPC-Stream_stats")
return &req, err
}
// Bidirectional streaming wrapper
type bidiStreamWrapperBiDiStream struct {
*instrumentedStream
}
func (w *bidiStreamWrapperBiDiStream) Send(m *Response) error {
start := time.Now()
span := w.ctx.Trace(w.method + "/Send")
defer span.End()
err := w.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/Send", "app_gRPC-Stream_stats")
return err
}
func (w *bidiStreamWrapperBiDiStream) Recv() (*Request, error) {
start := time.Now()
span := w.ctx.Trace(w.method + "/Recv")
defer span.End()
var req Request
err := w.ServerStream.RecvMsg(&req)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/Recv", "app_gRPC-Stream_stats")
return &req, err
}
func (w *bidiStreamWrapperBiDiStream) CloseSend() error {
start := time.Now()
span := w.ctx.Trace(w.method + "/CloseSend")
defer span.End()
err := w.ServerStream.(grpc.ClientStream).CloseSend()
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(w.ctx, w.ctx.Logger, w.ctx.Metrics(), start, err,
w.method+"/CloseSend", "app_gRPC-Stream_stats")
return err
}
// Server-side streaming handler for ServerStream
func (h *ChatServiceServerWrapper) ServerStream(req *Request, stream ChatService_ServerStreamServer) error {
ctx := stream.Context()
gctx := h.getGofrContext(ctx, &RequestWrapper{ctx: ctx, Request: req})
is := &instrumentedStream{
ServerStream: stream,
ctx: gctx,
method: "/ChatService/ServerStream",
}
wrappedStream := &serverStreamWrapperServerStream{instrumentedStream: is}
return h.server.ServerStream(gctx, wrappedStream)
}
// Client-side streaming handler for ClientStream
func (h *ChatServiceServerWrapper) ClientStream(stream ChatService_ClientStreamServer) error {
ctx := stream.Context()
gctx := h.getGofrContext(ctx, nil)
is := &instrumentedStream{
ServerStream: stream,
ctx: gctx,
method: "/ChatService/ClientStream",
}
wrappedStream := &clientStreamWrapperClientStream{instrumentedStream: is}
return h.server.ClientStream(gctx, wrappedStream)
}
// Bidirectional streaming handler for BiDiStream
func (h *ChatServiceServerWrapper) BiDiStream(stream ChatService_BiDiStreamServer) error {
ctx := stream.Context()
gctx := h.getGofrContext(ctx, nil)
is := &instrumentedStream{
ServerStream: stream,
ctx: gctx,
method: "/ChatService/BiDiStream",
}
wrappedStream := &bidiStreamWrapperBiDiStream{instrumentedStream: is}
return h.server.BiDiStream(gctx, wrappedStream)
}
// mustEmbedUnimplementedChatServiceServer ensures implementation
func (h *ChatServiceServerWrapper) mustEmbedUnimplementedChatServiceServer() {}
// RegisterChatServiceServerWithGofr registers the server
func RegisterChatServiceServerWithGofr(app *gofr.App, srv ChatServiceServerWithGofr) {
registerServerWithGofr(app, srv, func(s grpc.ServiceRegistrar, srv any) {
wrapper := &ChatServiceServerWrapper{
server: srv.(ChatServiceServerWithGofr),
healthServer: getOrCreateHealthServer(),
}
RegisterChatServiceServer(s, wrapper)
wrapper.Server.SetServingStatus("Hello", healthpb.HealthCheckResponse_SERVING)
})
}
// getGofrContext creates GoFr context
func (h *ChatServiceServerWrapper) getGofrContext(ctx context.Context, req gofr.Request) *gofr.Context {
return &gofr.Context{
Context: ctx,
Container: h.Container,
Request: req,
}
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/chatservice_server.go
================================================
// versions:
// gofr-cli v0.6.0
// gofr.dev v1.37.0
// source: chat.proto
package server
import (
"fmt"
"io"
"strings"
"time"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"gofr.dev/pkg/gofr"
)
// Register the gRPC service in your app using the following code in your main.go:
//
// server.RegisterChatServiceServerWithGofr(app, &server.NewChatServiceGoFrServer())
//
// ChatServiceGoFrServer defines the gRPC server implementation.
// Customize the struct with required dependencies and fields as needed.
type ChatServiceGoFrServer struct {
health *healthServer
}
func (s *ChatServiceGoFrServer) ServerStream(ctx *gofr.Context, stream ChatService_ServerStreamServer) error {
req := Request{}
err := ctx.Bind(&req)
if err != nil {
return status.Errorf(codes.InvalidArgument, "invalid request: %v", err)
}
for i := 0; i < 5; i++ {
// Check if context is canceled
select {
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
default:
}
resp := &Response{Message: fmt.Sprintf("Server stream %d: %s", i, req.Message)}
if err := stream.Send(resp); err != nil {
return status.Errorf(codes.Internal, "error sending stream: %v", err)
}
time.Sleep(1 * time.Second) // Simulate processing delay
}
return nil
}
func (s *ChatServiceGoFrServer) ClientStream(ctx *gofr.Context, stream ChatService_ClientStreamServer) error {
var messageCount int
var finalMessage strings.Builder
for {
// Check if context is canceled before receiving
select {
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
default:
}
req, err := stream.Recv()
if err == io.EOF {
// Client has finished sending, send final response
return stream.SendAndClose(&Response{
Message: fmt.Sprintf("Received %d messages. Final: %s", messageCount, finalMessage.String()),
})
}
if err != nil {
return status.Errorf(codes.Internal, "error receiving stream: %v", err)
}
messageCount++
finalMessage.WriteString(req.Message + " ")
}
}
func (s *ChatServiceGoFrServer) BiDiStream(ctx *gofr.Context, stream ChatService_BiDiStreamServer) error {
// Handle incoming messages in a goroutine
errChan := make(chan error)
go func() {
for {
// Check if context is canceled
select {
case <-stream.Context().Done():
errChan <- status.Error(codes.Canceled, "client disconnected")
return
default:
}
req, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
errChan <- status.Errorf(codes.Internal, "error receiving stream: %v", err)
return
}
// Process request and send response
resp := &Response{Message: "Echo: " + req.Message}
if err := stream.Send(resp); err != nil {
errChan <- status.Errorf(codes.Internal, "error sending stream: %v", err)
return
}
}
errChan <- nil
}()
// Wait for completion
select {
case err := <-errChan:
return err
case <-stream.Context().Done():
return status.Error(codes.Canceled, "client disconnected")
}
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/health_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: chat.proto
package server
import (
"fmt"
"google.golang.org/grpc"
"time"
"gofr.dev/pkg/gofr"
gofrGRPC "gofr.dev/pkg/gofr/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type healthServer struct {
*health.Server
}
var globalHealthServer *healthServer
var healthServerRegistered bool // Global flag to track if health server is registered
// getOrCreateHealthServer ensures only one health server is created and reused.
func getOrCreateHealthServer() *healthServer {
if globalHealthServer == nil {
globalHealthServer = &healthServer{health.NewServer()}
}
return globalHealthServer
}
func registerServerWithGofr(app *gofr.App, srv any, registerFunc func(grpc.ServiceRegistrar, any)) {
var s grpc.ServiceRegistrar = app
h := getOrCreateHealthServer()
// Register metrics and health server only once
if !healthServerRegistered {
gRPCBuckets := []float64{0.005, 0.01, .05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
app.Metrics().NewHistogram("app_gRPC-Server_stats", "Response time of gRPC server in milliseconds.", gRPCBuckets...)
app.Metrics().NewHistogram("app_gRPC-Stream_stats", "Duration of gRPC stream in milliseconds.", gRPCBuckets...)
healthpb.RegisterHealthServer(s, h.Server)
h.Server.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthServerRegistered = true
}
// Register the provided server
registerFunc(s, srv)
}
func (h *healthServer) Check(ctx *gofr.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Check")
res, err := h.Server.Check(ctx.Context, req)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, err,
fmt.Sprintf("/grpc.health.v1.Health/Check Service: %q", req.Service), "app_gRPC-Server_stats")
span.End()
return res, err
}
func (h *healthServer) Watch(ctx *gofr.Context, in *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Watch")
err := h.Server.Watch(in, stream)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, err,
fmt.Sprintf("/grpc.health.v1.Health/Watch Service: %q", in.Service), "app_gRPC-Server_stats")
span.End()
return err
}
func (h *healthServer) SetServingStatus(ctx *gofr.Context, service string, servingStatus healthpb.HealthCheckResponse_ServingStatus) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/SetServingStatus")
h.Server.SetServingStatus(service, servingStatus)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
fmt.Sprintf("/grpc.health.v1.Health/SetServingStatus Service: %q", service), "app_gRPC-Server_stats")
span.End()
}
func (h *healthServer) Shutdown(ctx *gofr.Context) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Shutdown")
h.Server.Shutdown()
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
"/grpc.health.v1.Health/Shutdown", "app_gRPC-Server_stats")
span.End()
}
func (h *healthServer) Resume(ctx *gofr.Context) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Resume")
h.Server.Resume()
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
"/grpc.health.v1.Health/Resume", "app_gRPC-Server_stats")
span.End()
}
================================================
FILE: examples/grpc/grpc-streaming-server/server/request_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: chat.proto
package server
import (
"context"
"fmt"
"reflect"
)
// Request Wrappers
type RequestWrapper struct {
ctx context.Context
*Request
}
func (h *RequestWrapper) Context() context.Context {
return h.ctx
}
func (h *RequestWrapper) Param(s string) string {
return ""
}
func (h *RequestWrapper) PathParam(s string) string {
return ""
}
func (h *RequestWrapper) Bind(p interface{}) error {
ptr := reflect.ValueOf(p)
if ptr.Kind() != reflect.Ptr {
return fmt.Errorf("expected a pointer, got %T", p)
}
hValue := reflect.ValueOf(h.Request).Elem()
ptrValue := ptr.Elem()
for i := 0; i < hValue.NumField(); i++ {
field := hValue.Type().Field(i)
if field.Name == "state" || field.Name == "sizeCache" || field.Name == "unknownFields" {
continue
}
if field.IsExported() {
ptrValue.Field(i).Set(hValue.Field(i))
}
}
return nil
}
func (h *RequestWrapper) HostName() string {
return ""
}
func (h *RequestWrapper) Params(s string) []string {
return nil
}
================================================
FILE: examples/grpc/grpc-unary-client/README.md
================================================
# gRPC Unary Client Example
This GoFr example demonstrates a simple gRPC unary client that communicates with another gRPC service hosted on a different machine. It serves as a client for another gRPC example included in this examples folder.
Refer to the documentation to setup
### Steps to Run the Example
1. First, start the corresponding `grpc-unary-server` example, which is located at the relative path: `../grpc-unary-server`.
Use the following command to start it:
```console
go run main.go
```
2. Once the `grpc-unary-server` is running, start this server using a similar command:
```console
go run main.go
```
================================================
FILE: examples/grpc/grpc-unary-client/client/health_client.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.6.0
// gofr.dev v1.37.0
// source: hello.proto
package client
import (
"fmt"
"sync"
"time"
"gofr.dev/pkg/gofr"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
gofrgRPC "gofr.dev/pkg/gofr/grpc"
)
var (
metricsOnce sync.Once
gRPCBuckets = []float64{0.005, 0.01, .05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
)
type HealthClient interface {
Check(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error)
Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest, opts ...grpc.CallOption) (
grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse], error)
}
type HealthClientWrapper struct {
client grpc_health_v1.HealthClient
}
func NewHealthClient(conn *grpc.ClientConn) HealthClient {
return &HealthClientWrapper{
client: grpc_health_v1.NewHealthClient(conn),
}
}
func createGRPCConn(host string, serviceName string, dialOptions ...grpc.DialOption) (*grpc.ClientConn, error) {
serviceConfig := `{"loadBalancingPolicy": "round_robin"}`
defaultOpts := []grpc.DialOption{
grpc.WithDefaultServiceConfig(serviceConfig),
grpc.WithTransportCredentials(insecure.NewCredentials()),
}
// Developer Note: If the user provides custom DialOptions, they will override the default options due to
// the ordering of dialOptions. This behavior is intentional to ensure the gRPC client connection is properly
// configured even when the user does not specify any DialOptions.
dialOptions = append(defaultOpts, dialOptions...)
conn, err := grpc.NewClient(host, dialOptions...)
if err != nil {
return nil, err
}
return conn, nil
}
func invokeRPC(ctx *gofr.Context, rpcName string, rpcFunc func() (interface{}, error), metricName string) (interface{}, error) {
span := ctx.Trace("gRPC-srv-call: " + rpcName)
defer span.End()
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
md := metadata.Pairs("x-gofr-traceid", traceID, "x-gofr-spanid", spanID)
ctx.Context = metadata.NewOutgoingContext(ctx.Context, md)
transactionStartTime := time.Now()
res, err := rpcFunc()
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), transactionStartTime, err, rpcName, metricName)
return res, err
}
func (h *HealthClientWrapper) Check(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest,
opts ...grpc.CallOption) (*grpc_health_v1.HealthCheckResponse, error) {
result, err := invokeRPC(ctx, fmt.Sprintf("/grpc.health.v1.Health/Check Service: %q", in.Service), func() (interface{}, error) {
return h.client.Check(ctx, in, opts...)
}, "app_gRPC-Client_stats")
if err != nil {
return nil, err
}
return result.(*grpc_health_v1.HealthCheckResponse), nil
}
func (h *HealthClientWrapper) Watch(ctx *gofr.Context, in *grpc_health_v1.HealthCheckRequest,
opts ...grpc.CallOption) (grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse], error) {
result, err := invokeRPC(ctx, fmt.Sprintf("/grpc.health.v1.Health/Watch Service: %q", in.Service), func() (interface{}, error) {
return h.client.Watch(ctx, in, opts...)
}, "app_gRPC-Stream_stats")
if err != nil {
return nil, err
}
return result.(grpc.ServerStreamingClient[grpc_health_v1.HealthCheckResponse]), nil
}
================================================
FILE: examples/grpc/grpc-unary-client/client/health_test.go
================================================
package client
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/testutil"
)
func TestGoFrHealthClientWrapper_Creation(t *testing.T) {
t.Setenv("GOFR_TELEMETRY", "false")
configs := testutil.NewServerConfigs(t)
t.Run("NewHealthClient", func(t *testing.T) {
// Test GoFr's NewHealthClient function
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
healthClient := NewHealthClient(conn)
assert.NotNil(t, healthClient, "GoFr health client should not be nil")
// Test that it implements the GoFr interface
var _ HealthClient = healthClient
})
t.Run("HealthClientWrapperInterface", func(t *testing.T) {
// Test GoFr's interface compliance
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
healthClient := NewHealthClient(conn)
// Test HealthClient interface compliance
var _ HealthClient = healthClient
// Test that wrapper has the correct GoFr type
wrapper, ok := healthClient.(*HealthClientWrapper)
assert.True(t, ok, "Should be able to cast to GoFr HealthClientWrapper")
assert.NotNil(t, wrapper.client, "Underlying health client should not be nil")
})
}
func TestGoFrHealthClientWrapper_Methods(t *testing.T) {
t.Setenv("GOFR_TELEMETRY", "false")
configs := testutil.NewServerConfigs(t)
// Test GoFr's wrapper methods without actual gRPC calls
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
healthClient := NewHealthClient(conn)
ctx := createTestContext()
t.Run("CheckMethodExists", func(t *testing.T) {
// Test that GoFr's Check method exists and accepts correct parameters
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// This will fail due to connection, but we're testing GoFr's method signature
_, err := healthClient.Check(ctx, req)
assert.Error(t, err, "Should fail with invalid connection, but method should exist")
})
t.Run("WatchMethodExists", func(t *testing.T) {
// Test that GoFr's Watch method exists and accepts correct parameters
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// This will fail due to connection, but we're testing GoFr's method signature
_, err := healthClient.Watch(ctx, req)
assert.Error(t, err, "Should fail with invalid connection, but method should exist")
})
}
func TestGoFrHealthClientWrapper_ContextIntegration(t *testing.T) {
t.Setenv("GOFR_TELEMETRY", "false")
configs := testutil.NewServerConfigs(t)
// Test GoFr's context integration
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
healthClient := NewHealthClient(conn)
t.Run("ContextParameter", func(t *testing.T) {
// Test that GoFr's methods accept *gofr.Context
ctx := createTestContext()
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// Test that the method signature is correct for GoFr context
_, err := healthClient.Check(ctx, req)
assert.Error(t, err, "Should fail with invalid connection")
// Test that context is properly passed (even though call fails)
assert.NotNil(t, ctx, "GoFr context should not be nil")
})
t.Run("ContextTypeCompliance", func(t *testing.T) {
// Test that GoFr's methods expect *gofr.Context specifically
ctx := createTestContext()
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// Verify the method signature expects *gofr.Context
var _ func(*gofr.Context, *healthpb.HealthCheckRequest, ...grpc.CallOption) (*healthpb.HealthCheckResponse, error) = healthClient.Check
var _ func(*gofr.Context, *healthpb.HealthCheckRequest, ...grpc.CallOption) (grpc.ServerStreamingClient[healthpb.HealthCheckResponse], error) = healthClient.Watch
// Ensure the call compiles (even if it fails at runtime)
_, _ = healthClient.Check(ctx, req)
_, _ = healthClient.Watch(ctx, req)
})
}
func TestGoFrHealthClientWrapper_ErrorHandling(t *testing.T) {
t.Setenv("GOFR_TELEMETRY", "false")
// Test GoFr's error handling patterns
t.Run("InvalidConnectionHandling", func(t *testing.T) {
// Test GoFr's handling of invalid connections
conn, err := grpc.Dial("invalid:address", grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
healthClient := NewHealthClient(conn)
ctx := createTestContext()
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// Test GoFr's error handling
_, err = healthClient.Check(ctx, req)
assert.Error(t, err, "GoFr should handle invalid connection errors")
})
}
================================================
FILE: examples/grpc/grpc-unary-client/client/hello.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v5.29.3
// source: hello.proto
package client
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *HelloRequest) Reset() {
*x = HelloRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloRequest) ProtoMessage() {}
func (x *HelloRequest) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{0}
}
func (x *HelloRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type HelloResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *HelloResponse) Reset() {
*x = HelloResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloResponse) ProtoMessage() {}
func (x *HelloResponse) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead.
func (*HelloResponse) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{1}
}
func (x *HelloResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_hello_proto protoreflect.FileDescriptor
var file_hello_proto_rawDesc = []byte{
0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a,
0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x22, 0x29, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x34, 0x0a, 0x05,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x2b, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x12, 0x0d, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x0e, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x6f, 0x66, 0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65,
0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x72, 0x70,
0x63, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_hello_proto_rawDescOnce sync.Once
file_hello_proto_rawDescData = file_hello_proto_rawDesc
)
func file_hello_proto_rawDescGZIP() []byte {
file_hello_proto_rawDescOnce.Do(func() {
file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData)
})
return file_hello_proto_rawDescData
}
var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_hello_proto_goTypes = []interface{}{
(*HelloRequest)(nil), // 0: HelloRequest
(*HelloResponse)(nil), // 1: HelloResponse
}
var file_hello_proto_depIdxs = []int32{
0, // 0: Hello.SayHello:input_type -> HelloRequest
1, // 1: Hello.SayHello:output_type -> HelloResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_hello_proto_init() }
func file_hello_proto_init() {
if File_hello_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_hello_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_hello_proto_goTypes,
DependencyIndexes: file_hello_proto_depIdxs,
MessageInfos: file_hello_proto_msgTypes,
}.Build()
File_hello_proto = out.File
file_hello_proto_rawDesc = nil
file_hello_proto_goTypes = nil
file_hello_proto_depIdxs = nil
}
================================================
FILE: examples/grpc/grpc-unary-client/client/hello.proto
================================================
syntax = "proto3";
option go_package = "gofr.dev/examples/grpc/grpc-unary-client/client";
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service Hello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
================================================
FILE: examples/grpc/grpc-unary-client/client/hello_client.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: hello.proto
package client
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/metrics"
"google.golang.org/grpc"
)
type HelloGoFrClient interface {
SayHello(ctx *gofr.Context, req *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
HealthClient
}
type HelloClientWrapper struct {
client HelloClient
HealthClient
}
func NewHelloGoFrClient(host string, metrics metrics.Manager, dialOptions ...grpc.DialOption) (HelloGoFrClient, error) {
conn, err := createGRPCConn(host, "Hello", dialOptions...)
if err != nil {
return &HelloClientWrapper{
client: nil,
HealthClient: &HealthClientWrapper{client: nil},
}, err
}
metricsOnce.Do(func() {
metrics.NewHistogram("app_gRPC-Client_stats", "Response time of gRPC client in milliseconds.", gRPCBuckets...)
})
res := NewHelloClient(conn)
healthClient := NewHealthClient(conn)
return &HelloClientWrapper{
client: res,
HealthClient: healthClient,
}, nil
}
func (h *HelloClientWrapper) SayHello(ctx *gofr.Context, req *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
result, err := invokeRPC(ctx, "/Hello/SayHello", func() (interface{}, error) {
return h.client.SayHello(ctx.Context, req, opts...)
}, "app_gRPC-Client_stats")
if err != nil {
return nil, err
}
return result.(*HelloResponse), nil
}
================================================
FILE: examples/grpc/grpc-unary-client/client/hello_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v5.29.3
// source: hello.proto
package client
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// HelloClient is the client API for Hello service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}
type helloClient struct {
cc grpc.ClientConnInterface
}
func NewHelloClient(cc grpc.ClientConnInterface) HelloClient {
return &helloClient{cc}
}
func (c *helloClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
out := new(HelloResponse)
err := c.cc.Invoke(ctx, "/Hello/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HelloServer is the server API for Hello service.
// All implementations must embed UnimplementedHelloServer
// for forward compatibility
type HelloServer interface {
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedHelloServer()
}
// UnimplementedHelloServer must be embedded to have forward compatible implementations.
type UnimplementedHelloServer struct {
}
func (UnimplementedHelloServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedHelloServer) mustEmbedUnimplementedHelloServer() {}
// UnsafeHelloServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HelloServer will
// result in compilation errors.
type UnsafeHelloServer interface {
mustEmbedUnimplementedHelloServer()
}
func RegisterHelloServer(s grpc.ServiceRegistrar, srv HelloServer) {
s.RegisterService(&Hello_ServiceDesc, srv)
}
func _Hello_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Hello/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
// Hello_ServiceDesc is the grpc.ServiceDesc for Hello service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Hello_ServiceDesc = grpc.ServiceDesc{
ServiceName: "Hello",
HandlerType: (*HelloServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Hello_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "hello.proto",
}
================================================
FILE: examples/grpc/grpc-unary-client/client/hello_test.go
================================================
package client
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
// createTestContext creates a test gofr.Context
func createTestContext() *gofr.Context {
container := &container.Container{}
return &gofr.Context{
Context: context.Background(),
Container: container,
}
}
func TestGoFrHelloClientWrapper_Creation(t *testing.T) {
configs := testutil.NewServerConfigs(t)
t.Run("NewHelloGoFrClient", func(t *testing.T) {
// Test GoFr's NewHelloGoFrClient function
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
app := gofr.New()
helloClient, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "GoFr hello client creation should not fail")
assert.NotNil(t, helloClient, "GoFr hello client should not be nil")
// Test that it implements the GoFr interface
var _ HelloGoFrClient = helloClient
})
t.Run("HelloClientWrapperInterface", func(t *testing.T) {
// Test GoFr's interface compliance
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Connection creation should not fail immediately")
defer conn.Close()
app := gofr.New()
helloClient, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "GoFr hello client creation should not fail")
// Test HelloGoFrClient interface compliance
var _ HelloGoFrClient = helloClient
// Test that wrapper has the correct GoFr type
wrapper, ok := helloClient.(*HelloClientWrapper)
assert.True(t, ok, "Should be able to cast to GoFr HelloClientWrapper")
assert.NotNil(t, wrapper.client, "Underlying hello client should not be nil")
})
}
func TestGoFrHelloClientWrapper_Methods(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Test GoFr's wrapper methods without actual gRPC calls
app := gofr.New()
helloClient, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "GoFr hello client creation should not fail")
ctx := createTestContext()
t.Run("SayHelloMethodExists", func(t *testing.T) {
// Test that GoFr's SayHello method exists and accepts correct parameters
req := &HelloRequest{
Name: "test-name",
}
// This will fail due to connection, but we're testing GoFr's method signature
_, err := helloClient.SayHello(ctx, req)
assert.Error(t, err, "Should fail with invalid connection, but method should exist")
})
t.Run("HealthClientEmbedded", func(t *testing.T) {
// Test that GoFr's HelloGoFrClient embeds HealthClient
// The HelloGoFrClient interface should include HealthClient methods
var _ HealthClient = helloClient
})
}
func TestGoFrHelloClientWrapper_ContextIntegration(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Test GoFr's context integration
app := gofr.New()
helloClient, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "GoFr hello client creation should not fail")
t.Run("ContextParameter", func(t *testing.T) {
// Test that GoFr's methods accept *gofr.Context
ctx := createTestContext()
req := &HelloRequest{
Name: "test-name",
}
// Test that the method signature is correct for GoFr context
_, err := helloClient.SayHello(ctx, req)
assert.Error(t, err, "Should fail with invalid connection")
// Test that context is properly passed (even though call fails)
assert.NotNil(t, ctx, "GoFr context should not be nil")
})
t.Run("ContextTypeCompliance", func(t *testing.T) {
// Test that GoFr's methods expect *gofr.Context specifically
ctx := createTestContext()
req := &HelloRequest{
Name: "test-name",
}
// Verify the method signature expects *gofr.Context
var _ func(*gofr.Context, *HelloRequest, ...grpc.CallOption) (*HelloResponse, error) = helloClient.SayHello
// Ensure the call compiles (even if it fails at runtime)
_, _ = helloClient.SayHello(ctx, req)
})
}
func TestGoFrHelloClientWrapper_MultipleInstances(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Test GoFr's client creation with multiple instances
t.Run("MultipleHelloClients", func(t *testing.T) {
app := gofr.New()
client1, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "First GoFr hello client creation should not fail")
client2, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "Second GoFr hello client creation should not fail")
assert.NotNil(t, client1, "First GoFr hello client should not be nil")
assert.NotNil(t, client2, "Second GoFr hello client should not be nil")
assert.NotEqual(t, client1, client2, "GoFr hello client instances should be different")
})
}
func TestGoFrHelloClientWrapper_ErrorHandling(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's error handling patterns
t.Run("InvalidAddressHandling", func(t *testing.T) {
// Test GoFr's handling of invalid addresses
app := gofr.New()
helloClient, err := NewHelloGoFrClient("invalid:address", app.Metrics())
require.NoError(t, err, "Client creation should not fail immediately")
ctx := createTestContext()
req := &HelloRequest{
Name: "test-name",
}
// Test GoFr's error handling
_, err = helloClient.SayHello(ctx, req)
assert.Error(t, err, "GoFr should handle invalid address errors")
})
t.Run("EmptyAddressHandling", func(t *testing.T) {
// Test GoFr's handling of empty addresses
app := gofr.New()
helloClient, err := NewHelloGoFrClient("", app.Metrics())
require.NoError(t, err, "Client creation should not fail immediately")
ctx := createTestContext()
req := &HelloRequest{
Name: "test-name",
}
// Test GoFr's error handling
_, err = helloClient.SayHello(ctx, req)
assert.Error(t, err, "GoFr should handle empty address errors")
})
}
func TestGoFrHelloClientWrapper_ConcurrentAccess(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Test GoFr's concurrent access patterns
t.Run("ConcurrentSayHelloCalls", func(t *testing.T) {
app := gofr.New()
helloClient, err := NewHelloGoFrClient(configs.GRPCHost, app.Metrics())
require.NoError(t, err, "GoFr hello client creation should not fail")
numGoroutines := 5
done := make(chan bool, numGoroutines)
for i := 0; i < numGoroutines; i++ {
go func(id int) {
ctx := createTestContext()
req := &HelloRequest{
Name: "concurrent-test",
}
// This will fail due to connection, but we're testing GoFr's concurrency
_, err := helloClient.SayHello(ctx, req)
assert.Error(t, err, "Should fail with invalid connection")
done <- true
}(i)
}
// Wait for all goroutines to complete
for i := 0; i < numGoroutines; i++ {
<-done
}
})
}
================================================
FILE: examples/grpc/grpc-unary-client/main.go
================================================
package main
import (
"gofr.dev/examples/grpc/grpc-unary-client/client"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
// Create a gRPC client for the Hello service
helloGRPCClient, err := client.NewHelloGoFrClient(app.Config.Get("GRPC_SERVER_HOST"), app.Metrics())
if err != nil {
app.Logger().Errorf("Failed to create Hello gRPC client: %v", err)
return
}
greet := NewGreetHandler(helloGRPCClient)
app.GET("/hello", greet.Hello)
app.Run()
}
type GreetHandler struct {
helloGRPCClient client.HelloGoFrClient
}
func NewGreetHandler(helloClient client.HelloGoFrClient) *GreetHandler {
return &GreetHandler{
helloGRPCClient: helloClient,
}
}
func (g GreetHandler) Hello(ctx *gofr.Context) (any, error) {
userName := ctx.Param("name")
if userName == "" {
ctx.Log("Name parameter is empty, defaulting to 'World'")
userName = "World"
}
// HealthCheck to SayHello Service.
// res, err := g.helloGRPCClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{Service: "Hello"})
// if err != nil {
// return nil, err
// } else if res.Status == grpc_health_v1.HealthCheckResponse_NOT_SERVING {
// ctx.Error("Hello Service is down")
// return nil, fmt.Errorf("Hello Service is down")
// }
// Make a gRPC call to the Hello service
helloResponse, err := g.helloGRPCClient.SayHello(ctx, &client.HelloRequest{Name: userName})
if err != nil {
return nil, err
}
return helloResponse, nil
}
================================================
FILE: examples/grpc/grpc-unary-client/main_test.go
================================================
package main
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"gofr.dev/examples/grpc/grpc-unary-client/client"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/testutil"
)
// SimpleHelloServer implements a normal gRPC server using client types
type SimpleHelloServer struct {
client.UnimplementedHelloServer
}
// SayHello implements the unary RPC
func (s *SimpleHelloServer) SayHello(ctx context.Context, req *client.HelloRequest) (*client.HelloResponse, error) {
return &client.HelloResponse{
Message: "Hello " + req.Name + "!",
}, nil
}
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestIntegration_UnaryClient(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Start a simple gRPC server using client types
grpcServer := grpc.NewServer()
helloServer := &SimpleHelloServer{}
client.RegisterHelloServer(grpcServer, helloServer)
// Start the gRPC server
listener, err := net.Listen("tcp", configs.GRPCHost)
require.NoError(t, err, "Failed to create gRPC listener")
go func() {
if err := grpcServer.Serve(listener); err != nil {
t.Logf("gRPC server error: %v", err)
}
}()
defer grpcServer.Stop()
// Give gRPC server time to start
time.Sleep(100 * time.Millisecond)
// Set the gRPC server host for the client
t.Setenv("GRPC_SERVER_HOST", configs.GRPCHost)
// Start the HTTP server (unary client example)
go main()
time.Sleep(100 * time.Millisecond) // Give HTTP server time to start
// Test HTTP endpoints that use GoFr gRPC client internally
tests := []struct {
desc string
path string
expected string
}{
{"hello with name", "/hello?name=" + url.QueryEscape("gofr"), "Hello gofr!"},
{"hello with empty name", "/hello", "Hello World!"},
{"hello with unicode", "/hello?name=" + url.QueryEscape("你好世界"), "Hello 你好世界!"},
{"hello with long name", "/hello?name=" + url.QueryEscape("ThisIsAVeryLongNameThatShouldStillWork"), "Hello ThisIsAVeryLongNameThatShouldStillWork!"},
}
for i, tc := range tests {
// Properly encode the URL to handle special characters
baseURL := fmt.Sprintf("http://localhost:%d%s", configs.HTTPPort, tc.path)
parsedURL, err := url.Parse(baseURL)
require.NoError(t, err, "TEST[%d], Failed to parse URL.\n%s", i, tc.desc)
resp, err := http.Get(parsedURL.String())
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Contains(t, string(body), tc.expected, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, http.StatusOK, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestIntegration_UnaryClient_Concurrent(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Start a simple gRPC server using client types
grpcServer := grpc.NewServer()
helloServer := &SimpleHelloServer{}
client.RegisterHelloServer(grpcServer, helloServer)
// Start the gRPC server
listener, err := net.Listen("tcp", configs.GRPCHost)
require.NoError(t, err, "Failed to create gRPC listener")
go func() {
if err := grpcServer.Serve(listener); err != nil {
t.Logf("gRPC server error: %v", err)
}
}()
defer grpcServer.Stop()
// Give gRPC server time to start
time.Sleep(100 * time.Millisecond)
// Set the gRPC server host for the client
t.Setenv("GRPC_SERVER_HOST", configs.GRPCHost)
// Start the HTTP server (unary client example)
go main()
time.Sleep(100 * time.Millisecond) // Give HTTP server time to start
numClients := 5
done := make(chan bool, numClients)
for i := 0; i < numClients; i++ {
go func(id int) {
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/hello?name=concurrent+client+%d", configs.HTTPPort, id))
require.NoError(t, err, "Concurrent HTTP request failed for client %d", id)
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Concurrent HTTP request failed for client %d", id)
assert.Contains(t, string(body), fmt.Sprintf("Hello concurrent client %d!", id), "Unexpected response message for concurrent client %d", id)
assert.Equal(t, http.StatusOK, resp.StatusCode, "Concurrent HTTP request failed for client %d", id)
done <- true
}(i)
}
// Wait for all concurrent clients to complete
for i := 0; i < numClients; i++ {
<-done
}
}
func TestIntegration_UnaryClient_ErrorHandling(t *testing.T) {
t.Run("InvalidGRPCServerHost", func(t *testing.T) {
// Test with invalid gRPC server host
t.Setenv("GRPC_SERVER_HOST", "invalid:address")
// Create a new app to test with invalid host
app := gofr.New()
_, err := client.NewHelloGoFrClient(app.Config.Get("GRPC_SERVER_HOST"), app.Metrics())
// GoFr client creation might not fail immediately for invalid addresses
// The error will occur when actually making RPC calls
if err != nil {
assert.Error(t, err, "Should fail with invalid gRPC server address")
}
})
t.Run("EmptyGRPCServerHost", func(t *testing.T) {
// Test with empty gRPC server host
t.Setenv("GRPC_SERVER_HOST", "")
// Create a new app to test with empty host
app := gofr.New()
_, err := client.NewHelloGoFrClient(app.Config.Get("GRPC_SERVER_HOST"), app.Metrics())
// GoFr client creation might not fail immediately for empty addresses
// The error will occur when actually making RPC calls
if err != nil {
assert.Error(t, err, "Should fail with empty gRPC server address")
}
})
}
================================================
FILE: examples/grpc/grpc-unary-server/README.md
================================================
# GRPC Server Example
This GoFr example showcases a basic gRPC unary server implementation. For detailed instructions on setting up gRPC handlers with GoFr’s context support—enabling built-in tracing and database integration within your gRPC handlers—please refer to our [official documentation](https://gofr.dev/docs/advanced-guide/grpc).
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/grpc/grpc-unary-server/main.go
================================================
package main
import (
"gofr.dev/examples/grpc/grpc-unary-server/server"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
server.RegisterHelloServerWithGofr(app, server.NewHelloGoFrServer())
app.Run()
}
================================================
FILE: examples/grpc/grpc-unary-server/main_test.go
================================================
package main
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/status"
"gofr.dev/examples/grpc/grpc-unary-server/server"
"gofr.dev/pkg/gofr"
gofrGrpc "gofr.dev/pkg/gofr/grpc"
"gofr.dev/pkg/gofr/http/middleware"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestIntegration_UnaryServer(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
// Create gRPC client connection
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Failed to connect to unary server")
defer conn.Close()
client := server.NewHelloClient(conn)
tests := []struct {
desc string
name string
expected string
}{
{"hello with name", "gofr", "Hello gofr!"},
{"hello with empty name", "", "Hello World!"},
{"hello with special chars", "!@#$%^&*", "Hello !@#$%^&*!"},
{"hello with unicode", "你好世界", "Hello 你好世界!"},
{"hello with long name", "ThisIsAVeryLongNameThatShouldStillWork", "Hello ThisIsAVeryLongNameThatShouldStillWork!"},
}
for i, tc := range tests {
resp, err := client.SayHello(context.Background(), &server.HelloRequest{
Name: tc.name,
})
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, resp.Message, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestIntegration_UnaryServer_Concurrent(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
// Create gRPC client connection
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Failed to connect to unary server")
defer conn.Close()
client := server.NewHelloClient(conn)
numClients := 5
done := make(chan bool, numClients)
for i := 0; i < numClients; i++ {
go func(id int) {
resp, err := client.SayHello(context.Background(), &server.HelloRequest{
Name: "concurrent client " + string(rune(id)),
})
require.NoError(t, err, "Concurrent SayHello RPC failed for client %d", id)
assert.Contains(t, resp.Message, "concurrent client", "Unexpected response message for concurrent client %d", id)
done <- true
}(i)
}
// Wait for all concurrent clients to complete
for i := 0; i < numClients; i++ {
<-done
}
}
func TestIntegration_UnaryServer_ErrorHandling(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
// Create gRPC client connection
conn, err := grpc.Dial(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Failed to connect to unary server")
defer conn.Close()
client := server.NewHelloClient(conn)
t.Run("ContextCancellation", func(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
_, err := client.SayHello(ctx, &server.HelloRequest{
Name: "cancel test",
})
assert.Error(t, err, "Context cancellation should return error")
assert.Contains(t, err.Error(), "context canceled")
})
t.Run("TimeoutHandling", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Nanosecond) // Very short timeout
defer cancel()
_, err := client.SayHello(ctx, &server.HelloRequest{
Name: "timeout test",
})
assert.Error(t, err, "Timeout should return error")
assert.Contains(t, err.Error(), "context deadline exceeded")
})
}
func TestHelloProtoMethods(t *testing.T) {
// Test HelloRequest methods
req := &server.HelloRequest{Name: "John"}
assert.Equal(t, "John", req.GetName())
assert.Equal(t, "name:\"John\"", req.String())
// Test HelloResponse methods
resp := &server.HelloResponse{Message: "Hello World"}
assert.Equal(t, "Hello World", resp.GetMessage())
assert.Equal(t, "message:\"Hello World\"", resp.String())
}
func TestIntegration_UnaryServer_RateLimited(t *testing.T) {
configs := testutil.NewServerConfigs(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
app := gofr.New()
rateLimiterCfg := middleware.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
}
app.AddGRPCUnaryInterceptors(
gofrGrpc.UnaryRateLimitInterceptor(ctx, rateLimiterCfg, app.Logger(), app.Metrics()),
)
server.RegisterHelloServerWithGofr(app, server.NewHelloGoFrServer())
app.Run()
}()
time.Sleep(200 * time.Millisecond)
conn, err := grpc.NewClient(configs.GRPCHost, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err, "Failed to connect to rate-limited server")
defer conn.Close()
client := server.NewHelloClient(conn)
// Should succeed
for i := 0; i < 2; i++ {
resp, callErr := client.SayHello(context.Background(), &server.HelloRequest{Name: "gofr"})
require.NoError(t, callErr, "Request %d should succeed within burst", i+1)
assert.Equal(t, "Hello gofr!", resp.GetMessage())
}
// Should hit the rate limit
_, callErr := client.SayHello(context.Background(), &server.HelloRequest{Name: "gofr"})
require.Error(t, callErr, "3rd request should be rate limited")
st, ok := status.FromError(callErr)
require.True(t, ok, "Error should be a gRPC status")
assert.Equal(t, codes.ResourceExhausted, st.Code(), "Should return RESOURCE_EXHAUSTED")
}
================================================
FILE: examples/grpc/grpc-unary-server/server/health_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: hello.proto
package server
import (
"fmt"
"google.golang.org/grpc"
"time"
"gofr.dev/pkg/gofr"
gofrGRPC "gofr.dev/pkg/gofr/grpc"
"google.golang.org/grpc/health"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
type healthServer struct {
*health.Server
}
var globalHealthServer *healthServer
var healthServerRegistered bool // Global flag to track if health server is registered
// getOrCreateHealthServer ensures only one health server is created and reused.
func getOrCreateHealthServer() *healthServer {
if globalHealthServer == nil {
globalHealthServer = &healthServer{health.NewServer()}
}
return globalHealthServer
}
func registerServerWithGofr(app *gofr.App, srv any, registerFunc func(grpc.ServiceRegistrar, any)) {
var s grpc.ServiceRegistrar = app
h := getOrCreateHealthServer()
// Register metrics and health server only once
if !healthServerRegistered {
gRPCBuckets := []float64{0.005, 0.01, .05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
app.Metrics().NewHistogram("app_gRPC-Server_stats", "Response time of gRPC server in milliseconds.", gRPCBuckets...)
app.Metrics().NewHistogram("app_gRPC-Stream_stats", "Duration of gRPC stream in milliseconds.", gRPCBuckets...)
healthpb.RegisterHealthServer(s, h.Server)
h.Server.SetServingStatus("", healthpb.HealthCheckResponse_SERVING)
healthServerRegistered = true
}
// Register the provided server
registerFunc(s, srv)
}
func (h *healthServer) Check(ctx *gofr.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Check")
res, err := h.Server.Check(ctx.Context, req)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, err,
fmt.Sprintf("/grpc.health.v1.Health/Check Service: %q", req.Service), "app_gRPC-Server_stats")
span.End()
return res, err
}
func (h *healthServer) Watch(ctx *gofr.Context, in *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Watch")
err := h.Server.Watch(in, stream)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, err,
fmt.Sprintf("/grpc.health.v1.Health/Watch Service: %q", in.Service), "app_gRPC-Server_stats")
span.End()
return err
}
func (h *healthServer) SetServingStatus(ctx *gofr.Context, service string, servingStatus healthpb.HealthCheckResponse_ServingStatus) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/SetServingStatus")
h.Server.SetServingStatus(service, servingStatus)
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
fmt.Sprintf("/grpc.health.v1.Health/SetServingStatus Service: %q", service), "app_gRPC-Server_stats")
span.End()
}
func (h *healthServer) Shutdown(ctx *gofr.Context) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Shutdown")
h.Server.Shutdown()
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
"/grpc.health.v1.Health/Shutdown", "app_gRPC-Server_stats")
span.End()
}
func (h *healthServer) Resume(ctx *gofr.Context) {
start := time.Now()
span := ctx.Trace("/grpc.health.v1.Health/Resume")
h.Server.Resume()
logger := gofrGRPC.NewgRPCLogger()
logger.DocumentRPCLog(ctx.Context, ctx.Logger, ctx.Metrics(), start, nil,
"/grpc.health.v1.Health/Resume", "app_gRPC-Server_stats")
span.End()
}
================================================
FILE: examples/grpc/grpc-unary-server/server/health_test.go
================================================
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
// createTestContext creates a test gofr.Context
func createTestContext() *gofr.Context {
container := &container.Container{}
return &gofr.Context{
Context: context.Background(),
Container: container,
}
}
func TestGoFrHealthServer_Creation(t *testing.T) {
t.Run("GetOrCreateHealthServer", func(t *testing.T) {
// Test GoFr's getOrCreateHealthServer function
healthServer := getOrCreateHealthServer()
assert.NotNil(t, healthServer, "GoFr health server should not be nil")
// Test that it implements the GoFr interface (not the standard gRPC interface)
// The GoFr health server has different method signatures
assert.NotNil(t, healthServer, "Health server should not be nil")
})
t.Run("HealthServerSingleton", func(t *testing.T) {
// Test GoFr's singleton pattern for health server
healthServer1 := getOrCreateHealthServer()
healthServer2 := getOrCreateHealthServer()
assert.Equal(t, healthServer1, healthServer2, "GoFr health server should be singleton")
})
}
func TestGoFrHealthServer_Methods(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's health server methods
healthServer := getOrCreateHealthServer()
ctx := createTestContext()
t.Run("CheckMethodExists", func(t *testing.T) {
// Test that GoFr's Check method exists and accepts correct parameters
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// Test GoFr's Check method signature - this will fail with "unknown service" which is expected
resp, err := healthServer.Check(ctx, req)
assert.Error(t, err, "Health check should fail for unknown service")
assert.Nil(t, resp, "Health check response should be nil for unknown service")
assert.Contains(t, err.Error(), "unknown service", "Error should indicate unknown service")
})
t.Run("WatchMethodExists", func(t *testing.T) {
// Test that GoFr's Watch method exists and accepts correct parameters
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
// Test GoFr's Watch method signature - this will panic with nil stream, but we're testing method existence
assert.Panics(t, func() {
healthServer.Watch(ctx, req, nil)
}, "Watch should panic with nil stream, but method should exist")
})
}
func TestGoFrHealthServer_SetServingStatus(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's SetServingStatus functionality
healthServer := getOrCreateHealthServer()
ctx := createTestContext()
t.Run("SetServingStatus", func(t *testing.T) {
// Test GoFr's SetServingStatus method
healthServer.SetServingStatus(ctx, "test-service", healthpb.HealthCheckResponse_SERVING)
// Verify the status was set
req := &healthpb.HealthCheckRequest{
Service: "test-service",
}
resp, err := healthServer.Check(ctx, req)
require.NoError(t, err, "Health check should not fail")
assert.Equal(t, healthpb.HealthCheckResponse_SERVING, resp.Status, "Service should be serving")
})
t.Run("SetNotServingStatus", func(t *testing.T) {
// Test GoFr's SetServingStatus with NOT_SERVING
healthServer.SetServingStatus(ctx, "test-service-not-serving", healthpb.HealthCheckResponse_NOT_SERVING)
// Verify the status was set
req := &healthpb.HealthCheckRequest{
Service: "test-service-not-serving",
}
resp, err := healthServer.Check(ctx, req)
require.NoError(t, err, "Health check should not fail")
assert.Equal(t, healthpb.HealthCheckResponse_NOT_SERVING, resp.Status, "Service should not be serving")
})
}
func TestGoFrHealthServer_Shutdown(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's Shutdown functionality
healthServer := getOrCreateHealthServer()
ctx := createTestContext()
t.Run("Shutdown", func(t *testing.T) {
// Test GoFr's Shutdown method
healthServer.Shutdown(ctx)
// After shutdown, all services should return NOT_SERVING
req := &healthpb.HealthCheckRequest{
Service: "any-service",
}
resp, err := healthServer.Check(ctx, req)
// After shutdown, health checks should fail with "unknown service"
assert.Error(t, err, "Health check should fail after shutdown")
assert.Nil(t, resp, "Health check response should be nil after shutdown")
assert.Contains(t, err.Error(), "unknown service", "Error should indicate unknown service after shutdown")
})
}
func TestGoFrHealthServer_Resume(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's Resume functionality
healthServer := getOrCreateHealthServer()
ctx := createTestContext()
t.Run("Resume", func(t *testing.T) {
// Test GoFr's Resume method
healthServer.Resume(ctx)
// After resume, services should return to their previous status
healthServer.SetServingStatus(ctx, "test-service-resume", healthpb.HealthCheckResponse_SERVING)
req := &healthpb.HealthCheckRequest{
Service: "test-service-resume",
}
resp, err := healthServer.Check(ctx, req)
require.NoError(t, err, "Health check should not fail")
assert.Equal(t, healthpb.HealthCheckResponse_SERVING, resp.Status, "Service should be serving after resume")
})
}
func TestGoFrHealthServer_MultipleInstances(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's singleton pattern
t.Run("SingletonPattern", func(t *testing.T) {
healthServer1 := getOrCreateHealthServer()
healthServer2 := getOrCreateHealthServer()
ctx := createTestContext()
assert.Equal(t, healthServer1, healthServer2, "GoFr health server should be singleton")
// Test that operations on one affect the other
healthServer1.SetServingStatus(ctx, "singleton-test", healthpb.HealthCheckResponse_SERVING)
req := &healthpb.HealthCheckRequest{
Service: "singleton-test",
}
resp, err := healthServer2.Check(ctx, req)
require.NoError(t, err, "Health check should not fail")
assert.Equal(t, healthpb.HealthCheckResponse_SERVING, resp.Status, "Singleton should share state")
})
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello.pb.go
================================================
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v5.29.3
// source: hello.proto
package server
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type HelloRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
}
func (x *HelloRequest) Reset() {
*x = HelloRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloRequest) ProtoMessage() {}
func (x *HelloRequest) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead.
func (*HelloRequest) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{0}
}
func (x *HelloRequest) GetName() string {
if x != nil {
return x.Name
}
return ""
}
type HelloResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
}
func (x *HelloResponse) Reset() {
*x = HelloResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_hello_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *HelloResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HelloResponse) ProtoMessage() {}
func (x *HelloResponse) ProtoReflect() protoreflect.Message {
mi := &file_hello_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HelloResponse.ProtoReflect.Descriptor instead.
func (*HelloResponse) Descriptor() ([]byte, []int) {
return file_hello_proto_rawDescGZIP(), []int{1}
}
func (x *HelloResponse) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_hello_proto protoreflect.FileDescriptor
var file_hello_proto_rawDesc = []byte{
0x0a, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x22, 0x0a,
0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a,
0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d,
0x65, 0x22, 0x29, 0x0a, 0x0d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x34, 0x0a, 0x05,
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x2b, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c,
0x6f, 0x12, 0x0d, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x0e, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x00, 0x42, 0x2b, 0x5a, 0x29, 0x67, 0x6f, 0x66, 0x72, 0x2e, 0x64, 0x65, 0x76, 0x2f, 0x65,
0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x67, 0x72, 0x70,
0x63, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_hello_proto_rawDescOnce sync.Once
file_hello_proto_rawDescData = file_hello_proto_rawDesc
)
func file_hello_proto_rawDescGZIP() []byte {
file_hello_proto_rawDescOnce.Do(func() {
file_hello_proto_rawDescData = protoimpl.X.CompressGZIP(file_hello_proto_rawDescData)
})
return file_hello_proto_rawDescData
}
var file_hello_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_hello_proto_goTypes = []interface{}{
(*HelloRequest)(nil), // 0: HelloRequest
(*HelloResponse)(nil), // 1: HelloResponse
}
var file_hello_proto_depIdxs = []int32{
0, // 0: Hello.SayHello:input_type -> HelloRequest
1, // 1: Hello.SayHello:output_type -> HelloResponse
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_hello_proto_init() }
func file_hello_proto_init() {
if File_hello_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_hello_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_hello_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*HelloResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_hello_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_hello_proto_goTypes,
DependencyIndexes: file_hello_proto_depIdxs,
MessageInfos: file_hello_proto_msgTypes,
}.Build()
File_hello_proto = out.File
file_hello_proto_rawDesc = nil
file_hello_proto_goTypes = nil
file_hello_proto_depIdxs = nil
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello.proto
================================================
syntax = "proto3";
option go_package = "gofr.dev/examples/grpc/grpc-server/server";
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service Hello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: hello.proto
package server
import (
"context"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
gofrgRPC "gofr.dev/pkg/gofr/grpc"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
)
// NewHelloGoFrServer creates a new instance of HelloGoFrServer
func NewHelloGoFrServer() *HelloGoFrServer {
return &HelloGoFrServer{
health: getOrCreateHealthServer(), // Initialize the health server
}
}
// HelloServerWithGofr is the interface for the server implementation
type HelloServerWithGofr interface {
SayHello(*gofr.Context) (any, error)
}
// HelloServerWrapper wraps the server and handles request and response logic
type HelloServerWrapper struct {
HelloServer
*healthServer
Container *container.Container
server HelloServerWithGofr
}
// Base instrumented stream
type instrumentedStream struct {
grpc.ServerStream
ctx *gofr.Context
method string
}
func (s *instrumentedStream) Context() context.Context {
return s.ctx
}
func (s *instrumentedStream) SendMsg(m interface{}) error {
start := time.Now()
span := s.ctx.Trace(s.method + "/SendMsg")
defer span.End()
err := s.ServerStream.SendMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(s.ctx, s.ctx.Logger, s.ctx.Metrics(), start, err,
s.method+"/SendMsg", "app_gRPC-Stream_stats")
return err
}
func (s *instrumentedStream) RecvMsg(m interface{}) error {
start := time.Now()
span := s.ctx.Trace(s.method + "/RecvMsg")
defer span.End()
err := s.ServerStream.RecvMsg(m)
logger := gofrgRPC.NewgRPCLogger()
logger.DocumentRPCLog(s.ctx, s.ctx.Logger, s.ctx.Metrics(), start, err,
s.method+"/RecvMsg", "app_gRPC-Stream_stats")
return err
}
// Unary method handler for SayHello
func (h *HelloServerWrapper) SayHello(ctx context.Context, req *HelloRequest) (*HelloResponse, error) {
gctx := h.getGofrContext(ctx, &HelloRequestWrapper{ctx: ctx, HelloRequest: req})
res, err := h.server.SayHello(gctx)
if err != nil {
return nil, err
}
resp, ok := res.(*HelloResponse)
if !ok {
return nil, status.Errorf(codes.Unknown, "unexpected response type %T", res)
}
return resp, nil
}
// mustEmbedUnimplementedHelloServer ensures implementation
func (h *HelloServerWrapper) mustEmbedUnimplementedHelloServer() {}
// RegisterHelloServerWithGofr registers the server
func RegisterHelloServerWithGofr(app *gofr.App, srv HelloServerWithGofr) {
registerServerWithGofr(app, srv, func(s grpc.ServiceRegistrar, srv any) {
wrapper := &HelloServerWrapper{
server: srv.(HelloServerWithGofr),
healthServer: getOrCreateHealthServer(),
}
RegisterHelloServer(s, wrapper)
wrapper.Server.SetServingStatus("Hello", healthpb.HealthCheckResponse_SERVING)
})
}
// getGofrContext creates GoFr context
func (h *HelloServerWrapper) getGofrContext(ctx context.Context, req gofr.Request) *gofr.Context {
return &gofr.Context{
Context: ctx,
Container: h.Container,
Request: req,
}
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello_grpc.pb.go
================================================
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v5.29.3
// source: hello.proto
package server
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// HelloClient is the client API for Hello service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type HelloClient interface {
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error)
}
type helloClient struct {
cc grpc.ClientConnInterface
}
func NewHelloClient(cc grpc.ClientConnInterface) HelloClient {
return &helloClient{cc}
}
func (c *helloClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloResponse, error) {
out := new(HelloResponse)
err := c.cc.Invoke(ctx, "/Hello/SayHello", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// HelloServer is the server API for Hello service.
// All implementations must embed UnimplementedHelloServer
// for forward compatibility
type HelloServer interface {
SayHello(context.Context, *HelloRequest) (*HelloResponse, error)
mustEmbedUnimplementedHelloServer()
}
// UnimplementedHelloServer must be embedded to have forward compatible implementations.
type UnimplementedHelloServer struct {
}
func (UnimplementedHelloServer) SayHello(context.Context, *HelloRequest) (*HelloResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented")
}
func (UnimplementedHelloServer) mustEmbedUnimplementedHelloServer() {}
// UnsafeHelloServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to HelloServer will
// result in compilation errors.
type UnsafeHelloServer interface {
mustEmbedUnimplementedHelloServer()
}
func RegisterHelloServer(s grpc.ServiceRegistrar, srv HelloServer) {
s.RegisterService(&Hello_ServiceDesc, srv)
}
func _Hello_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(HelloRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(HelloServer).SayHello(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/Hello/SayHello",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(HelloServer).SayHello(ctx, req.(*HelloRequest))
}
return interceptor(ctx, in, info, handler)
}
// Hello_ServiceDesc is the grpc.ServiceDesc for Hello service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Hello_ServiceDesc = grpc.ServiceDesc{
ServiceName: "Hello",
HandlerType: (*HelloServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "SayHello",
Handler: _Hello_SayHello_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "hello.proto",
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello_server.go
================================================
// versions:
// gofr-cli v0.6.0
// gofr.dev v1.37.0
// source: hello.proto
package server
import (
"fmt"
"gofr.dev/pkg/gofr"
)
// Register the gRPC service in your app using the following code in your main.go:
//
// server.RegisterHelloServerWithGofr(app, &server.NewHelloGoFrServer())
//
// HelloGoFrServer defines the gRPC server implementation.
// Customize the struct with required dependencies and fields as needed.
type HelloGoFrServer struct {
health *healthServer
}
func (s *HelloGoFrServer) SayHello(ctx *gofr.Context) (any, error) {
request := HelloRequest{}
err := ctx.Bind(&request)
if err != nil {
return nil, err
}
name := request.Name
if name == "" {
name = "World"
}
//Performing HealthCheck
//res, err := s.health.Check(ctx, &grpc_health_v1.HealthCheckRequest{
// Service: "Hello",
//})
//ctx.Log(res.String())
// Setting the serving status
//s.health.SetServingStatus(ctx, "Hello", grpc_health_v1.HealthCheckResponse_NOT_SERVING)
return &HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}, nil
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello_server_test.go
================================================
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr"
)
func TestServer_SayHello(t *testing.T) {
s := HelloGoFrServer{}
tests := []struct {
input string
resp string
}{
{"world", "Hello world!"},
{"123", "Hello 123!"},
{"", "Hello World!"},
}
for i, tc := range tests {
req := &HelloRequest{Name: tc.input}
request := &HelloRequestWrapper{
context.Background(),
req,
}
ctx := &gofr.Context{
Request: request,
}
resp, err := s.SayHello(ctx)
grpcResponse, ok := resp.(*HelloResponse)
require.True(t, ok)
require.NoError(t, err, "TEST[%d], Failed.\n", i)
assert.Equal(t, tc.resp, grpcResponse.Message, "TEST[%d], Failed.\n", i)
}
}
================================================
FILE: examples/grpc/grpc-unary-server/server/hello_test.go
================================================
package server
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
healthpb "google.golang.org/grpc/health/grpc_health_v1"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func TestGoFrHelloServer_Creation(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("HelloGoFrServerCreation", func(t *testing.T) {
// Test GoFr's HelloGoFrServer creation
app := gofr.New()
helloServer := &HelloGoFrServer{}
assert.NotNil(t, helloServer, "GoFr hello server should not be nil")
assert.NotNil(t, app, "GoFr app should not be nil")
// Test that it implements the GoFr interface
var _ HelloServerWithGofr = helloServer
})
t.Run("HelloServerWrapperCreation", func(t *testing.T) {
// Test GoFr's HelloServerWrapper creation
app := gofr.New()
helloServer := &HelloGoFrServer{}
wrapper := &HelloServerWrapper{
server: helloServer,
}
assert.NotNil(t, wrapper, "GoFr hello server wrapper should not be nil")
assert.Equal(t, helloServer, wrapper.server, "Wrapper should contain the server")
assert.NotNil(t, app, "GoFr app should not be nil")
})
}
func TestGoFrHelloServer_Methods(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's hello server methods
helloServer := &HelloGoFrServer{}
ctx := createTestContext()
t.Run("SayHelloMethodExists", func(t *testing.T) {
// Test that GoFr's SayHello method exists and accepts correct parameters
// Create a mock request in the context using the wrapper
ctx.Request = &HelloRequestWrapper{
HelloRequest: &HelloRequest{
Name: "test-name",
},
}
// Test GoFr's SayHello method signature
resp, err := helloServer.SayHello(ctx)
require.NoError(t, err, "GoFr SayHello should not fail")
assert.NotNil(t, resp, "SayHello response should not be nil")
// Verify the response type
helloResp, ok := resp.(*HelloResponse)
assert.True(t, ok, "Response should be HelloResponse")
assert.Contains(t, helloResp.Message, "test-name", "Response should contain the name")
})
t.Run("SayHelloWithEmptyName", func(t *testing.T) {
// Test GoFr's SayHello with empty name (should default to "World")
ctx.Request = &HelloRequestWrapper{
HelloRequest: &HelloRequest{
Name: "",
},
}
resp, err := helloServer.SayHello(ctx)
require.NoError(t, err, "GoFr SayHello with empty name should not fail")
assert.NotNil(t, resp, "SayHello response should not be nil")
helloResp, ok := resp.(*HelloResponse)
assert.True(t, ok, "Response should be HelloResponse")
assert.Contains(t, helloResp.Message, "World", "Empty name should default to World")
})
}
func TestGoFrHelloServer_ContextIntegration(t *testing.T) {
_ = testutil.NewServerConfigs(t)
helloServer := &HelloGoFrServer{}
t.Run("ContextBinding", func(t *testing.T) {
// Test GoFr's context binding functionality
ctx := createTestContext()
ctx.Request = &HelloRequestWrapper{
HelloRequest: &HelloRequest{
Name: "context-test",
},
}
resp, err := helloServer.SayHello(ctx)
require.NoError(t, err, "GoFr SayHello should not fail")
assert.NotNil(t, resp, "SayHello response should not be nil")
helloResp, ok := resp.(*HelloResponse)
assert.True(t, ok, "Response should be HelloResponse")
assert.Contains(t, helloResp.Message, "context-test", "Response should contain the context name")
})
t.Run("ContextTypeCompliance", func(t *testing.T) {
// Test that GoFr's methods expect *gofr.Context specifically
ctx := createTestContext()
ctx.Request = &HelloRequestWrapper{
HelloRequest: &HelloRequest{
Name: "type-test",
},
}
// Verify the method signature expects *gofr.Context
var _ func(*gofr.Context) (any, error) = helloServer.SayHello
// Ensure the call compiles (even if it fails at runtime)
_, _ = helloServer.SayHello(ctx)
})
}
func TestGoFrHelloServer_Registration(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's server registration functionality
t.Run("RegisterHelloServerWithGofr", func(t *testing.T) {
// Test GoFr's RegisterHelloServerWithGofr function
app := gofr.New()
helloServer := &HelloGoFrServer{}
// This should not panic and should register the server
assert.NotPanics(t, func() {
RegisterHelloServerWithGofr(app, helloServer)
}, "RegisterHelloServerWithGofr should not panic")
})
}
func TestGoFrHelloServer_HealthIntegration(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's health integration
t.Run("HealthIntegration", func(t *testing.T) {
app := gofr.New()
helloServer := &HelloGoFrServer{}
// Register the server to set up health checks
RegisterHelloServerWithGofr(app, helloServer)
// Test that health server is properly integrated
healthServer := getOrCreateHealthServer()
assert.NotNil(t, healthServer, "Health server should be available")
// Create a context for the health check
ctx := createTestContext()
// Check that Hello service is registered as serving
req := &healthpb.HealthCheckRequest{
Service: "Hello",
}
resp, err := healthServer.Check(ctx, req)
require.NoError(t, err, "Health check should not fail")
assert.Equal(t, healthpb.HealthCheckResponse_SERVING, resp.Status, "Hello service should be serving")
})
}
func TestGoFrHelloServer_MultipleInstances(t *testing.T) {
_ = testutil.NewServerConfigs(t)
// Test GoFr's multiple server instances
t.Run("MultipleHelloServers", func(t *testing.T) {
app := gofr.New()
server1 := &HelloGoFrServer{}
server2 := &HelloGoFrServer{}
assert.NotNil(t, server1, "First GoFr hello server should not be nil")
assert.NotNil(t, server2, "Second GoFr hello server should not be nil")
// Check that they are different objects (different memory addresses)
assert.True(t, server1 != server2, "GoFr hello server instances should be different objects")
assert.NotNil(t, app, "GoFr app should not be nil")
// Test that both can be created (but not registered to avoid duplicate service error)
assert.NotNil(t, server1, "First server should be valid")
assert.NotNil(t, server2, "Second server should be valid")
})
}
func TestNewHelloGoFrServer(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("NewHelloGoFrServerCreation", func(t *testing.T) {
// Test GoFr's NewHelloGoFrServer function
server := NewHelloGoFrServer()
assert.NotNil(t, server, "NewHelloGoFrServer should not return nil")
assert.NotNil(t, server.health, "Health server should be initialized")
// Test that it implements the GoFr interface
var _ HelloServerWithGofr = server
})
}
func TestHelloServerWrapper_SayHello(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("SayHelloWrapper", func(t *testing.T) {
// Create a mock server implementation
mockServer := &mockHelloServer{}
// Create wrapper
wrapper := &HelloServerWrapper{
server: mockServer,
healthServer: getOrCreateHealthServer(),
Container: &container.Container{},
}
// Test SayHello method
ctx := context.Background()
req := &HelloRequest{Name: "test"}
resp, err := wrapper.SayHello(ctx, req)
require.NoError(t, err, "SayHello should not fail")
assert.NotNil(t, resp, "Response should not be nil")
assert.Equal(t, "Hello test!", resp.Message, "Response message should match")
})
t.Run("SayHelloWithError", func(t *testing.T) {
// Create a mock server that returns an error
mockServer := &mockHelloServerWithError{}
// Create wrapper
wrapper := &HelloServerWrapper{
server: mockServer,
healthServer: getOrCreateHealthServer(),
Container: &container.Container{},
}
// Test SayHello method with error
ctx := context.Background()
req := &HelloRequest{Name: "error"}
resp, err := wrapper.SayHello(ctx, req)
assert.Error(t, err, "SayHello should return error")
assert.Nil(t, resp, "Response should be nil on error")
assert.Contains(t, err.Error(), "test error", "Error message should match")
})
t.Run("SayHelloWithWrongResponseType", func(t *testing.T) {
// Create a mock server that returns wrong type
mockServer := &mockHelloServerWrongType{}
// Create wrapper
wrapper := &HelloServerWrapper{
server: mockServer,
healthServer: getOrCreateHealthServer(),
Container: &container.Container{},
}
// Test SayHello method with wrong response type
ctx := context.Background()
req := &HelloRequest{Name: "wrong"}
resp, err := wrapper.SayHello(ctx, req)
assert.Error(t, err, "SayHello should return error for wrong response type")
assert.Nil(t, resp, "Response should be nil on error")
assert.Contains(t, err.Error(), "unexpected response type", "Error message should indicate wrong type")
})
}
func TestHelloServerWrapper_getGofrContext(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("getGofrContext", func(t *testing.T) {
// Create wrapper
wrapper := &HelloServerWrapper{
Container: &container.Container{},
}
// Test getGofrContext method
ctx := context.Background()
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
gofrCtx := wrapper.getGofrContext(ctx, req)
assert.NotNil(t, gofrCtx, "GoFr context should not be nil")
assert.Equal(t, ctx, gofrCtx.Context, "Context should match")
assert.Equal(t, &container.Container{}, gofrCtx.Container, "Container should match")
assert.Equal(t, req, gofrCtx.Request, "Request should match")
})
}
func TestInstrumentedStream(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("InstrumentedStreamContext", func(t *testing.T) {
// Create a mock server stream
mockStream := &mockServerStream{}
// Create instrumented stream
gofrCtx := createTestContext()
stream := &instrumentedStream{
ServerStream: mockStream,
ctx: gofrCtx,
method: "/Hello/Test",
}
// Test Context method
ctx := stream.Context()
assert.Equal(t, gofrCtx, ctx, "Context should match GoFr context")
})
t.Run("InstrumentedStreamSendMsg", func(t *testing.T) {
// Create a mock server stream
mockStream := &mockServerStream{}
// Create instrumented stream
gofrCtx := createTestContext()
stream := &instrumentedStream{
ServerStream: mockStream,
ctx: gofrCtx,
method: "/Hello/Test",
}
// Test SendMsg method
msg := &HelloResponse{Message: "test"}
err := stream.SendMsg(msg)
assert.NoError(t, err, "SendMsg should not fail")
assert.True(t, mockStream.sendMsgCalled, "SendMsg should be called on underlying stream")
})
t.Run("InstrumentedStreamRecvMsg", func(t *testing.T) {
// Create a mock server stream
mockStream := &mockServerStream{}
// Create instrumented stream
gofrCtx := createTestContext()
stream := &instrumentedStream{
ServerStream: mockStream,
ctx: gofrCtx,
method: "/Hello/Test",
}
// Test RecvMsg method
msg := &HelloRequest{}
err := stream.RecvMsg(msg)
assert.NoError(t, err, "RecvMsg should not fail")
assert.True(t, mockStream.recvMsgCalled, "RecvMsg should be called on underlying stream")
})
}
// Mock implementations for testing
type mockHelloServer struct{}
func (m *mockHelloServer) SayHello(ctx *gofr.Context) (any, error) {
req := &HelloRequest{}
err := ctx.Bind(req)
if err != nil {
return nil, err
}
return &HelloResponse{Message: "Hello " + req.Name + "!"}, nil
}
type mockHelloServerWithError struct{}
func (m *mockHelloServerWithError) SayHello(ctx *gofr.Context) (any, error) {
return nil, fmt.Errorf("test error")
}
type mockHelloServerWrongType struct{}
func (m *mockHelloServerWrongType) SayHello(ctx *gofr.Context) (any, error) {
return "wrong type", nil
}
type mockServerStream struct {
sendMsgCalled bool
recvMsgCalled bool
}
func (m *mockServerStream) SendMsg(msg interface{}) error {
m.sendMsgCalled = true
return nil
}
func (m *mockServerStream) RecvMsg(msg interface{}) error {
m.recvMsgCalled = true
return nil
}
func (m *mockServerStream) SetHeader(metadata.MD) error {
return nil
}
func (m *mockServerStream) SendHeader(metadata.MD) error {
return nil
}
func (m *mockServerStream) SetTrailer(metadata.MD) {
}
func (m *mockServerStream) Context() context.Context {
return context.Background()
}
================================================
FILE: examples/grpc/grpc-unary-server/server/request_gofr.go
================================================
// Code generated by gofr.dev/cli/gofr. DO NOT EDIT.
// versions:
// gofr-cli v0.7.0
// gofr.dev v1.39.0
// source: hello.proto
package server
import (
"context"
"fmt"
"reflect"
)
// Request Wrappers
type HelloRequestWrapper struct {
ctx context.Context
*HelloRequest
}
func (h *HelloRequestWrapper) Context() context.Context {
return h.ctx
}
func (h *HelloRequestWrapper) Param(s string) string {
return ""
}
func (h *HelloRequestWrapper) PathParam(s string) string {
return ""
}
func (h *HelloRequestWrapper) Bind(p interface{}) error {
ptr := reflect.ValueOf(p)
if ptr.Kind() != reflect.Ptr {
return fmt.Errorf("expected a pointer, got %T", p)
}
hValue := reflect.ValueOf(h.HelloRequest).Elem()
ptrValue := ptr.Elem()
for i := 0; i < hValue.NumField(); i++ {
field := hValue.Type().Field(i)
if field.Name == "state" || field.Name == "sizeCache" || field.Name == "unknownFields" {
continue
}
if field.IsExported() {
ptrValue.Field(i).Set(hValue.Field(i))
}
}
return nil
}
func (h *HelloRequestWrapper) HostName() string {
return ""
}
func (h *HelloRequestWrapper) Params(s string) []string {
return nil
}
================================================
FILE: examples/grpc/grpc-unary-server/server/request_test.go
================================================
package server
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestHelloRequestWrapper_Context(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("Context", func(t *testing.T) {
// Create request wrapper
ctx := context.Background()
req := &HelloRequestWrapper{
ctx: ctx,
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Context method
returnedCtx := req.Context()
assert.Equal(t, ctx, returnedCtx, "Context should match")
})
}
func TestHelloRequestWrapper_Param(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("Param", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Param method (should return empty string)
param := req.Param("name")
assert.Equal(t, "", param, "Param should return empty string")
})
}
func TestHelloRequestWrapper_PathParam(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("PathParam", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test PathParam method (should return empty string)
param := req.PathParam("name")
assert.Equal(t, "", param, "PathParam should return empty string")
})
}
func TestHelloRequestWrapper_HostName(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("HostName", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test HostName method (should return empty string)
hostname := req.HostName()
assert.Equal(t, "", hostname, "HostName should return empty string")
})
}
func TestHelloRequestWrapper_Params(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("Params", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Params method (should return nil)
params := req.Params("name")
assert.Nil(t, params, "Params should return nil")
})
}
func TestHelloRequestWrapper_Bind(t *testing.T) {
_ = testutil.NewServerConfigs(t)
t.Run("BindSuccess", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Bind method with valid pointer
var target HelloRequest
err := req.Bind(&target)
require.NoError(t, err, "Bind should not fail")
assert.Equal(t, "test", target.Name, "Name should be bound correctly")
})
t.Run("BindWithNonPointer", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Bind method with non-pointer (should fail)
var target HelloRequest
err := req.Bind(target)
assert.Error(t, err, "Bind should fail with non-pointer")
assert.Contains(t, err.Error(), "expected a pointer", "Error message should indicate pointer expected")
})
t.Run("BindWithNilPointer", func(t *testing.T) {
// Create request wrapper
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: "test"},
}
// Test Bind method with nil pointer (should fail)
err := req.Bind(nil)
assert.Error(t, err, "Bind should fail with nil pointer")
assert.Contains(t, err.Error(), "expected a pointer", "Error message should indicate pointer expected")
})
t.Run("BindWithEmptyRequest", func(t *testing.T) {
// Create request wrapper with empty request
req := &HelloRequestWrapper{
HelloRequest: &HelloRequest{Name: ""},
}
// Test Bind method with empty request
var target HelloRequest
err := req.Bind(&target)
require.NoError(t, err, "Bind should not fail with empty request")
assert.Equal(t, "", target.Name, "Name should be empty")
})
}
================================================
FILE: examples/http-server/Dockerfile
================================================
# Build stage
FROM golang:1.25-alpine AS build
RUN apk add --no-cache build-base
WORKDIR /src
# Copy go.mod + go.sum first for better caching
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source code
COPY . .
# Move into http-server folder and build the package (not main.go)
WORKDIR /src/examples/http-server
RUN CGO_ENABLED=0 go build -a -o /app/main .
# Final stage
FROM alpine:3.14
RUN apk add --no-cache tzdata ca-certificates
COPY --from=build /app/main /main
COPY --from=build /src/examples/http-server/configs /configs
EXPOSE 9000
CMD ["/main"]
================================================
FILE: examples/http-server/README.md
================================================
# HTTP Server Example
This GoFr example demonstrates a simple HTTP server which supports Redis and MySQL as datasources.
### To run the example, follow the steps below:
#### 1. Run with Docker Compose (recommended)
From the project root (`/gofr`):
```console
docker compose -f examples/http-server/docker/docker-compose.yml up -d
```
* Explanation:
* `-f examples/http-server/docker/docker-compose.yml` → path to the docker-compose file
* `up -d` → builds (if needed) and runs services in detached mode
---
#### 2. Build & Run Manually (without docker-compose)
##### Build the Docker image
From the project root (`/gofr`):
```console
docker build -f examples/http-server/Dockerfile -t http-server:latest .
```
* Explanation:
* `-f examples/http-server/Dockerfile` → path to the Dockerfile
* `-t http-server:latest` → tag for the Docker image
* `.` → build context (project root; needed for `go.mod` and `go.sum`)
##### Run the Docker container
```console
docker run -p 9000:9000 --name http-server http-server:latest
```
* Explanation:
* `-p 9000:9000` → maps container port 9000 to host port 9000
* `--name http-server` → optional, gives your container a name
* Use **Compose** when you want the whole stack (app + Redis + MySQL + Grafana + Prometheus).
* Use **Docker build/run** when you just want to run the app container alone.
To test the example, follow these steps:
1. Open your browser and navigate to `http://localhost:9000/hello`.
2. To view the GoFr trace, open `https://tracer.gofr.dev` and paste the traceid.
3. To access the Grafana Dashboard, open `http://localhost:3000`. The dashboard UI will be displayed. Use the default admin credentials to log in:
- Username: `admin`
- Password: `password`
================================================
FILE: examples/http-server/docker/docker-compose.yaml
================================================
version: '3.8'
services:
gofr-http-server:
build:
context: ../../../ # project root (so go.mod and go.sum are included)
dockerfile: examples/http-server/Dockerfile
environment:
TRACE_EXPORTER: gofr
TRACER_RATIO: 0.1
REDIS_HOST: redisdb
REDIS_PORT: 2002
DB_HOST: mysqldb
DB_USER: root
DB_PASSWORD: password
DB_NAME: test
DB_PORT: 2001
DB_DIALECT: mysql
ports:
- "9000:9000"
- "2121:2121"
depends_on:
- redisdb
- mysqldb
- grafana
- prometheus
networks:
- gofr-network
redisdb:
image: redis:7.0.5
ports:
- "2002:6379"
networks:
- gofr-network
mysqldb:
image: mysql:8.0.30
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test
ports:
- "2001:3306"
networks:
- gofr-network
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: password
volumes:
- ./provisioning:/etc/grafana/provisioning
networks:
- gofr-network
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus:/etc/prometheus
networks:
- gofr-network
networks:
gofr-network:
================================================
FILE: examples/http-server/docker/prometheus/prometheus.yml
================================================
global:
scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
# scrape_timeout is set to the global default (10s).
scrape_configs:
- job_name: 'prometheus'
scrape_interval: 5s
metrics_path: '/metrics'
static_configs:
- targets: ['host.docker.internal:2121']
================================================
FILE: examples/http-server/docker/provisioning/dashboards/dashboards.yaml
================================================
apiVersion: 1
providers:
- name: 'Gofr Dashboard'
orgId: 1
folder: ''
type: file
disableDeletion: false
updateIntervalSeconds: 10
options:
path: /etc/grafana/provisioning/dashboards/gofr-dashboard
================================================
FILE: examples/http-server/docker/provisioning/dashboards/gofr-dashboard/dashboards.json
================================================
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"description": "Gofr Dashboard offers real-time insights into our system's performance, displaying key metrics like response times and error rates. Tailored for DevOps and engineering teams, it pulls data from Prometheus and Kubernetes. Updated regularly, we encourage your feedback to make it even more valuable for your needs.",
"editable": true,
"fiscalYearStartMonth": 0,
"gnetId": 19905,
"graphTooltip": 0,
"id": 3,
"links": [],
"liveNow": false,
"panels": [
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 90,
"panels": [],
"title": "App Information",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 7,
"x": 0,
"y": 1
},
"id": 87,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [],
"fields": "/.*/",
"values": false
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "app_info{app_name='$Service'}",
"format": "table",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "App Version",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"app_version"
]
}
}
}
],
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 8,
"x": 7,
"y": 1
},
"id": 88,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [],
"fields": "/.*/",
"values": false
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "app_info{app_name='$Service'}",
"format": "table",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Framework Version",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"framework_version"
]
}
}
}
],
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 9,
"x": 15,
"y": 1
},
"id": 93,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [],
"fields": "/.*/",
"values": false
},
"showPercentChange": false,
"textMode": "value",
"wideLayout": true
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "go_info{}",
"format": "table",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Go Version",
"transformations": [
{
"id": "filterFieldsByName",
"options": {
"include": {
"names": [
"version"
],
"pattern": "version"
}
}
}
],
"type": "stat"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 5
},
"id": 14,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "System Information",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "# Of GoRoutines",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "none",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"id": 6,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (pod) (app_go_routines{})",
"format": "time_series",
"hide": false,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(app_go_routines{})",
"hide": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Go Routines",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Memory (bytes)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "decbytes",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"id": 16,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (pod) (app_sys_memory_alloc{})",
"format": "time_series",
"hide": false,
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(app_sys_memory_alloc{})",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Memory Utilization",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "$${DataSource}"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 14
},
"id": 8,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "$${DataSource}"
},
"refId": "A"
}
],
"title": "Inbound Requests",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 15
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum(rate(app_http_response_bucket{}[$__rate_interval])) by (le))",
"legendFormat": " 90",
"range": true,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(app_http_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "95",
"range": true,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(app_http_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "99",
"range": true,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum(rate(app_http_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "99.9",
"range": true,
"refId": "99.9"
}
],
"title": "Response Time SLA",
"transformations": [
{
"id": "organize",
"options": {}
}
],
"type": "timeseries"
},
{
"cards": {},
"color": {
"cardColor": "#96D98D",
"colorScale": "sqrt",
"colorScheme": "interpolateGreens",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 15
},
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"id": 2,
"legend": {
"show": false
},
"options": {
"calculate": false,
"calculation": {},
"cellGap": 2,
"cellValues": {},
"color": {
"exponent": 0.5,
"fill": "#96D98D",
"mode": "opacity",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 128
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-09
},
"legend": {
"show": false
},
"rowsFrame": {
"layout": "auto"
},
"showValue": "never",
"tooltip": {
"mode": "single",
"showColorScale": false,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false,
"unit": "s",
"max": 0.1
}
},
"pluginVersion": "10.3.3",
"reverseYBuckets": false,
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum(increase(app_http_response_bucket{le!~\"10|30|\\\\+Inf\"}[$__rate_interval])) by (le)",
"format": "heatmap",
"instant": false,
"legendFormat": "{{le}}",
"range": true,
"refId": "A"
}
],
"title": "Request Latency Distribution",
"tooltip": {
"show": true,
"showHistogram": false
},
"type": "heatmap",
"xAxis": {
"show": true
},
"yAxis": {
"format": "s",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "No of requests",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [
{
"targetBlank": true,
"title": "Path level service dashboard",
"url": "/d/InboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__field.labels.path}&var-method=$${__field.labels.method}&from=$${__from}&to=$${__to}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 23
},
"id": 126,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (path, method) (\n increase(app_http_response_count{}[$__rate_interval])\n)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_http_response_count{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Request Count Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Response Count ",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 23
},
"id": 12,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (status) (\n increase(app_http_response_count{}[$__rate_interval])\n)",
"legendFormat": "{{status}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_http_response_count{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Response Code Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"targetBlank": true,
"title": "Path Level Dashboard",
"url": "/d/InboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__data.fields.Path}&var-method=$${__data.fields.Method}&from=$${__from}&to=$${__to} "
}
],
"mappings": [
{
"options": {
"NaN": {
"index": 0,
"text": "N/A"
}
},
"type": "value"
}
],
"noValue": "N/A",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "purple",
"value": 80
}
]
},
"unit": "ms",
"unitScale": true
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Count"
},
"properties": [
{
"id": "unit",
"value": "none"
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 31
},
"id": 74,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "p99.9"
}
]
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.999, sum(rate(app_http_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "99.9"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.95, sum(rate(app_http_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.99, sum(rate(app_http_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.90, sum(rate(app_http_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum(increase(app_http_response_count{}[$__range])) by (le, path,method)",
"format": "table",
"hide": true,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "A"
}
],
"title": "Route Level SLA",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"status": true
},
"indexByName": {
"Time": 0,
"Value": 7,
"Value #90": 3,
"Value #95": 4,
"Value #99": 5,
"Value #99.9": 6,
"method": 1,
"path": 2
},
"renameByName": {
"Time": "",
"Value": "Count",
"Value #2xx": "Response Code 2xx",
"Value #400": "Response Code 400",
"Value #401": "Response Code 401",
"Value #404": "Response Code 404",
"Value #50": "50%(ms)",
"Value #5xx": "Response Code 5xx",
"Value #90": "p90",
"Value #95": "p95",
"Value #99": "p99",
"Value #99.9": "p99.9",
"Value #A": "Count",
"Value #Total": "Total Count",
"method": "Method",
"path": "Path",
"status": "Status"
}
}
}
],
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"targetBlank": true,
"title": "Path Level Dashboard",
"url": "/d/InboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__data.fields.Path}&var-method=$${__data.fields.Method}&from=$${__from}&to=$${__to} "
}
],
"mappings": [],
"noValue": "N/A",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "purple",
"value": 80
}
]
},
"unitScale": true
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Path"
},
"properties": [
{
"id": "custom.width",
"value": 258
}
]
}
]
},
"gridPos": {
"h": 9,
"w": 24,
"x": 0,
"y": 40
},
"id": 75,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "Method"
}
]
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=\"400\"}[$__range]))",
"format": "table",
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "400"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=\"401\"}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "401"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=\"403\"}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "403"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=\"404\"}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "404"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=~\"5.+\"}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "5xx"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{status=~\"2.+\"}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "2xx"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_response_count{}[$__range]))",
"format": "table",
"hide": true,
"instant": true,
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "Total"
}
],
"title": "Route Level Response Code",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"status": true
},
"indexByName": {
"Time": 0,
"Value #2xx": 3,
"Value #400": 4,
"Value #401": 5,
"Value #403": 6,
"Value #404": 7,
"Value #5xx": 8,
"Value #Total": 9,
"method": 1,
"path": 2
},
"renameByName": {
"Time": "",
"Value #2xx": "2xx",
"Value #400": "400",
"Value #401": "401",
"Value #403": "403",
"Value #404": "404",
"Value #50": "50%(ms)",
"Value #5xx": "5xx",
"Value #90": "90%(ms)",
"Value #95": "95%(ms)",
"Value #99": "99%(ms)",
"Value #Total": "Total",
"method": "Method",
"path": "Path",
"status": "Status"
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 49
},
"id": 20,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "Outbound Requests",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "Outbound request level 99%ile response time",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [
{
"targetBlank": true,
"title": "Path level drill down ",
"url": "/d/OutboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__field.labels.path}&var-method=$${__field.labels.method}&from=$${__from}&to=$${__to}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 50
},
"id": 28,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum(rate(app_http_service_response_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "90",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(app_http_service_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "95",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(app_http_service_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "99",
"range": true,
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum(rate(app_http_service_response_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "99.9",
"range": true,
"refId": "D"
}
],
"title": "Response Time SLA",
"type": "timeseries"
},
{
"cards": {},
"color": {
"cardColor": "#96D98D",
"colorScale": "sqrt",
"colorScheme": "interpolateGreens",
"exponent": 0.5,
"mode": "opacity"
},
"dataFormat": "tsbuckets",
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"scaleDistribution": {
"type": "linear"
}
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 50
},
"heatmap": {},
"hideZeroBuckets": false,
"highlightCards": true,
"id": 94,
"legend": {
"show": false
},
"options": {
"calculate": false,
"calculation": {},
"cellGap": 2,
"cellValues": {},
"color": {
"exponent": 0.5,
"fill": "#96D98D",
"mode": "opacity",
"reverse": false,
"scale": "exponential",
"scheme": "Oranges",
"steps": 128
},
"exemplars": {
"color": "rgba(255,0,255,0.7)"
},
"filterValues": {
"le": 1e-09
},
"legend": {
"show": false
},
"rowsFrame": {
"layout": "auto"
},
"showValue": "never",
"tooltip": {
"mode": "single",
"showColorScale": false,
"yHistogram": false
},
"yAxis": {
"axisPlacement": "left",
"reverse": false,
"unit": "s",
"max": 0.1
}
},
"pluginVersion": "10.3.3",
"reverseYBuckets": false,
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_http_service_response_bucket{le!~\"10|30|\\\\+Inf\"}[$__rate_interval])) by (le)",
"format": "heatmap",
"legendFormat": "__auto",
"range": true,
"refId": "A"
}
],
"title": "Request Latency Distribution",
"tooltip": {
"show": true,
"showHistogram": false
},
"type": "heatmap",
"xAxis": {
"show": true
},
"yAxis": {
"format": "s",
"logBase": 1,
"show": true
},
"yBucketBound": "auto"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "No. of requests",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [
{
"targetBlank": true,
"title": "Path level Dashboard",
"url": "/d/OutboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__field.labels.path}&var-method=$${__field.labels.method}&from=$${__from}&to=$${__to}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 58
},
"id": 26,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (path, method) (\n increase(app_http_service_response_count{}[$__rate_interval])\n)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_http_service_response_count{}[$__rate_interval]))",
"hide": true,
"legendFormat": "__auto",
"range": true,
"refId": "B"
}
],
"title": "Request Count over time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Request Count ",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"Value"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": false,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 58
},
"id": 22,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (status) (\n increase(app_http_service_response_count{}[$__rate_interval])\n)",
"legendFormat": "{{status}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_http_service_response_count{}[$__rate_interval]))",
"hide": true,
"legendFormat": "sum",
"range": true,
"refId": "B"
}
],
"title": "Response Code Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"targetBlank": true,
"title": "Path Level Dashboard",
"url": "/d/OutboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__data.fields.Path}&var-method=$${__data.fields.Method}&from=$${__from}&to=$${__to}"
}
],
"mappings": [
{
"options": {
"NaN": {
"index": 0,
"text": "N/A"
}
},
"type": "value"
}
],
"noValue": "N/A",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "orange"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms",
"unitScale": true
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Path"
},
"properties": [
{
"id": "custom.width",
"value": 342
}
]
},
{
"matcher": {
"id": "byName",
"options": "Count"
},
"properties": [
{
"id": "unit",
"value": "none"
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 66
},
"id": 77,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.99, sum(rate(app_http_service_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"instant": true,
"interval": "",
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.95, sum(rate(app_http_service_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.90, sum(rate(app_http_service_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.999, sum(rate(app_http_service_response_bucket{}[$__range])) by (le, path,method)) * 1000",
"format": "table",
"hide": false,
"instant": true,
"interval": "",
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "99.9"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by (service,method,path) (increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": true,
"instant": true,
"interval": "",
"legendFormat": "{{path}} {{method}}",
"range": false,
"refId": "A"
}
],
"title": "Route Level SLA",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"le": true,
"status": true
},
"indexByName": {
"Time": 0,
"Value #90": 3,
"Value #95": 4,
"Value #99": 5,
"Value #99.9": 6,
"method": 1,
"path": 2
},
"renameByName": {
"Value #2xx": "Response Code 2xx",
"Value #400": "Response Code 400",
"Value #404": "Response Code 404",
"Value #50": "50%(ms)",
"Value #90": "p90",
"Value #95": "p95",
"Value #99": "p99",
"Value #99.9": "p99.9",
"Value #A": "Count",
"Value #total": "Total Count",
"method": "Method",
"path": "Path",
"status": "Status",
"{status=\"400\"}": "400"
}
}
}
],
"type": "table"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "left",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"targetBlank": true,
"title": "Path Level Dashboard",
"url": "/d/OutboundCallsPathLevelDrillDownDashboard/path-level-drill-down-dashboard?orgId=1&var-DataSource=$${DataSource}&var-namespace=$${namespace}&var-service=$${service}&var-path=$${__data.fields.Path}&var-method=$${__data.fields.Method}&from=$${__from}&to=$${__to}"
}
],
"mappings": [],
"noValue": "N/A",
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "orange"
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 74
},
"id": 78,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "Path"
}
]
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"instant": true,
"legendFormat": "Verbose",
"range": false,
"refId": "400"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "401"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "404"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "5xx"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "2xx"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method)(increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "403"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(path,method) (increase(app_http_service_response_count{}[$__range]))",
"format": "table",
"hide": true,
"instant": true,
"legendFormat": "",
"range": false,
"refId": "Total"
}
],
"title": "Route Level Response Code",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"status": true
},
"indexByName": {
"Time": 0,
"Value #2xx": 3,
"Value #400": 4,
"Value #401": 5,
"Value #403": 6,
"Value #404": 7,
"Value #5xx": 8,
"Value #A": 9,
"method": 1,
"path": 2
},
"renameByName": {
"Value #2xx": "2xx",
"Value #400": "400",
"Value #401": "401",
"Value #403": "403",
"Value #404": "404",
"Value #50": "50%(ms)",
"Value #5xx": "5xx",
"Value #90": "90%(ms)",
"Value #95": "95%(ms)",
"Value #99": "99%(ms)",
"Value #A": "Total",
"Value #total": "Total",
"method": "Method",
"path": "Path",
"status": "Status",
"{status=\"400\"}": "400"
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 82
},
"id": 100,
"panels": [],
"title": "Resilience (Outbound)",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Retry Count",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"lineWidth": 1,
"pointSize": 5,
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 8,
"x": 0,
"y": 83
},
"id": 101,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (service) (increase(app_http_retry_count{}[$__rate_interval]))",
"legendFormat": "{{service}}",
"range": true,
"refId": "A"
}
],
"title": "HTTP Service Retries",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "State",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "stepAfter",
"lineWidth": 2,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"decimals": 0,
"mappings": [
{
"options": {
"0": {
"color": "green",
"text": "Closed"
},
"1": {
"color": "red",
"text": "Open"
}
},
"type": "value"
}
],
"max": 1.2,
"min": -0.2,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 0.5
}
]
},
"unit": "none"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 16,
"x": 8,
"y": 83
},
"id": 103,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "app_http_circuit_breaker_state",
"legendFormat": "{{service}}",
"range": true,
"refId": "A"
}
],
"title": "Circuit Breaker Status over Time",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 90
},
"id": 30,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "SQL Database",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 91
},
"id": 34,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum(rate(app_sql_stats_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "p90",
"range": true,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(app_sql_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p95",
"range": true,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(app_sql_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p99",
"range": true,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum(rate(app_sql_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p99.9",
"range": true,
"refId": "99.9"
}
],
"title": "Response Time SLA",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "No. of query ",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 91
},
"id": 32,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (type) (rate(app_sql_stats_count{}[$__rate_interval]))",
"legendFormat": "{{type}} - {{database}}",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(rate(app_sql_stats_count{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Query Count over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "smooth",
"lineStyle": {
"fill": "solid"
},
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 100
},
"id": 58,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "app_sql_inUse_connections{}",
"hide": false,
"legendFormat": "InUse",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "app_sql_open_connections{}",
"hide": false,
"legendFormat": "Open",
"range": true,
"refId": "C"
}
],
"title": "Connection Count",
"transformations": [],
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [
{
"options": {
"NaN": {
"index": 0,
"text": "N/A"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "ms",
"unitScale": true
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Count"
},
"properties": [
{
"id": "unit",
"value": "none"
}
]
}
]
},
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 108
},
"id": 95,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": []
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.90, sum(rate(app_sql_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.95, sum(rate(app_sql_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.99, sum(rate(app_sql_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.999, sum(rate(app_sql_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "99.9"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(type)(increase(app_sql_stats_count{}[$__range]))",
"format": "table",
"hide": true,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "A"
}
],
"title": "Query Type Level SLA",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"indexByName": {},
"renameByName": {
"Value #90": "p90",
"Value #95": "p95",
"Value #99": "p99",
"Value #99.9": "p99.9",
"Value #A": "Total",
"Value #B": "p90",
"Value #C": "p99",
"Value #D": "p99.9",
"Value #E": "Count",
"type": "Type"
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 114
},
"id": 42,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "Redis",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 115
},
"id": 36,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum(rate(app_redis_stats_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "p90",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(app_redis_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p95",
"range": true,
"refId": "B"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(app_redis_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p99",
"range": true,
"refId": "C"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum(rate(app_redis_stats_bucket{}[$__rate_interval])) by (le))",
"hide": false,
"legendFormat": "p99.9",
"range": true,
"refId": "D"
}
],
"title": "Response Time SLA",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "No. of query ",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 115
},
"id": 46,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (type) (rate(app_redis_stats_bucket{}[$__rate_interval]))",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum (rate(app_redis_stats_bucket{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Query Count over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Count"
},
"properties": [
{
"id": "unit",
"value": "none"
}
]
}
]
},
"gridPos": {
"h": 5,
"w": 24,
"x": 0,
"y": 123
},
"id": 107,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"sortBy": [
{
"desc": false,
"displayName": "Type"
}
]
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.90, sum(rate(app_redis_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.95, sum(rate(app_redis_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.99, sum(rate(app_redis_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "histogram_quantile(0.999, sum(rate(app_redis_stats_bucket{}[$__range])) by (le,type))",
"format": "table",
"hide": false,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "99.9"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "sum by(type)(increase(app_redis_stats_count{}[$__range]))",
"format": "table",
"hide": true,
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "A"
}
],
"title": "Query Type Level SLA",
"transformations": [
{
"id": "merge",
"options": {}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true
},
"indexByName": {},
"renameByName": {
"Value #90": "p90",
"Value #95": "p95",
"Value #99": "p99",
"Value #99.9": "p99.9",
"Value #A": "Total",
"Value #B": "p90",
"Value #C": "p99",
"Value #D": "p99.9",
"Value #E": "Count",
"type": "Type"
}
}
}
],
"type": "table"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 128
},
"id": 60,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "PubSub Consumer",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 4,
"w": 24,
"x": 0,
"y": 129
},
"id": 114,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [],
"fields": "/.*/",
"values": false
},
"showPercentChange": false,
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "app_pubsub_subscribe_total_count{}",
"format": "table",
"instant": true,
"range": false,
"refId": "A"
}
],
"title": "Topics",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Value": true,
"__name__": true,
"consumerGroup": true,
"container": true,
"endpoint": true,
"instance": true,
"job": true,
"namespace": true,
"otel_scope_name": true,
"otel_scope_version": true,
"pod": true,
"prometheus": true,
"prometheus_replica": true,
"service": true
},
"includeByName": {},
"indexByName": {
"Time": 1,
"Value": 13,
"__name__": 2,
"consumerGroup": 3,
"container": 4,
"endpoint": 5,
"instance": 6,
"job": 7,
"namespace": 8,
"pod": 9,
"prometheus": 10,
"prometheus_replica": 11,
"service": 12,
"topic": 0
},
"renameByName": {}
}
}
],
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 133
},
"id": 62,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (topic) (increase(app_pubsub_subscribe_total_count{}[$__rate_interval]))",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum (increase(app_pubsub_subscribe_total_count{}[$__rate_interval]))",
"hide": false,
"legendFormat": "__auto",
"range": true,
"refId": "B"
}
],
"title": "Consumption Over Time (Receive)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 143
},
"id": 120,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (topic) (increase(app_pubsub_subscribe_success_count[$__rate_interval]))",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum (increase(app_pubsub_subscribe_success_count{}[$__rate_interval]))",
"hide": false,
"legendFormat": "__auto",
"range": true,
"refId": "B"
}
],
"title": "Consumption Over Time (Success)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 143
},
"id": 121,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "clamp_min(sum by (topic) (\n increase(app_pubsub_subscribe_total_count{}[$__rate_interval])\n)\n-\nsum by (topic) (\n increase(app_pubsub_subscribe_success_count{}[$__rate_interval])\n), 0)",
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "clamp_min(sum (\n increase(app_pubsub_subscribe_total_count{}[$__rate_interval])\n)\n-\nsum (\n increase(app_pubsub_subscribe_success_count{}[$__rate_interval])\n), 0)",
"hide": false,
"legendFormat": "Total Failure",
"range": true,
"refId": "B"
}
],
"title": "Consumption Over Time (Failure)",
"type": "timeseries"
},
{
"collapsed": false,
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 152
},
"id": 48,
"panels": [],
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"refId": "A"
}
],
"title": "PubSub Publisher",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 153
},
"id": 118,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "/^topic$/",
"values": true
},
"showPercentChange": false,
"text": {},
"textMode": "auto",
"wideLayout": true
},
"pluginVersion": "10.3.3",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": false,
"expr": "app_pubsub_publish_total_count{}",
"format": "table",
"instant": true,
"legendFormat": "__auto",
"range": false,
"refId": "A"
}
],
"title": "Topics",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Value": true,
"__name__": true,
"consumerGroup": true,
"container": true,
"endpoint": true,
"instance": true,
"job": true,
"namespace": true,
"pod": true,
"prometheus": true,
"prometheus_replica": true,
"service": true
},
"indexByName": {
"Time": 1,
"Value": 13,
"__name__": 2,
"consumerGroup": 3,
"container": 4,
"endpoint": 5,
"instance": 6,
"job": 7,
"namespace": 8,
"pod": 9,
"prometheus": 10,
"prometheus_replica": 11,
"service": 12,
"topic": 0
},
"renameByName": {}
}
}
],
"type": "stat"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 161
},
"id": 64,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (topic) (increase(app_pubsub_publish_total_count{}[$__rate_interval]))",
"instant": false,
"legendFormat": "{{topic}}({{consumerGroup}})",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum (increase(app_pubsub_publish_total_count{}[$__rate_interval]))",
"hide": false,
"instant": false,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Publish Over Time (Total)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 171
},
"id": 122,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum by (topic) (\n increase(app_pubsub_publish_success_count{}[$__rate_interval])\n)",
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "sum(\n increase(app_pubsub_publish_success_count{}[$__rate_interval])\n)",
"hide": false,
"instant": false,
"legendFormat": "Total Success",
"range": true,
"refId": "B"
}
],
"title": "Publish Over Time (Success)",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": [
{
"__systemRef": "hideSeriesFrom",
"matcher": {
"id": "byNames",
"options": {
"mode": "exclude",
"names": [
"{consumerGroup=\"gofr-consumerGroup\", topic=\"tn-subscription\"}"
],
"prefix": "All except:",
"readOnly": true
}
},
"properties": [
{
"id": "custom.hideFrom",
"value": {
"legend": false,
"tooltip": false,
"viz": true
}
}
]
}
]
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 171
},
"id": 123,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "clamp_min(sum by (topic) (\n increase(app_pubsub_publish_total_count{}[$__rate_interval])\n)\n-\nsum by (topic) (\n increase(app_pubsub_publish_success_count{}[$__rate_interval])\n), 0)",
"instant": false,
"legendFormat": "__auto",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"exemplar": true,
"expr": "clamp_min(sum (\n increase(app_pubsub_publish_total_count{}[$__rate_interval])\n)\n-\nsum (\n increase(app_pubsub_publish_success_count{}[$__rate_interval])\n), 0)",
"hide": false,
"instant": false,
"legendFormat": "Total Failure",
"range": true,
"refId": "B"
}
],
"title": "Publish Over Time (Failure)",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 180
},
"id": 130,
"panels": [],
"title": "GraphQL",
"type": "row"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 181
},
"id": 131,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum(rate(app_graphql_request_duration_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "90",
"range": true,
"refId": "90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum(rate(app_graphql_request_duration_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "95",
"range": true,
"refId": "95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum(rate(app_graphql_request_duration_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "99",
"range": true,
"refId": "99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum(rate(app_graphql_request_duration_bucket{}[$__rate_interval])) by (le))",
"legendFormat": "99.9",
"range": true,
"refId": "99.9"
}
],
"title": "Response Time SLA",
"transformations": [
{
"id": "organize",
"options": {}
}
],
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "No of requests",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 189
},
"id": 133,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (operation_name, type) (\n increase(app_graphql_operations_total{}[$__rate_interval])\n)",
"legendFormat": "{{operation_name}} ({{type}})",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_graphql_operations_total{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Request Count Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Error Count",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 189
},
"id": 134,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum by (operation_name, type) (\n increase(app_graphql_error_total{}[$__rate_interval])\n)",
"legendFormat": "{{operation_name}} ({{type}})",
"range": true,
"refId": "A"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "sum(increase(app_graphql_error_total{}[$__rate_interval]))",
"hide": true,
"legendFormat": "Total",
"range": true,
"refId": "B"
}
],
"title": "Error Count Over Time",
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 197
},
"id": 135,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum by (le, operation_name, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{operation_name}} ({{type}}) p90",
"range": true,
"refId": "p90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum by (le, operation_name, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{operation_name}} ({{type}}) p95",
"range": true,
"refId": "p95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum by (le, operation_name, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{operation_name}} ({{type}}) p99",
"range": true,
"refId": "p99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum by (le, operation_name, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{operation_name}} ({{type}}) p99.9",
"range": true,
"refId": "p99.9"
}
],
"title": "Latency by Operation",
"transformations": [
{
"id": "organize",
"options": {}
}
],
"type": "timeseries"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"description": "",
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "Time (sec)",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "s",
"unitScale": true
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 197
},
"id": 136,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "multi",
"sort": "none"
}
},
"pluginVersion": "9.2.4",
"targets": [
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.90, sum by (le, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{type}} p90",
"range": true,
"refId": "p90"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.95, sum by (le, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{type}} p95",
"range": true,
"refId": "p95"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.99, sum by (le, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{type}} p99",
"range": true,
"refId": "p99"
},
{
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"editorMode": "code",
"expr": "histogram_quantile(0.999, sum by (le, type) (rate(app_graphql_request_duration_bucket{}[$__rate_interval])))",
"legendFormat": "{{type}} p99.9",
"range": true,
"refId": "p99.9"
}
],
"title": "Latency by Operation Type",
"transformations": [
{
"id": "organize",
"options": {}
}
],
"type": "timeseries"
}
],
"refresh": "5s",
"schemaVersion": 39,
"tags": [
"Observability Common"
],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "prometheus",
"value": "f7d51eca-7c3e-4b91-81a6-00d485fddaba"
},
"hide": 0,
"includeAll": false,
"label": "Data Source",
"multi": false,
"name": "DataSource",
"options": [],
"query": "prometheus",
"queryValue": "",
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"type": "datasource"
},
{
"current": {
"selected": true,
"text": "order-service",
"value": "order-service"
},
"datasource": {
"type": "prometheus",
"uid": "${DataSource}"
},
"definition": "label_values(app_name)",
"hide": 0,
"includeAll": false,
"label": "",
"multi": false,
"name": "Service",
"options": [],
"query": {
"qryType": 1,
"query": "label_values(app_name)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
},
"time": {
"from": "now-30m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "GoFr - Application Services Monitoring",
"uid": "SMDAllServices123",
"version": 7,
"weekStart": ""
}
================================================
FILE: examples/http-server/docker/provisioning/datasources/datasource.yaml
================================================
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: true
editable: true
================================================
FILE: examples/http-server/main.go
================================================
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource"
)
func main() {
// Create a new application
a := gofr.New()
//HTTP service with default health check endpoint
a.AddHTTPService("anotherService", "http://localhost:9000")
// Add all the routes
a.GET("/hello", HelloHandler)
a.GET("/error", ErrorHandler)
a.GET("/redis", RedisHandler)
a.GET("/trace", TraceHandler)
a.GET("/mysql", MysqlHandler)
// Run the application
a.Run()
}
func HelloHandler(c *gofr.Context) (any, error) {
name := c.Param("name")
if name == "" {
c.Log("Name came empty")
name = "World"
}
return fmt.Sprintf("Hello %s!", name), nil
}
func ErrorHandler(c *gofr.Context) (any, error) {
return nil, errors.New("some error occurred")
}
func RedisHandler(c *gofr.Context) (any, error) {
val, err := c.Redis.Get(c, "test").Result()
if err != nil && err != redis.Nil { // If key is not found, we are not considering this an error and returning "".
return nil, datasource.ErrorDB{Err: err, Message: "error from redis db"}
}
return val, nil
}
func TraceHandler(c *gofr.Context) (any, error) {
defer c.Trace("traceHandler").End()
span2 := c.Trace("some-sample-work")
// Waiting for 1ms to simulate workload
<-time.After(time.Millisecond * 1) //nolint:wsl
defer span2.End()
// Ping redis 5 times concurrently and wait.
count := 5
wg := sync.WaitGroup{}
wg.Add(count)
for i := 0; i < count; i++ {
go func() {
c.Redis.Ping(c)
wg.Done()
}()
}
wg.Wait()
// Call to Another service
resp, err := c.GetHTTPService("anotherService").Get(c, "redis", nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var data = struct {
Data any `json:"data"`
}{}
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
}
return data.Data, nil
}
func MysqlHandler(c *gofr.Context) (any, error) {
var value int
err := c.SQL.QueryRowContext(c, "select 2+2").Scan(&value)
if err != nil {
return nil, datasource.ErrorDB{Err: err, Message: "error from sql db"}
}
return value, nil
}
================================================
FILE: examples/http-server/main_test.go
================================================
package main
// This test file demonstrates how to test handlers in GoFr.
//
// Key Concepts:
// 1. GoFr wraps http.Request using gofrHTTP.NewRequest(req)
// 2. Handlers receive gofr.Context which contains the wrapped request
// 3. Use mux.SetURLVars() to set path parameters for ctx.PathParam()
// 4. Each HTTP service registered with WithMockHTTPService gets its own separate mock instance
// 5. Expectations set on one service do NOT affect other services
// 6. Always use mocks.HTTPServices["serviceName"] when you have multiple services
//
// For detailed documentation, see:
// - https://gofr.dev/docs/references/testing (Official GoFr Testing Guide)
// - https://gofr.dev/docs/references/context (GoFr Context Documentation)
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
"time"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource/redis"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestIntegration_SimpleAPIServer(t *testing.T) {
httpPort := testutil.GetFreePort(t)
port := testutil.GetFreePort(t)
t.Setenv("HTTP_PORT", strconv.Itoa(httpPort))
t.Setenv("METRICS_PORT", strconv.Itoa(port))
host := fmt.Sprintf("http://localhost:%d", httpPort)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
path string
body any
}{
{"hello handler", "/hello", "Hello World!"},
{"hello handler with query parameter", "/hello?name=gofr", "Hello gofr!"},
{"redis handler", "/redis", ""},
{"mysql handler", "/mysql", float64(4)},
}
for i, tc := range tests {
req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil)
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
var data = struct {
Data any `json:"data"`
}{}
b, err := io.ReadAll(resp.Body)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
_ = json.Unmarshal(b, &data)
assert.Equal(t, tc.body, data.Data, "TEST[%d], Failed.\n%s", i, tc.desc)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, http.StatusOK, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
resp.Body.Close()
}
}
func TestIntegration_SimpleAPIServer_Errors(t *testing.T) {
httpPort := testutil.GetFreePort(t)
port := testutil.GetFreePort(t)
t.Setenv("HTTP_PORT", strconv.Itoa(httpPort))
t.Setenv("METRICS_PORT", strconv.Itoa(port))
host := fmt.Sprintf("http://localhost:%d", httpPort)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
path string
body any
statusCode int
}{
{
desc: "error handler called",
path: "/error",
statusCode: http.StatusInternalServerError,
body: map[string]any{"message": "some error occurred"},
},
{
desc: "empty route",
path: "/",
statusCode: http.StatusNotFound,
body: map[string]any{"message": "route not registered"},
},
{
desc: "route not registered with the server",
path: "/route",
statusCode: http.StatusNotFound,
body: map[string]any{"message": "route not registered"},
},
}
for i, tc := range tests {
req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil)
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
var data = struct {
Error any `json:"error"`
}{}
b, err := io.ReadAll(resp.Body)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
_ = json.Unmarshal(b, &data)
assert.Equal(t, tc.body, data.Error, "TEST[%d], Failed.\n%s", i, tc.desc)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
resp.Body.Close()
}
}
func TestIntegration_SimpleAPIServer_Health(t *testing.T) {
httpPort := testutil.GetFreePort(t)
port := testutil.GetFreePort(t)
t.Setenv("HTTP_PORT", strconv.Itoa(httpPort))
t.Setenv("METRICS_PORT", strconv.Itoa(port))
host := fmt.Sprintf("http://localhost:%d", httpPort)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
path string
statusCode int
}{
{"health handler", "/.well-known/health", http.StatusOK}, // Health check should be added by the framework.
{"favicon handler", "/favicon.ico", http.StatusOK}, // Favicon should be added by the framework.
}
for i, tc := range tests {
req, _ := http.NewRequest(http.MethodGet, host+tc.path, nil)
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestRedisHandler(t *testing.T) {
metricsPort := testutil.GetFreePort(t)
httpPort := testutil.GetFreePort(t)
t.Setenv("METRICS_PORT", strconv.Itoa(metricsPort))
t.Setenv("HTTP_PORT", strconv.Itoa(httpPort))
a := gofr.New()
logger := logging.NewLogger(logging.DEBUG)
redisClient, mock := redismock.NewClientMock()
rc := redis.NewClient(config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", "REDIS_PORT": "2001"}), logger, a.Metrics())
rc.Client = redisClient
mock.ExpectGet("test").SetErr(testutil.CustomError{ErrorMessage: "redis get error"})
ctx := &gofr.Context{Context: context.Background(),
Request: nil, Container: &container.Container{Logger: logger, Redis: rc}}
resp, err := RedisHandler(ctx)
assert.Nil(t, resp)
require.Error(t, err)
}
// MockRequest implements the Request interface for testing
type MockRequest struct {
*http.Request
params map[string]string
}
func (m *MockRequest) HostName() string {
if m.Request != nil {
return m.Request.Host
}
return ""
}
func (m *MockRequest) Params(s string) []string {
if m.Request != nil {
return m.Request.URL.Query()[s]
}
return nil
}
func NewMockRequest(req *http.Request) *MockRequest {
// Parse query parameters
queryParams := make(map[string]string)
for k, v := range req.URL.Query() {
if len(v) > 0 {
queryParams[k] = v[0]
}
}
return &MockRequest{
Request: req,
params: queryParams,
}
}
// Param returns URL query parameters
func (m *MockRequest) Param(key string) string {
return m.params[key]
}
// PathParam returns URL path parameters
func (m *MockRequest) PathParam(key string) string {
return ""
}
// Bind implements the Bind method required by the Request interface
func (m *MockRequest) Bind(i any) error {
return nil
}
// createTestContext sets up a GoFr context for unit tests with a given URL and optional mock container.
// This demonstrates how GoFr wraps http.Request into gofr.Request.
//
// Note: For path parameters, use mux.SetURLVars() before calling this function.
// See TestHandler_WithPathParams example for usage with path parameters.
func createTestContext(method, url string, mockContainer *container.Container) *gofr.Context {
// Create standard HTTP request
req := httptest.NewRequest(method, url, nil)
req.Header.Set("Content-Type", "application/json")
// Wrap with GoFr's Request wrapper (this is how GoFr wraps requests)
gofrReq := gofrHTTP.NewRequest(req)
var c *container.Container
if mockContainer != nil {
c = mockContainer
} else {
c = &container.Container{Logger: logging.NewLogger(logging.DEBUG)}
}
logger := c.Logger
return &gofr.Context{
Context: req.Context(),
Request: gofrReq,
Container: c,
ContextLogger: *logging.NewContextLogger(req.Context(), logger),
}
}
func TestHelloHandler(t *testing.T) {
// With name parameter
ctx := createTestContext(http.MethodGet, "/hello?name=test", nil)
resp, err := HelloHandler(ctx)
assert.NoError(t, err)
assert.Equal(t, "Hello test!", resp)
// Without name parameter
ctx = createTestContext(http.MethodGet, "/hello", nil)
resp, err = HelloHandler(ctx)
assert.NoError(t, err)
assert.Equal(t, "Hello World!", resp)
}
func TestErrorHandler(t *testing.T) {
ctx := createTestContext(http.MethodGet, "/error", nil)
resp, err := ErrorHandler(ctx)
assert.Nil(t, resp)
assert.Error(t, err)
assert.Equal(t, "some error occurred", err.Error())
}
func TestMysqlHandler(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
// Setup SQL mock to return 4
mocks.SQL.ExpectQuery("select 2+2").
WillReturnRows(mocks.SQL.NewRows([]string{"value"}).AddRow(4))
ctx := createTestContext(http.MethodGet, "/mysql", mockContainer)
resp, err := MysqlHandler(ctx)
assert.NoError(t, err)
assert.Equal(t, 4, resp)
}
func TestTraceHandler(t *testing.T) {
// Register HTTP service - each service gets its own separate mock instance
mockContainer, mocks := container.NewMockContainer(t, container.WithMockHTTPService("anotherService"))
// Redis expectations
mocks.Redis.EXPECT().Ping(gomock.Any()).Return(nil).Times(5)
// Create the test context FIRST
ctx := createTestContext(http.MethodGet, "/trace", mockContainer)
// TraceHandler calls Trace() twice, which modifies ctx.Context each time:
// 1. defer c.Trace("traceHandler").End() - modifies ctx.Context
// 2. span2 := c.Trace("some-sample-work") - modifies ctx.Context again
// We need to simulate this exact sequence to get the actual context that will be used
defer ctx.Trace("traceHandler").End() // First Trace() call (same as TraceHandler)
span2 := ctx.Trace("some-sample-work") // Second Trace() call (same as TraceHandler)
defer span2.End()
// HTTP service mock - use mocks.HTTPServices["serviceName"] to access the specific service
// Important: Use the map keyed by service name, not mocks.HTTPService (singular)
mockResp := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"data":"mock data"}`)),
}
// Now ctx.Context has been modified by both Trace() calls, matching what TraceHandler does
// TraceHandler calls: c.GetHTTPService("anotherService").Get(c, "redis", nil)
// When passing 'c' (*gofr.Context) to Get(), Go uses the embedded context.Context
// which is now the modified context after both Trace() calls
mocks.HTTPServices["anotherService"].EXPECT().Get(
ctx.Context, // Use the context after both Trace() calls (use gomock.Any to avoid this!)
"redis",
nil, // queryParams is nil in TraceHandler
).Return(mockResp, nil)
resp, err := TraceHandler(ctx)
assert.NoError(t, err)
assert.Equal(t, "mock data", resp)
}
================================================
FILE: examples/http-server/static/openapi.json
================================================
{
"openapi": "3.0.0",
"info": {
"title": "Http-Server API",
"description": "Example Http-Server with multiple endpoints. This swagger represents the requests and responses for various endpoints included in the [http-server example of gofr](https://github.com/gofr-dev/gofr/tree/development/examples/http-server).",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:9000"
}
],
"paths": {
"/hello": {
"get": {
"summary": "Get a greeting message",
"parameters": [
{
"in": "query",
"name": "name",
"schema": {
"type": "string"
},
"description": "Name to include in the greeting message"
}
],
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/error": {
"get": {
"summary": "Simulate an error response",
"responses": {
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"error": {
"type": "object",
"properties": {
"message": {
"type": "string"
}
}
}
}
}
}
}
}
}
}
},
"/redis": {
"get": {
"summary": "Simulate a response from Redis",
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "string"
}
}
}
}
}
}
}
}
},
"/mysql": {
"get": {
"summary": "Simulate a response from MySQL",
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"data": {
"type": "integer"
}
}
}
}
}
}
}
}
},
"/trace": {
"get": {
"summary": "Simulate the tracing feature for a request",
"responses": {
"200": {
"description": "Successful response"
}
}
}
}
}
}
================================================
FILE: examples/http-server-using-redis/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 8000
CMD ["/main"]
================================================
FILE: examples/http-server-using-redis/README.md
================================================
# Redis Example
This GoFr example demonstrates the use of Redis as datasource through a simple HTTP server.
### To run the example follow the steps below:
- Run the docker image of Redis
```console
docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5
```
- Now run the example
```console
go run main.go
```
================================================
FILE: examples/http-server-using-redis/main.go
================================================
package main
import (
"time"
"gofr.dev/pkg/gofr"
)
const redisExpiryTime = 5
func main() {
// Create a new application
app := gofr.New()
// Add routes for Redis operations
app.GET("/redis/{key}", RedisGetHandler)
app.POST("/redis", RedisSetHandler)
app.GET("/redis-pipeline", RedisPipelineHandler)
// Register an OnStart hook to warm up a cache.
// This runs before route registration as intended.
app.OnStart(func(ctx *gofr.Context) error {
ctx.Logger.Info("Warming up the cache...")
// Example: Fetch some data and store it in Redis.
// In a real app, this might come from a database or another service.
cacheKey := "initial-data"
cacheValue := "This is some data cached at startup."
err := ctx.Redis.Set(ctx, cacheKey, cacheValue, 0).Err()
if err != nil {
ctx.Logger.Errorf("Failed to warm up cache: %v", err)
return err // Return the error to halt startup if caching fails.
}
ctx.Logger.Info("Cache warmed up successfully!")
return nil
})
// Run the application
app.Run()
}
// RedisSetHandler sets a key-value pair in Redis using the Set Command.
func RedisSetHandler(c *gofr.Context) (any, error) {
input := make(map[string]string)
if err := c.Request.Bind(&input); err != nil {
return nil, err
}
for key, value := range input {
err := c.Redis.Set(c, key, value, redisExpiryTime*time.Minute).Err()
if err != nil {
return nil, err
}
}
return "Successful", nil
}
// RedisGetHandler gets the value from Redis.
func RedisGetHandler(c *gofr.Context) (any, error) {
key := c.PathParam("key")
value, err := c.Redis.Get(c, key).Result()
if err != nil {
return nil, err
}
resp := make(map[string]string)
resp[key] = value
return resp, nil
}
// RedisPipelineHandler demonstrates using multiple Redis commands efficiently within a pipeline.
func RedisPipelineHandler(c *gofr.Context) (any, error) {
pipe := c.Redis.Pipeline()
// Add multiple commands to the pipeline
pipe.Set(c, "testKey1", "testValue1", redisExpiryTime*time.Minute)
pipe.Get(c, "testKey1")
// Execute the pipeline and get results
cmds, err := pipe.Exec(c)
if err != nil {
return nil, err
}
// Process or return the results of each command in the pipeline (implementation omitted for brevity)
return cmds, nil
}
================================================
FILE: examples/http-server-using-redis/main_test.go
================================================
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource/redis"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestHTTPServerUsingRedis(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
method string
body []byte
path string
statusCode int
}{
{"post handler", http.MethodPost, []byte(`{"key1":"GoFr"}`), "/redis",
http.StatusCreated},
{"post invalid body", http.MethodPost, []byte(`{key:abc}`), "/redis",
http.StatusInternalServerError},
{"get handler", http.MethodGet, nil, "/redis/key1", http.StatusOK},
{"get handler invalid key", http.MethodGet, nil, "/redis/key2",
http.StatusInternalServerError},
{"pipeline handler", http.MethodGet, nil, "/redis-pipeline", http.StatusOK},
}
for i, tc := range tests {
req, _ := http.NewRequest(tc.method, configs.HTTPHost+tc.path, bytes.NewBuffer(tc.body))
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestRedisSetHandler(t *testing.T) {
configs := testutil.NewServerConfigs(t)
a := gofr.New()
logger := logging.NewLogger(logging.DEBUG)
redisClient, mock := redismock.NewClientMock()
rc := redis.NewClient(config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", "REDIS_PORT": "2001"}), logger, a.Metrics())
rc.Client = redisClient
mock.ExpectSet("key", "value", 5*time.Minute).SetErr(testutil.CustomError{ErrorMessage: "redis get error"})
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprintf("http://localhost:%d/handle", configs.HTTPPort), bytes.NewBuffer([]byte(`{"key":"value"}`)))
req.Header.Set("content-type", "application/json")
gofrReq := gofrHTTP.NewRequest(req)
ctx := &gofr.Context{Context: context.Background(),
Request: gofrReq, Container: &container.Container{Logger: logger, Redis: rc}}
resp, err := RedisSetHandler(ctx)
assert.Nil(t, resp)
require.Error(t, err)
}
func TestRedisPipelineHandler(t *testing.T) {
configs := testutil.NewServerConfigs(t)
a := gofr.New()
logger := logging.NewLogger(logging.DEBUG)
redisClient, mock := redismock.NewClientMock()
rc := redis.NewClient(config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", "REDIS_PORT": "2001"}), logger, a.Metrics())
rc.Client = redisClient
mock.ExpectSet("testKey1", "testValue1", time.Minute*5).SetErr(testutil.CustomError{ErrorMessage: "redis get error"})
mock.ClearExpect()
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprint("http://localhost:", configs.HTTPHost, "/handle"), bytes.NewBuffer([]byte(`{"key":"value"}`)))
req.Header.Set("content-type", "application/json")
gofrReq := gofrHTTP.NewRequest(req)
ctx := &gofr.Context{Context: context.Background(),
Request: gofrReq, Container: &container.Container{Logger: logger, Redis: rc}}
resp, err := RedisPipelineHandler(ctx)
assert.Nil(t, resp)
require.Error(t, err)
}
================================================
FILE: examples/sample-cmd/README.md
================================================
# CMD Example
This GoFr example demonstrates a simple CMD application.
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/sample-cmd/configs/.test.env
================================================
CMD_LOGS_FILE=logs.txt
================================================
FILE: examples/sample-cmd/main.go
================================================
package main
import (
"fmt"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/cmd/terminal"
)
func main() {
// Create a new command-line application
app := gofr.NewCMD()
// Add a sub-command "hello" with its handler, help and description
app.SubCommand("hello", func(c *gofr.Context) (any, error) {
return "Hello World!", nil
},
gofr.AddDescription("Print 'Hello World!'"),
gofr.AddHelp("hello world option"),
)
// Add a sub-command "params" with its handler, help and description
app.SubCommand("params", func(c *gofr.Context) (any, error) {
return fmt.Sprintf("Hello %s!", c.Param("name")), nil
})
app.SubCommand("spinner", spinner)
app.SubCommand("progress", progress)
// Run the command-line application
app.Run()
}
func spinner(ctx *gofr.Context) (any, error) {
// initialize the spinner
sp := terminal.NewDotSpinner(ctx.Out)
sp.Spin(ctx)
defer sp.Stop()
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(2 * time.Second):
}
return "Process Complete", nil
}
func progress(ctx *gofr.Context) (any, error) {
p, err := terminal.NewProgressBar(ctx.Out, 100)
if err != nil {
ctx.Warn("error initializing progress bar, err : %v", err)
}
for i := 1; i <= 100; i++ {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(50 * time.Millisecond):
// do a time taking process or compute a small subset of a bigger problem,
// this could be processing batches of a data set.
// increment the progress to display on the progress bar.
p.Incr(int64(1))
}
}
return "Process Complete", nil
}
================================================
FILE: examples/sample-cmd/main_test.go
================================================
package main
import (
"context"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/cmd"
"gofr.dev/pkg/gofr/cmd/terminal"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
// TestCMDRunWithNoArg checks that if no subcommand is found then error comes on stderr.
func TestCMDRunWithNoArg(t *testing.T) {
expErr := "No Command Found!\n"
output := testutil.StderrOutputForFunc(main)
assert.Equal(t, expErr, output, "TEST Failed.\n")
}
func TestCMDRunWithProperArg(t *testing.T) {
expResp := "Hello World!\n"
os.Args = []string{"command", "hello"}
output := testutil.StdoutOutputForFunc(main)
assert.Contains(t, output, expResp, "TEST Failed.\n")
}
func TestCMDRunWithParams(t *testing.T) {
expResp := "Hello Vikash!\n"
commands := []string{
"command params -name=Vikash",
"command params -name=Vikash",
"command -name=Vikash params",
"command params -name=Vikash -",
}
for i, command := range commands {
os.Args = strings.Split(command, " ")
output := testutil.StdoutOutputForFunc(main)
assert.Contains(t, output, expResp, "TEST[%d], Failed.\n", i)
}
}
func TestCMDRun_Spinner(t *testing.T) {
os.Args = []string{"command", "spinner"}
output := testutil.StdoutOutputForFunc(main)
// contains the spinner in the correct order
assert.Contains(t, output, "\r⣾ \r⣽ \r⣻ \r⢿ \r⡿")
// contains the process completion message
assert.Contains(t, output, "Process Complete\n")
}
func TestCMDRun_SpinnerContextCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
// add an already canceled context
res, err := spinner(&gofr.Context{
Context: ctx,
Request: cmd.NewRequest([]string{"command", "spinner"}),
Container: nil,
Out: terminal.New(),
})
assert.Empty(t, res)
assert.ErrorIs(t, err, context.Canceled)
}
func TestCMDRun_Progress(t *testing.T) {
os.Args = []string{"command", "progress"}
output := testutil.StdoutOutputForFunc(main)
assert.Contains(t, output, "\r1.000%")
assert.Contains(t, output, "\r20.000%")
assert.Contains(t, output, "\r50.000%")
assert.Contains(t, output, "\r100.000%")
assert.Contains(t, output, "Process Complete\n")
}
func TestCMDRun_ProgressContextCancelled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
// Create a proper context with logger to avoid nil pointer dereference
container := &container.Container{
Logger: logging.NewMockLogger(logging.ERROR),
}
res, err := progress(&gofr.Context{
Context: ctx,
Request: cmd.NewRequest([]string{"command", "progress"}),
Container: container,
Out: terminal.New(),
ContextLogger: *logging.NewContextLogger(ctx, container.Logger),
})
assert.Empty(t, res)
assert.ErrorIs(t, err, context.Canceled)
}
// TestCMDRunWithInvalidCommand tests that invalid commands return appropriate error
func TestCMDRunWithInvalidCommand(t *testing.T) {
expErr := "No Command Found!\n"
os.Args = []string{"command", "invalid"}
output := testutil.StderrOutputForFunc(main)
assert.Equal(t, expErr, output, "TEST Failed.\n")
}
// TestCMDRunWithEmptyParams tests the params command with empty name parameter
func TestCMDRunWithEmptyParams(t *testing.T) {
expResp := "Hello !\n"
os.Args = []string{"command", "params", "-name="}
output := testutil.StdoutOutputForFunc(main)
assert.Contains(t, output, expResp, "TEST Failed.\n")
}
// TestCMDRunHelpCommand tests the help functionality
func TestCMDRunHelpCommand(t *testing.T) {
testCases := []struct {
args []string
expected []string
}{
{[]string{"command", "help"}, []string{"Available commands:", "hello", "params", "spinner", "progress"}},
{[]string{"command", "-h"}, []string{"Available commands:", "hello", "params", "spinner", "progress"}},
{[]string{"command", "--help"}, []string{"Available commands:", "hello", "params", "spinner", "progress"}},
}
for i, tc := range testCases {
os.Args = tc.args
output := testutil.StdoutOutputForFunc(main)
for _, expected := range tc.expected {
assert.Contains(t, output, expected, "TEST[%d] Failed. Expected to contain: %s\n", i, expected)
}
}
}
// TestCMDRunHelpForSpecificCommand tests help for specific commands
func TestCMDRunHelpForSpecificCommand(t *testing.T) {
testCases := []struct {
args []string
expected string
}{
{[]string{"command", "hello", "-h"}, "hello world option"},
{[]string{"command", "hello", "--help"}, "hello world option"},
}
for i, tc := range testCases {
os.Args = tc.args
output := testutil.StdoutOutputForFunc(main)
assert.Contains(t, output, tc.expected, "TEST[%d] Failed.\n", i)
}
}
================================================
FILE: examples/using-add-filestore/README.md
================================================
# Add FileStore Example
This GoFr example demonstrates a CMD application that can be used to interact with a remote file server using FTP or SFTP protocol
### Setting up an FTP server in local machine
- https://security.appspot.com/vsftpd.html
- https://pypi.org/project/pyftpdlib/
Choose a library listed above and follow their respective documentation to configure an FTP server in your local machine and replace the configs/env file with correct HOST,USER_NAME,PASSWORD,PORT and REMOTE_DIR_PATH details.
### To run the example use the commands below:
To print the current working directory of the configured remote file server
```console
go run main.go pwd
```
To get the list of all directories or files in the given path of the configured remote file server
```
go run main.go ls -path=/
```
To grep the list of all files and directories in the given path that is matching with the keyword provided
```
go run main.go grep -keyword=fi -path=/
```
To create a file in the current working directory with the provided filename
```
go run main.go createfile -filename=file.txt
```
To remove the file with the provided filename from the current working directory
```
go run main.go rm -filename=file.txt
```
================================================
FILE: examples/using-add-filestore/go.mod
================================================
module gofr.dev/examples/using-add-filestore
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
gofr.dev/pkg/gofr/datasource/file/ftp v0.2.2
)
require (
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/pubsub v1.50.1 // indirect
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/XSAM/otelsql v0.41.0 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/graphql-go/graphql v0.8.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jlaffaye/ftp v0.2.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/kafka-go v0.4.50 // indirect
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.270.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
================================================
FILE: examples/using-add-filestore/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8=
github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d h1:abDbP7XBVgwda+h0J5Qra5p2OQpidU2FdkXvzCKL+H8=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d/go.mod h1:wKFzULXAPj3U2BDAPWXhSbQQNC6FU1+1/5iika6IY7g=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9 h1:cC0Hbb+18DJ4i6ybqDybvj4wdIDS4vnD0QEci98PgM8=
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9/go.mod h1:GpOj6zuVBG3Inr9qjEnuVTgBlk2lZ1S9DcoFiXWyKss=
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42 h1:JdOp2qR5PF4O75tzHeqrwnDDv8oHDptWyTbyYS4fD8E=
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42/go.mod h1:k/SS6VWkxY7dHPhoMQ8IdRu8L4lQtmGbhyXGg+vCnXE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 h1:U++6AfUpXXSILim4iH6Jb2oeK/mp7J4lNzzyO8Cx4Zw=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0/go.mod h1:HVNUDNMGMeykut/2GZ++AZjglCqew/+Hf4lxRVqFFxQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0 h1:Z7ARHF7193vyVltPYcmuhSKPLf8dP5rtJZLtTQnbMH4=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0/go.mod h1:DW09+gaEg5kydlb9g8kp4Nos3yqo9YSA1uHXkeJihXc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
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=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
gofr.dev/pkg/gofr/datasource/file/ftp v0.2.2 h1:uiJqbPI5wEugsZ0b5wSxezFijETZ3K/tOfeKIFeM5F0=
gofr.dev/pkg/gofr/datasource/file/ftp v0.2.2/go.mod h1:cfbTm5Uyf2Qnmj15QnkW8Fq9nVDutlpgTIqjCgHcqDg=
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: examples/using-add-filestore/main.go
================================================
package main
import (
"fmt"
"strconv"
"strings"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/datasource/file/ftp"
)
type FileServerType int
const (
FTP FileServerType = iota
SFTP
)
func main() {
app := gofr.NewCMD()
fileSystemProvider := configureFileServer(app)
app.AddFileStore(fileSystemProvider)
app.SubCommand("pwd", pwdCommandHandler)
app.SubCommand("ls", lsCommandHandler)
app.SubCommand("grep", grepCommandHandler)
app.SubCommand("createfile", createFileCommandHandler)
app.SubCommand("rm", rmCommandHandler)
app.Run()
}
func pwdCommandHandler(c *gofr.Context) (any, error) {
workingDirectory, err := c.File.Getwd()
return workingDirectory, err
}
func lsCommandHandler(c *gofr.Context) (any, error) {
path := c.Param("path")
files, err := c.File.ReadDir(path)
if err != nil {
return nil, err
}
printFiles(files)
return "", err
}
func grepCommandHandler(c *gofr.Context) (any, error) {
keyword := c.Param("keyword")
path := c.Param("path")
files, err := c.File.ReadDir(path)
if err != nil {
return nil, err
}
grepFiles(files, keyword)
return "", err
}
func createFileCommandHandler(c *gofr.Context) (any, error) {
fileName := c.Param("filename")
_, err := c.File.Create(fileName)
if err != nil {
return fmt.Sprintln("File Creation error"), err
}
return fmt.Sprintln("Successfully created file:", fileName), nil
}
func rmCommandHandler(c *gofr.Context) (any, error) {
fileName := c.Param("filename")
err := c.File.Remove(fileName)
if err != nil {
return fmt.Sprintln("File removal error"), err
}
return fmt.Sprintln("Successfully removed file:", fileName), nil
}
// This can be a common function to configure both FTP and SFTP server.
func configureFileServer(app *gofr.App) file.FileSystemProvider {
port, _ := strconv.Atoi(app.Config.Get("PORT"))
return ftp.New(&ftp.Config{
Host: app.Config.Get("HOST"),
User: app.Config.Get("USER_NAME"),
Password: app.Config.Get("PASSWORD"),
Port: port,
RemoteDir: app.Config.Get("REMOTE_DIR_PATH"),
})
}
func printFiles(files []file.FileInfo) {
for _, f := range files {
fmt.Println(f.Name())
}
}
func grepFiles(files []file.FileInfo, keyword string) {
for _, f := range files {
if strings.HasPrefix(f.Name(), keyword) {
fmt.Println(f.Name())
}
}
}
================================================
FILE: examples/using-add-filestore/main_test.go
================================================
package main
import (
"context"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/cmd"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/testutil"
)
type mockFileInfo struct {
name string
}
func (m mockFileInfo) Name() string { return m.name }
func (mockFileInfo) Size() int64 { return 0 }
func (mockFileInfo) Mode() os.FileMode { return 0 }
func (mockFileInfo) ModTime() time.Time { return time.Now() }
func (mockFileInfo) IsDir() bool { return false }
func (mockFileInfo) Sys() any { return nil }
func getContext(request gofr.Request, fileMock file.FileSystem) *gofr.Context {
return &gofr.Context{
Context: context.Background(),
Request: request,
Container: &container.Container{File: fileMock},
}
}
func TestPwdCommandHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().Getwd().DoAndReturn(func() (string, error) {
return "/", nil
})
ctx := getContext(nil, mock)
workingDirectory, _ := pwdCommandHandler(ctx)
assert.Contains(t, workingDirectory, "/", "Test failed")
}
func TestLSCommandHandler(t *testing.T) {
var (
res any
err error
)
path := "/"
logs := testutil.StdoutOutputForFunc(func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().ReadDir(path).DoAndReturn(func(_ string) ([]file.FileInfo, error) {
files := []file.FileInfo{
mockFileInfo{name: "file1.txt"},
mockFileInfo{name: "file2.txt"},
}
return files, nil
})
r := cmd.NewRequest([]string{"command", "ls", "-path=/"})
ctx := getContext(r, mock)
res, err = lsCommandHandler(ctx)
})
require.NoError(t, err)
assert.Equal(t, "", res)
assert.Contains(t, logs, "file1.txt", "Test failed")
assert.Contains(t, logs, "file2.txt", "Test failed")
assert.NotContains(t, logs, "file3.txt", "Test failed")
}
func TestGrepCommandHandler(t *testing.T) {
var (
res any
err error
)
path := "/"
logs := testutil.StdoutOutputForFunc(func() {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().ReadDir("/").DoAndReturn(func(_ string) ([]file.FileInfo, error) {
files := []file.FileInfo{
mockFileInfo{name: "file1.txt"},
mockFileInfo{name: "file2.txt"},
}
return files, nil
})
r := cmd.NewRequest([]string{"command", "grep", "-keyword=fi", fmt.Sprintf("-path=%s", path)})
ctx := getContext(r, mock)
res, err = grepCommandHandler(ctx)
})
require.NoError(t, err)
assert.Equal(t, "", res)
assert.Contains(t, logs, "file1.txt", "Test failed")
assert.Contains(t, logs, "file2.txt", "Test failed")
assert.NotContains(t, logs, "file3.txt", "Test failed")
}
func TestCreateFileCommand(t *testing.T) {
fileName := "file.txt"
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().Create(fileName).DoAndReturn(func(_ string) (file.File, error) {
return &file.MockFile{}, nil
})
r := cmd.NewRequest([]string{"command", "createfile", fmt.Sprintf("-filename=%s", fileName)})
ctx := getContext(r, mock)
output, _ := createFileCommandHandler(ctx)
assert.Contains(t, output, "Successfully created file: file.txt", "Test failed")
}
func TestRmCommand(t *testing.T) {
fileName := "file.txt"
os.Args = []string{"command", "rm", fmt.Sprintf("-filename=%s", fileName)}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().Remove("file.txt").DoAndReturn(func(_ string) error {
return nil
})
r := cmd.NewRequest([]string{"command", "rm", fmt.Sprintf("-filename=%s", fileName)})
ctx := getContext(r, mock)
output, _ := rmCommandHandler(ctx)
assert.Contains(t, output, "Successfully removed file: file.txt", "Test failed")
}
================================================
FILE: examples/using-add-rest-handlers/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 9090
CMD ["/main"]
================================================
FILE: examples/using-add-rest-handlers/README.md
================================================
# AddRESTHandlers Example
This GoFr example demonstrates a simple HTTP server with CRUD operations which are created by GoFr using the given struct.
### To run the example follow the steps below:
- Run the docker image of MySQL
```console
docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30
```
- Now run the example
```console
go run main.go
```
================================================
FILE: examples/using-add-rest-handlers/main.go
================================================
package main
import (
"gofr.dev/examples/using-add-rest-handlers/migrations"
"gofr.dev/pkg/gofr"
)
type user struct {
Id int `json:"id"`
Name string `json:"name"`
Age int `json:"age"`
IsEmployed bool `json:"isEmployed"`
}
// GetAll : User can overwrite the specific handlers by implementing them like this
func (u *user) GetAll(c *gofr.Context) (any, error) {
return "user GetAll called", nil
}
func main() {
// Create a new application
a := gofr.New()
// Add migrations to run
a.Migrate(migrations.All())
// AddRESTHandlers creates CRUD handles for the given entity
err := a.AddRESTHandlers(&user{})
if err != nil {
return
}
// Run the application
a.Run()
}
================================================
FILE: examples/using-add-rest-handlers/main_test.go
================================================
package main
import (
"bytes"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestIntegration_AddRESTHandlers(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
method string
path string
body []byte
statusCode int
}{
{"empty path", http.MethodGet, "/", nil, 404},
{"success Create", http.MethodPost, "/user",
[]byte(`{"id":10, "name":"john doe", "age":99, "isEmployed": true}`), 201},
{"success GetAll", http.MethodGet, "/user", nil, 200},
{"success Get", http.MethodGet, "/user/10", nil, 200},
{"success Update", http.MethodPut, "/user/10",
[]byte(`{"name":"john doe", "age":99, "isEmployed": false}`), 200},
{"success Delete", http.MethodDelete, "/user/10", nil, 204},
}
for i, tc := range tests {
req, _ := http.NewRequest(tc.method, configs.HTTPHost+tc.path, bytes.NewReader(tc.body))
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
================================================
FILE: examples/using-add-rest-handlers/migrations/1721816030_create_user_table.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
const createTable = `CREATE TABLE IF NOT EXISTS user (
id int not null primary key,
name varchar(50) not null,
age int not null,
is_employed bool not null
);`
func createTableUser() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
_, err := d.SQL.Exec(createTable)
if err != nil {
return err
}
return nil
},
}
}
================================================
FILE: examples/using-add-rest-handlers/migrations/1721816030_create_user_table_test.go
================================================
package migrations
import (
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/migration"
)
func TestCreateTableUser(t *testing.T) {
tests := []struct {
desc string
mockBehaviors func(mock sqlmock.Sqlmock)
expectedError error
}{
{"successful creation", func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnResult(sqlmock.NewResult(0, 1))
}, nil},
{"error on create table", func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnError(fmt.Errorf("create table error"))
}, fmt.Errorf("create table error")},
}
for i, tc := range tests {
// Create mock database and datasource
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)
defer db.Close()
datasource := migration.Datasource{SQL: db}
// Set mock expectations
tc.mockBehaviors(mock)
// Execute the migration
err = createTableUser().UP(datasource)
assert.Equal(t, tc.expectedError, err, "TEST[%d] Failed.\n%s", i, tc.desc)
require.NoError(t, mock.ExpectationsWereMet())
}
}
================================================
FILE: examples/using-add-rest-handlers/migrations/all.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
1721816030: createTableUser(),
}
}
================================================
FILE: examples/using-add-rest-handlers/migrations/all_test.go
================================================
package migrations
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
func TestAll(t *testing.T) {
// Get the map of migrations
allMigrations := All()
expected := map[int64]migration.Migrate{
1721816030: createTableUser(),
}
// Check if the length of the maps match
assert.Equal(t, len(expected), len(allMigrations), "TestAll Failed!")
}
================================================
FILE: examples/using-cron-jobs/Readme.md
================================================
# Using Cron Jobs in GoFr
This example demonstrates how to schedule and run background jobs using the **GoFr** framework’s built-in Cron Job support.
---
## Overview
In this example, we:
* Schedule a job named `counter` to run **every second**.
* Increment a counter on each execution and log its value.
* Run the application for a limited duration (3 seconds) to demonstrate the cron execution in action.
* Include a unit test to verify that the cron job executes successfully.
---
## How to Run
1. **Clone the repository** and navigate to this example:
```bash
git clone https://github.com/gofr-dev/gofr.git
cd gofr/examples/using-cron-jobs
```
2. **Run the application**:
```bash
go run main.go
```
* The counter will increment every second.
* Application will stop automatically after 3 seconds.
*(In a real-world application, you would call `app.Run()` to keep the cron running indefinitely.)*
---
## How It Works
### main.go
```go
app.AddCronJob("* * * * * *", "counter", count)
```
* Schedules the `count` function to run every second.
* Cron syntax here uses six fields (including seconds) — `"* * * * * *"` means “every second.”
```go
time.Sleep(duration * time.Second)
```
* Stops the application after the `duration` (3 seconds) for demonstration purposes.
### count Function
* Acquires a write lock.
* Increments the counter variable `n`.
* Logs the current counter value.
---
## Testing
The test (`main_test.go`):
* Runs the `main()` function.
* Waits slightly longer than 1 second.
* Checks if the counter has incremented exactly once.
* Logs the metrics server host.
Run the test:
```bash
go test -v
```
Expected output:
```
=== RUN Test_UserPurgeCron
--- PASS: Test_UserPurgeCron (1.10s)
PASS
```
---
## Example Output
When running `main.go`, you should see:
```
INFO Count: 1
INFO Count: 2
INFO Count: 3
```
================================================
FILE: examples/using-cron-jobs/main.go
================================================
package main
import (
"sync"
"gofr.dev/pkg/gofr"
)
var (
n = 0
mu sync.RWMutex
)
const duration = 3
func main() {
app := gofr.New()
// runs every second
app.AddCronJob("* * * * * *", "counter", count)
app.Run()
}
func count(c *gofr.Context) {
mu.Lock()
defer mu.Unlock()
n++
c.Log("Count:", n)
}
================================================
FILE: examples/using-cron-jobs/main_test.go
================================================
package main
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_UserPurgeCron(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(1100 * time.Millisecond)
expected := 1
var m int
mu.Lock()
m = n
mu.Unlock()
assert.Equal(t, expected, m)
t.Logf("Metrics server running at: %s", configs.MetricsHost)
}
================================================
FILE: examples/using-custom-metrics/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 9011
CMD ["/main"]
================================================
FILE: examples/using-custom-metrics/README.md
================================================
# Custom Metrics Example
This GoFr example demonstrates the use of custom metrics through a simple HTTP server that creates and populate metrics.
GoFr by default pushes metrics to port `2121` on `/metrics` endpoint.
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/using-custom-metrics/main.go
================================================
package main
import (
"time"
"gofr.dev/pkg/gofr"
)
// This example simulates the usage of custom metrics for transactions of an ecommerce store.
const (
transactionSuccessful = "transaction_success"
transactionTime = "transaction_time"
totalCreditDaySales = "total_credit_day_sale"
productStock = "product_stock"
)
func main() {
// Create a new application
a := gofr.New()
a.Metrics().NewCounter(transactionSuccessful, "used to track the count of successful transactions")
a.Metrics().NewUpDownCounter(totalCreditDaySales, "used to track the total credit sales in a day")
a.Metrics().NewGauge(productStock, "used to track the number of products in stock")
a.Metrics().NewHistogram(transactionTime, "used to track the time taken by a transaction",
5, 10, 15, 20, 25, 35)
// Add all the routes
a.POST("/transaction", TransactionHandler)
a.POST("/return", ReturnHandler)
// Run the application
a.Run()
}
func TransactionHandler(c *gofr.Context) (any, error) {
transactionStartTime := time.Now()
// transaction logic
c.Metrics().IncrementCounter(c, transactionSuccessful)
tranTime := time.Now().Sub(transactionStartTime).Milliseconds()
c.Metrics().RecordHistogram(c, transactionTime, float64(tranTime))
c.Metrics().DeltaUpDownCounter(c, totalCreditDaySales, 1000, "sale_type", "credit")
c.Metrics().SetGauge(productStock, 10)
return "Transaction Successful", nil
}
func ReturnHandler(c *gofr.Context) (any, error) {
// logic to create a sales return
c.Metrics().DeltaUpDownCounter(c, totalCreditDaySales, -1000, "sale_type", "credit_return")
// Update the Gauge metric for product stock
c.Metrics().SetGauge(productStock, 50)
return "Return Successful", nil
}
================================================
FILE: examples/using-custom-metrics/main_test.go
================================================
package main
import (
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestIntegration(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
c := http.Client{}
req, _ := http.NewRequest(http.MethodPost, configs.HTTPHost+"/transaction", nil)
req.Header.Set("content-type", "application/json")
_, err := c.Do(req)
if err != nil {
t.Fatalf("request to /transaction failed %v", err)
}
req, _ = http.NewRequest(http.MethodPost, configs.HTTPHost+"/return", nil)
_, err = c.Do(req)
if err != nil {
t.Fatalf("request to /transaction failed %v", err)
}
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("http://localhost:%d/metrics", configs.MetricsPort), nil)
resp, err := c.Do(req)
if err != nil {
t.Fatalf("request to localhost:%d/metrics failed: %v", configs.MetricsPort, err)
}
body, _ := io.ReadAll(resp.Body)
strBody := string(body)
assert.Equal(t, http.StatusOK, resp.StatusCode, "TEST[%d], Failed.\n%s")
assert.Contains(t, strBody, `product_stock{otel_scope_name="using-metrics",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 50`)
assert.Contains(t, strBody, `total_credit_day_sale{otel_scope_name="using-metrics",otel_scope_schema_url="",otel_scope_version="v0.1.0",sale_type="credit"} 1000`)
assert.Contains(t, strBody, `total_credit_day_sale{otel_scope_name="using-metrics",otel_scope_schema_url="",otel_scope_version="v0.1.0",sale_type="credit_return"} -1000`)
assert.Contains(t, strBody, `transaction_success{otel_scope_name="using-metrics",otel_scope_schema_url="",otel_scope_version="v0.1.0"} 1`)
assert.Contains(t, strBody, "transaction_time")
}
================================================
FILE: examples/using-file-bind/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 8300
CMD ["/main"]
================================================
FILE: examples/using-file-bind/README.md
================================================
# Using File Bind Example
This GoFr example demonstrates the use of context Bind where incoming request has multipart-form data and then binds
it to the fields of the struct. GoFr currently supports zip file type and also binds the more generic [`multipart.FileHeader`](https://pkg.go.dev/mime/multipart#FileHeader)
### Usage
```go
type Data struct {
Compressed file.Zip `file:"upload"`
FileHeader *multipart.FileHeader `file:"file_upload"`
}
func Handler(c *gofr.Context) (any, error) {
var d Data
// bind the multipart data into the variable d
err := c.Bind(&d)
if err != nil {
return nil, err
}
}
```
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/using-file-bind/main.go
================================================
package main
import (
"fmt"
"io"
"mime/multipart"
"os"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/file"
)
func main() {
app := gofr.New()
app.POST("/upload", UploadHandler)
app.Run()
}
// Data is the struct that we are trying to bind files to
type Data struct {
// Name represents the non-file field in the struct
Name string `form:"name"`
// The Compressed field is of type zip,
// the tag `upload` signifies the key for the form where the file is uploaded
// if the tag is not present, the field name would be taken as a key.
Compressed file.Zip `file:"upload"`
// The FileHeader determines the generic file format that we can get
// from the multipart form that gets parsed by the incoming HTTP request
FileHeader *multipart.FileHeader `file:"file_upload"`
}
func UploadHandler(c *gofr.Context) (any, error) {
var d Data
// bind the multipart data into the variable d
err := c.Bind(&d)
if err != nil {
return nil, err
}
// create local copies of the zipped files in tmp folder
err = d.Compressed.CreateLocalCopies("tmp")
if err != nil {
return nil, err
}
defer os.RemoveAll("tmp")
f, err := d.FileHeader.Open()
if err != nil {
return nil, err
}
defer f.Close()
// read the file content
content, err := io.ReadAll(f)
if err != nil {
return false, err
}
// return the number of compressed files received
return fmt.Sprintf("zipped files: %d, len of file `a`: %d", len(d.Compressed.Files), len(content)), nil
}
================================================
FILE: examples/using-file-bind/main_test.go
================================================
package main
import (
"bytes"
"io"
"mime/multipart"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestMain_BindError(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond)
c := http.Client{}
req, _ := http.NewRequest(http.MethodPost, configs.HTTPHost+"/upload", http.NoBody)
req.Header.Set("content-type", "multipart/form-data")
resp, err := c.Do(req)
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
require.NoError(t, err)
buf, contentType := generateMultiPartBody(t)
req, _ = http.NewRequest(http.MethodPost, configs.HTTPHost+"/upload", buf)
req.Header.Set("content-type", contentType)
req.ContentLength = int64(buf.Len())
resp, err = c.Do(req)
require.Equal(t, http.StatusCreated, resp.StatusCode)
}
func generateMultiPartBody(t *testing.T) (*bytes.Buffer, string) {
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
f, err := os.Open("../../pkg/gofr/testutil/test.zip")
if err != nil {
t.Fatalf("Failed to open test.zip: %v", err)
}
defer f.Close()
zipPart, err := writer.CreateFormFile("upload", "test.zip")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
_, err = io.Copy(zipPart, f)
if err != nil {
t.Fatalf("Failed to write file to form: %v", err)
}
fileHeader, err := writer.CreateFormFile("file_upload", "hello.txt")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
_, err = io.Copy(fileHeader, bytes.NewReader([]byte(`Test hello!`)))
if err != nil {
t.Fatalf("Failed to write file to form: %v", err)
}
err = writer.WriteField("name", "test-name")
if err != nil {
t.Fatalf("Failed to write name to form: %v", err)
}
// Close the multipart writer
writer.Close()
return &buf, writer.FormDataContentType()
}
================================================
FILE: examples/using-graphql/configs/schema.graphqls
================================================
type User {
id: Int
name: String
role: String
}
type Query {
hello: String
getUser(id: Int): User
}
type Mutation {
createUser(name: String, role: String): User
}
================================================
FILE: examples/using-graphql/main.go
================================================
package main
import (
"gofr.dev/examples/using-graphql/migrations"
"gofr.dev/pkg/gofr"
)
// User is the domain type used in GraphQL resolvers and integration tests.
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
func main() {
app := gofr.New()
app.Migrate(migrations.All())
// Example: curl -X POST http://localhost:9091/graphql -H "Content-Type: application/json" -d '{"query": "{ hello }"}'
app.GraphQLQuery("hello", func(c *gofr.Context) (any, error) {
return "Hello GoFr GraphQL!", nil
})
// Example: curl -X POST -H "Content-Type: application/json" -d '{"query": "query GetUser($id: Int) { getUser(id: $id) { name role } }", "variables": {"id": 1}}' http://localhost:9091/graphql
app.GraphQLQuery("getUser", func(c *gofr.Context) (any, error) {
var args struct {
ID int `json:"id"`
}
if err := c.Bind(&args); err != nil {
return nil, err
}
var user User
// Errors returned from resolvers are automatically placed in the GraphQL
// response "errors" array with a 200 OK status, per the GraphQL spec.
err := c.SQL.QueryRowContext(c, "SELECT id, name, role FROM users WHERE id = ?", args.ID).
Scan(&user.ID, &user.Name, &user.Role)
if err != nil {
return nil, err
}
return user, nil
})
// Example: curl -X POST -H "Content-Type: application/json" -d '{"query": "mutation CreateUser($name: String, $role: String) { createUser(name: $name, role: $role) { id name } }", "variables": {"name": "New User", "role": "admin"}}' http://localhost:9091/graphql
app.GraphQLMutation("createUser", func(c *gofr.Context) (any, error) {
var args struct {
Name string `json:"name"`
Role string `json:"role"`
}
if err := c.Bind(&args); err != nil {
return nil, err
}
result, err := c.SQL.ExecContext(c, "INSERT INTO users (name, role) VALUES (?, ?)", args.Name, args.Role)
if err != nil {
return nil, err
}
id, err := result.LastInsertId()
if err != nil {
return nil, err
}
return User{ID: int(id), Name: args.Name, Role: args.Role}, nil
})
app.Run()
}
================================================
FILE: examples/using-graphql/main_test.go
================================================
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/testutil"
)
func waitForReady(t *testing.T, host string) {
t.Helper()
client := &http.Client{Timeout: 1 * time.Second}
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
resp, err := client.Get(host + "/.well-known/alive")
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
return
}
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("Server at %s not ready after 10s", host)
}
// newTestApp creates a GoFr application configured for integration testing.
func newTestApp(t *testing.T) (*gofr.App, string) {
t.Helper()
httpPort := testutil.GetFreePort(t)
metricsPort := testutil.GetFreePort(t)
t.Setenv("HTTP_PORT", strconv.Itoa(httpPort))
t.Setenv("METRICS_PORT", strconv.Itoa(metricsPort))
host := fmt.Sprintf("http://localhost:%d", httpPort)
app := gofr.New()
app.GraphQLQuery("hello", func(c *gofr.Context) (interface{}, error) {
return "Hello GoFr GraphQL with SQL!", nil
})
app.GraphQLQuery("getUser", func(c *gofr.Context) (interface{}, error) {
var args struct {
ID int `json:"id"`
}
if err := c.Bind(&args); err != nil {
return nil, err
}
// Return stubbed data instead of calling c.SQL
return User{ID: args.ID, Name: "Test User", Role: "Admin"}, nil
})
app.GraphQLMutation("createUser", func(c *gofr.Context) (interface{}, error) {
var args struct {
Name string `json:"name"`
Role string `json:"role"`
}
if err := c.Bind(&args); err != nil {
return nil, err
}
// Return stubbed data instead of calling c.SQL
return User{ID: 1, Name: args.Name, Role: args.Role}, nil
})
return app, host
}
func TestIntegration_GraphQL(t *testing.T) {
t.Setenv("APP_ENV", "dev")
app, host := newTestApp(t)
t.Cleanup(func() {
app.Shutdown(context.Background())
})
go app.Run()
waitForReady(t, host)
t.Run("hello query", func(t *testing.T) {
query := `{"query": "{ hello }"}`
resp, err := http.Post(host+"/graphql", "application/json", bytes.NewBufferString(query))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result struct {
Data struct {
Hello string `json:"hello"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Equal(t, "Hello GoFr GraphQL with SQL!", result.Data.Hello)
})
t.Run("createUser mutation", func(t *testing.T) {
query := `{"query": "mutation { createUser(name: \"Integration Test\", role: \"Tester\") { id name role } }"}`
resp, err := http.Post(host+"/graphql", "application/json", bytes.NewBufferString(query))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result struct {
Data struct {
CreateUser User `json:"createUser"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Greater(t, result.Data.CreateUser.ID, 0)
assert.Equal(t, "Integration Test", result.Data.CreateUser.Name)
assert.Equal(t, "Tester", result.Data.CreateUser.Role)
})
t.Run("getUser query", func(t *testing.T) {
query := `{"query": "{ getUser(id: 1) { id name role } }"}`
resp, err := http.Post(host+"/graphql", "application/json", bytes.NewBufferString(query))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var result struct {
Data struct {
GetUser User `json:"getUser"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
assert.Equal(t, 1, result.Data.GetUser.ID)
assert.NotEmpty(t, result.Data.GetUser.Name)
})
t.Run("playground UI is accessible", func(t *testing.T) {
resp, err := http.Get(host + "/.well-known/graphql/ui")
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
})
}
================================================
FILE: examples/using-graphql/migrations/20240205125300_create_users_table.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func CreateUsersTable() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
_, err := d.SQL.Exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), role VARCHAR(255))")
return err
},
}
}
================================================
FILE: examples/using-graphql/migrations/all.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
20240205125300: CreateUsersTable(),
}
}
================================================
FILE: examples/using-html-template/README.md
================================================
# Using HTML Template
This GoFr example demonstrates the use of html template, GoFr supports both static and dynamic html templates.
All template files—whether HTML or HTMX—should be placed inside a templates directory located at the root of your project.
### Usage
```go
// path to the static html files
app.AddStaticFiles("/", "./static")
func listHandler(*gofr.Context) (any, error) {
// Get data from somewhere
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{Title: "Expand on Gofr documentation ", Done: false},
{Title: "Add more examples", Done: true},
{Title: "Write some articles", Done: false},
},
}
// provide data and template name to response.Template
return response.Template{Data: data, Name: "todo.html"}, nil
}
```
### To run the example use the command below:
```console
go run main.go
```
================================================
FILE: examples/using-html-template/main.go
================================================
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/http/response"
)
func main() {
app := gofr.New()
app.GET("/list", listHandler)
app.AddStaticFiles("/", "./static")
app.Run()
}
type Todo struct {
Title string
Done bool
}
type TodoPageData struct {
PageTitle string
Todos []Todo
}
func listHandler(*gofr.Context) (any, error) {
// Get data from somewhere
data := TodoPageData{
PageTitle: "My TODO list",
Todos: []Todo{
{Title: "Expand on Gofr documentation ", Done: false},
{Title: "Add more examples", Done: true},
{Title: "Write some articles", Done: false},
},
}
return response.Template{Data: data, Name: "todo.html"}, nil
}
================================================
FILE: examples/using-html-template/main_test.go
================================================
package main
import (
"context"
"io"
"net/http"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_ListHandler(t *testing.T) {
configs := testutil.NewServerConfigs(t)
c := &http.Client{}
go main()
time.Sleep(100 * time.Millisecond)
// Make a GET request to the /list endpoint
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet,
configs.HTTPHost+"/list", http.NoBody)
resp, err := c.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "text/html", resp.Header.Get("Content-Type"))
// Read response body
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
bodyStr := strings.Join(strings.Fields(string(body)), " ")
// Validate key HTML elements using strings.Contains
assert.Contains(t, bodyStr, "My TODO list ", "Header text missing")
expectedItems := "Expand on Gofr documentation " +
"Add more examples Write some articles "
assert.Contains(t, bodyStr, expectedItems, "Missing TODO items")
// Validate stylesheet link
assert.Contains(t, bodyStr, ` `, "Stylesheet link missing")
}
func Test_IndexHTML(t *testing.T) {
configs := testutil.NewServerConfigs(t)
c := &http.Client{}
go main()
time.Sleep(100 * time.Millisecond) // Allow server to start
// Request root endpoint
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet,
configs.HTTPHost+"/", http.NoBody)
resp, err := c.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Validate basic response
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
bodyStr := strings.Join(strings.Fields(string(body)), " ")
// Validate index.html content
assert.Contains(t, bodyStr, "Hello HTML! ", "Main header missing")
assert.Contains(t, bodyStr, `src="/favicon.ico"`, "Favicon reference missing")
assert.Contains(t, bodyStr, `href="/list"`, "List endpoint link missing")
}
func Test_404HTML(t *testing.T) {
configs := testutil.NewServerConfigs(t)
c := &http.Client{}
go main()
time.Sleep(100 * time.Millisecond) // Allow server to start
// Request non-existent endpoint
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet,
configs.HTTPHost+"/non-existent-page", http.NoBody)
resp, err := c.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Validate 404 response
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
bodyStr := strings.Join(strings.Fields(string(body)), " ")
// Validate 404.html content
assert.Contains(t, bodyStr, "404 - Page Not Found ", "404 header missing")
assert.Contains(t, bodyStr, `href="index.html"`, "Home link missing")
assert.Contains(t, bodyStr, "The requested resource could not be found", "Error message missing")
}
================================================
FILE: examples/using-html-template/static/404.html
================================================
404 - Page Not Found
404 - Page Not Found
The requested resource could not be found.
Go Back to Home
================================================
FILE: examples/using-html-template/static/index.html
================================================
Hello HTML!
This is a simple HTML file served by the server.
Click to see template being rendered
================================================
FILE: examples/using-html-template/static/style.css
================================================
body,html {
margin: 0;
padding: 0;
background: oklch(0.208 0.042 265.755);
color: oklch(0.869 0.022 252.894);
font-family: ui-sans-serif, system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
h1 {
font-size: 2rem;
margin: 0;
padding: 1rem;
background: oklch(0.208 0.042 265.755);
color: oklch(0.869 0.022 252.894);
}
div#content {padding:80px 20px; text-align: center }
a {color: oklch(0.917 0.08 205.041); text-decoration: none;}
a:hover {font-weight: bold}
h2 {
text-align: left;
}
ul {
text-align: left ;
}
li {
list-style-type: circle;
}
li.done {
list-style-type: disc;
text-decoration: line-through;
}
================================================
FILE: examples/using-html-template/templates/todo.html
================================================
{{.PageTitle}}
{{range .Todos}}
{{if .Done}}
{{.Title}}
{{else}}
{{.Title}}
{{end}}
{{end}}
================================================
FILE: examples/using-http-auth-middleware/ReadMe.md
================================================
# HTTP Auth Middleware
This GoFr example demonstrates the usage of auth middlewares in Gofr. Gofr supports the following auth middlewares out of the box:
- API Key Auth
- Basic Auth
- OAuth
## Setup
User can enable requisite auth middleware by adding the respective code snippet
### Basic Auth Middleware Setup
```go
a := gofr.New()
// OPTION 1
basicAuthProvider, err := middleware.NewBasicAuthProvider(map[string]string{"username": "password"})
// handle error - typically caused by invalid configuration
basicAuthMiddleware := middleware.AuthMiddleware(basicAuthProvider)
a.UseMiddleware(basicAuthMiddleware)
// OR
// OPTION 2
a.EnableBasicAuthWithValidator(func(c *container.Container, username, password string) bool {
// basic validation based on fixed set of credentials
return username == "username" && password == "password"
// Alternatively, get the expected username/password from any storage and validate
//expectedPassword, err := c.KVStore.Get(context.Background(), username)
//if err != nil || expectedPassword != password {
// return false
//}
//return true
})
```
### API Key Auth Middleware Setup
```go
a := gofr.New()
// OPTION 1
apiKeyProvider, err := middleware.NewAPIKeyAuthProvider([]string{"valid-key-1", "valid-key-2"})
// handle error - typically caused by invalid configuration
apiKeyMiddleware := middleware.AuthMiddleware(apiKeyProvider)
a.UseMiddleware(apiKeyMiddleware)
// OR
// OPTION 2
a.EnableAPIKeyAuthWithValidator(func(c *container.Container, apiKey string) bool {
// basic validation based on fixed set of credentials
return apiKey == "valid-api-key"
// Alternatively, get the expected APIKey from any storage and validate
//data, err := c.KVStore.Get(context.Background(), apiKey)
//if err != nil || data == "" {
// return false
//}
//return true
})
```
### OAuth Middleware Setup
```go
a := gofr.New()
a.EnableOAuth("", 10)
```
## Execution:
- Enable the desired auth middleware (main.go)
- Run the example using below command :
```console
go run main.go
```
- Call the API on `localhost:8000/test-auth` with credentials in the Auth header
================================================
FILE: examples/using-http-auth-middleware/main.go
================================================
package main
import (
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
)
func main() {
a := gofr.New()
//For Basic Auth
//setupBasicAuth(a)
// For APIKey Auth
setupAPIKeyAuth(a)
//For OAuth
//a.EnableOAuth("", 10)
a.GET("/test-auth", testHandler)
a.Run()
}
func testHandler(_ *gofr.Context) (any, error) {
return "success", nil
}
func setupBasicAuth(a *gofr.App) {
a.EnableBasicAuthWithValidator(func(c *container.Container, username, password string) bool {
if username == "username" && password == "password" {
return true
}
return false
// Alternatively, get the expected username/password from any storage and validate
//expectedPassword, err := c.KVStore.Get(context.Background(), username)
//if err != nil || expectedPassword != password {
// return false
//}
//return true
})
}
func setupAPIKeyAuth(a *gofr.App) {
a.EnableAPIKeyAuthWithValidator(func(c *container.Container, apiKey string) bool {
// basic validation based on fixed set of credentials
return apiKey == "valid-api-key"
// Alternatively, get the expected APIKey from any storage and validate
//data, err := c.KVStore.Get(context.Background(), apiKey)
//if err != nil || data == "" {
// return false
//}
//return true
})
}
================================================
FILE: examples/using-http-auth-middleware/main_test.go
================================================
package main
import (
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
"net/http"
"testing"
"time"
)
func Test_setupAPIKeyAuthFailed(t *testing.T) {
serverConfigs := testutil.NewServerConfigs(t)
// Run main() in a goroutine to avoid blocking
go main()
// Allow time for server to start
time.Sleep(100 * time.Millisecond)
client := &http.Client{Timeout: 200 * time.Millisecond}
// Test invalid API key
t.Run("Invalid API Key", func(t *testing.T) {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
serverConfigs.HTTPHost+"/test-auth", http.NoBody)
req.Header.Set("X-Api-Key", "test-key")
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Error making request: %v", err)
}
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
}
func Test_setupAPIKeyAuthSuccess(t *testing.T) {
serverConfigs := testutil.NewServerConfigs(t)
// Run main() in a goroutine to avoid blocking
go main()
// Allow time for server to start
time.Sleep(100 * time.Millisecond)
client := &http.Client{Timeout: 200 * time.Millisecond}
// Test valid API key
t.Run("Valid API Key", func(t *testing.T) {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
serverConfigs.HTTPHost+"/test-auth", http.NoBody)
req.Header.Set("X-Api-Key", "valid-api-key")
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Error making request: %v", err)
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
})
}
//func encodeBasicAuthorization(t *testing.T, arg string) string {
// t.Helper()
//
// data := []byte(arg)
//
// dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
//
// base64.StdEncoding.Encode(dst, data)
//
// s := "Basic " + string(dst)
//
// return s
//}
//func Test_setupBasicAuthSuccess(t *testing.T) {
// serverConfigs := testutil.NewServerConfigs(t)
//
// app := gofr.New()
//
// setupBasicAuth(app)
//
// app.GET("/basic-auth-success", func(_ *gofr.Context) (any, error) {
// return "success", nil
// })
//
// go app.Run()
//
// time.Sleep(100 * time.Millisecond)
//
// var netClient = &http.Client{
// Timeout: 200 * time.Millisecond,
// }
//
// req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
// serverConfigs.HTTPHost + "/basic-auth-success", http.NoBody)
//
// req.Header.Add("Authorization", encodeBasicAuthorization(t, "username:password"))
//
// // Send the request and check for successful response
// resp, err := netClient.Do(req)
// if err != nil {
// t.Errorf("error while making HTTP request in Test_BasicAuthMiddleware. err: %v", err)
// return
// }
//
// defer resp.Body.Close()
//
// assert.Equal(t, http.StatusOK, resp.StatusCode, "Test_setupBasicAuthSuccess")
//}
//func Test_setupBasicAuthFailed(t *testing.T) {
// serverConfigs := testutil.NewServerConfigs(t)
//
// app := gofr.New()
//
// setupBasicAuth(app)
//
// app.GET("/basic-auth-failure", func(_ *gofr.Context) (any, error) {
// return "success", nil
// })
//
// go app.Run()
//
// time.Sleep(100 * time.Millisecond)
//
// var netClient = &http.Client{
// Timeout: 200 * time.Millisecond,
// }
//
// req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
// serverConfigs.HTTPHost + "/basic-auth-failure", http.NoBody)
//
// req.Header.Add("Authorization", encodeBasicAuthorization(t, "username"))
//
// // Send the request and check for successful response
// resp, err := netClient.Do(req)
// if err != nil {
// t.Errorf("error while making HTTP request in Test_BasicAuthMiddleware. err: %v", err)
// return
// }
//
// defer resp.Body.Close()
//
// assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "Test_setupBasicAuthFailed")
//}
================================================
FILE: examples/using-http-service/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:3.23.3
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 9001
CMD ["/main"]
================================================
FILE: examples/using-http-service/main.go
================================================
package main
import (
"encoding/json"
"io"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/service"
)
func main() {
a := gofr.New()
// HTTP service with Circuit Breaker, Health Check, and Connection Pool configuration
// Note: /breeds is not an actual health check endpoint for "https://catfact.ninja"
a.AddHTTPService("cat-facts", "https://catfact.ninja",
&service.ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
},
&service.CircuitBreakerConfig{
Threshold: 4,
Interval: 1 * time.Second,
},
&service.RateLimiterConfig{
Requests: 10,
Window: time.Second,
Burst: 15,
},
&service.HealthConfig{
HealthEndpoint: "breeds",
},
)
// service with connection pool configuration for high-frequency requests
a.AddHTTPService("fact-checker", "https://catfact.ninja",
&service.ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 5,
IdleConnTimeout: 15 * time.Second,
},
&service.HealthConfig{
HealthEndpoint: "breed",
},
)
a.GET("/fact", Handler)
a.Run()
}
func Handler(c *gofr.Context) (any, error) {
var data = struct {
Fact string `json:"fact"`
Length int `json:"length"`
}{}
var catFacts = c.GetHTTPService("cat-facts")
resp, err := catFacts.Get(c, "fact", map[string]any{
"max_length": 20,
})
if err != nil {
return nil, err
}
b, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(b, &data)
if err != nil {
return nil, err
}
return data, nil
}
================================================
FILE: examples/using-http-service/main_test.go
================================================
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
"gofr.dev/pkg/gofr/testutil"
)
var port int
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_main(t *testing.T) {
configs := testutil.NewServerConfigs(t)
c := &http.Client{}
go main()
time.Sleep(100 * time.Millisecond)
testCases := []struct {
desc string
path string
statusCode int
expectedRes string
}{
{
desc: "simple service handler",
path: "/fact",
expectedRes: `{"data":{"fact":"Cats have 3 eyelids.","length":20}}` + "\n",
statusCode: 200,
},
{
desc: "health check",
path: "/.well-known/health",
expectedRes: `{"data":{"cat-facts":{"status":"UP","details":{"host":"catfact.ninja"}},` +
`"fact-checker":{"status":"DOWN","details":{"error":"service down","host":"catfact.ninja"}},` +
`"name":"using-http-service","status":"DEGRADED","version":"dev"}}` + "\n",
statusCode: 200,
},
}
for i, tc := range testCases {
req, _ := http.NewRequest(http.MethodGet, configs.HTTPHost+tc.path, nil)
resp, err := c.Do(req)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expectedRes, string(bodyBytes), "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
resp.Body.Close()
}
}
func TestHTTPHandlerURLError(t *testing.T) {
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet,
fmt.Sprint("http://localhost:", port, "/handle"), bytes.NewBuffer([]byte(`{"key":"value"}`)))
gofrReq := gofrHTTP.NewRequest(req)
mockContainer, mocks := container.NewMockContainer(t)
ctx := &gofr.Context{Context: context.Background(), Request: gofrReq, Container: mockContainer}
ctx.Container.Services = map[string]service.HTTP{"cat-facts": service.NewHTTPService("http://invalid", ctx.Logger, mockContainer.Metrics())}
mocks.Metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), gomock.Any(),
"http://invalid", "method", "GET", "status", gomock.Any())
resp, err := Handler(ctx)
assert.Nil(t, resp)
require.Error(t, err)
}
func TestHTTPHandlerResponseUnmarshalError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{invalid body}`))
}))
defer server.Close()
logger := logging.NewLogger(logging.DEBUG)
req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, fmt.Sprint("http://localhost:", port, "/handle"), bytes.NewBuffer([]byte(`{"key":"value"}`)))
gofrReq := gofrHTTP.NewRequest(req)
ctx := &gofr.Context{Context: context.Background(),
Request: gofrReq, Container: &container.Container{Logger: logger}}
ctx.Container.Services = map[string]service.HTTP{"cat-facts": service.NewHTTPService(server.URL, ctx.Logger, nil)}
resp, err := Handler(ctx)
assert.Nil(t, resp)
require.Error(t, err)
}
================================================
FILE: examples/using-http-service/readme.md
================================================
# Http-Service Example
This GoFr example demonstrates an inter-service HTTP communication along with circuit-breaker as well as
service health config addition.
User can use the `AddHTTPService` method to add an HTTP service and then later get it using `GetHTTPService("service-name")`
### To run the example follow the below steps:
- Make sure your other service and health endpoint is ready and up on the given address.
- Now run the example using below command :
```console
go run main.go
```
================================================
FILE: examples/using-migrations/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 9100
CMD ["/main"]
================================================
FILE: examples/using-migrations/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.118.1 h1:b8RATMcrK9A4BH0rj8yQupPXp+aP+cJ0l6H7V9osV1E=
cloud.google.com/go v0.118.1/go.mod h1:CFO4UPEPi8oV21xoezZCrd3d81K4fFkDTEJu4R8K+9M=
cloud.google.com/go/auth v0.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/iam v1.3.1 h1:KFf8SaT71yYq+sQtRISn90Gyhyf4X8RGgeAVC8XGf3E=
cloud.google.com/go/iam v1.3.1/go.mod h1:3wMtuyT4NcbnYNPLMBzYRFiEfjKfJlLVLrisE7bwm34=
cloud.google.com/go/kms v1.20.5 h1:aQQ8esAIVZ1atdJRxihhdxGQ64/zEbJoJnCz/ydSmKg=
cloud.google.com/go/kms v1.20.5/go.mod h1:C5A8M1sv2YWYy1AE6iSrnddSG9lRGdJq5XEdBy28Lmw=
cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi3uHLg=
cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs=
cloud.google.com/go/pubsub v1.47.0 h1:Ou2Qu4INnf7ykrFjGv2ntFOjVo8Nloh/+OffF4mUu9w=
cloud.google.com/go/pubsub v1.47.0/go.mod h1:LaENesmga+2u0nDtLkIOILskxsfvn/BXX9Ak1NFxOs8=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/XSAM/otelsql v0.36.0 h1:SvrlOd/Hp0ttvI9Hu0FUWtISTTDNhQYwxe8WB4J5zxo=
github.com/XSAM/otelsql v0.36.0/go.mod h1:fo4M8MU+fCn/jDfu+JwTQ0n6myv4cZ+FU5VxrllIlxY=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.34.0 h1:mBFWMaJSNL9RwdGRyEDoAAv8OQc5UlEhLDQggTglU/0=
github.com/alicebob/miniredis/v2 v2.34.0/go.mod h1:kWShP4b58T1CW0Y5dViCd5ztzrDqRWqM3nksiyXk5s8=
github.com/arangodb/go-driver/v2 v2.1.2 h1:3dxx97pjcJPajw4hnHJMXRz2bY/KizUj/ZrlAVEx10Q=
github.com/arangodb/go-driver/v2 v2.1.2/go.mod h1:POYSylTzBPej3qEouU3dSyfdVfo3WxawaRwzhA9mbJ4=
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e h1:Xg+hGrY2LcQBbxd0ZFdbGSyRKTYMZCfBbw/pMJFOk1g=
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:mq7Shfa/CaixoDxiyAAc5jZ6CVBAyPaNQCGS7mkj4Ho=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/kkdai/maglev v0.2.0 h1:w6DCW0kAA6fstZqXkrBrlgIC3jeIRXkjOYea/m6EK/Y=
github.com/kkdai/maglev v0.2.0/go.mod h1:d+mt8Lmt3uqi9aRb/BnPjzD0fy+ETs1vVXiGRnqHVZ4=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0 h1:BIx9TNZH/Jsr4l1i7VVxnV0JPiwYj8qyrHyuL0fGZrk=
github.com/redis/go-redis/extra/rediscmd/v9 v9.7.0/go.mod h1:eTg/YQtGYAZD5r3DlGlJptJ45AHA+/G+2NPn30PKzik=
github.com/redis/go-redis/extra/redisotel/v9 v9.7.0 h1:bQk8xiVFw+3ln4pfELVktpWgYdFpgLLU+quwSoeIof0=
github.com/redis/go-redis/extra/redisotel/v9 v9.7.0/go.mod h1:0LyN+GHLIJmKtjYRPF7nHyTTMV6E91YngoOopNifQRo=
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/segmentio/kafka-go v0.4.47 h1:IqziR4pA3vrZq7YdRxaT3w1/5fvIH5qpCwstUanQQB0=
github.com/segmentio/kafka-go v0.4.47/go.mod h1:HjF6XbOKh0Pjlkr5GVZxt6CsjjwnmhVOfURM5KMd8qg=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.einride.tech/aip v0.68.1 h1:16/AfSxcQISGN5z9C5lM+0mLYXihrHbQ1onvYTr93aQ=
go.einride.tech/aip v0.68.1/go.mod h1:XaFtaj4HuA3Zwk9xoBtTWgNubZ0ZZXv9BZJCkuKuWbg=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0 h1:PS8wXpbyaDJQ2VDHHncMe9Vct0Zn1fEjpsjrLxGJoSc=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.58.0/go.mod h1:HDBUsEjOuRC0EzKZ1bSaRGZWUBAzo+MhAcUUORSr4D0=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0 h1:iQZYNQ7WwIcYXzOPR46FQv9O0dS1PW16RjvR0TjDOe8=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.59.0/go.mod h1:54CaSNqYEXvpzDh8KPjiMVoWm60t5R0dZRt0leEPgAs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0 h1:5pojmb1U1AogINhN3SurB+zm/nIcusopeBNp42f45QM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.33.0/go.mod h1:57gTHJSE5S1tqg+EKsLPlTWhpHMsWlVmer+LA926XiA=
go.opentelemetry.io/otel/exporters/prometheus v0.56.0 h1:GnCIi0QyG0yy2MrJLzVrIM7laaJstj//flf1zEJCG+E=
go.opentelemetry.io/otel/exporters/prometheus v0.56.0/go.mod h1:JQcVZtbIIPM+7SWBB+T6FK+xunlyidwLp++fN0sUaOk=
go.opentelemetry.io/otel/exporters/zipkin v1.34.0 h1:GSjCkoYqsnvUMCjxF18j2tCWH8fhGZYjH3iYgechPTI=
go.opentelemetry.io/otel/exporters/zipkin v1.34.0/go.mod h1:h830hluwAqgSNnZbxL2rJhmAlE7/0SF9esoHVLU04Gc=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-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-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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
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/api v0.220.0 h1:3oMI4gdBgB72WFVwE1nerDD8W3HUOS4kypK6rRLbGns=
google.golang.org/api v0.220.0/go.mod h1:26ZAlY6aN/8WgpCzjPNy18QpYaz7Zgg1h0qe1GkZEmY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4 h1:Pw6WnI9W/LIdRxqK7T6XGugGbHIRl5Q7q3BssH6xk4s=
google.golang.org/genproto v0.0.0-20250122153221-138b5a5a4fd4/go.mod h1:qbZzneIOXSq+KFAFut9krLfRLZiFLzZL5u2t8SV83EE=
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47 h1:5iw9XJTD4thFidQmFVvx0wi4g5yOHk76rNRUxz1ZG5g=
google.golang.org/genproto/googleapis/api v0.0.0-20250124145028-65684f501c47/go.mod h1:AfA77qWLcidQWywD0YgqfpJzf50w2VjzBml3TybHeJU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: examples/using-migrations/main.go
================================================
package main
import (
"errors"
"fmt"
"gofr.dev/examples/using-migrations/migrations"
"gofr.dev/pkg/gofr"
)
const (
queryGetEmployee = "SELECT id,name,gender,contact_number,dob from employee where name = ?"
queryInsertEmployee = "INSERT INTO employee (id, name, gender, contact_number,dob) values (?, ?, ?, ?, ?)"
)
func main() {
// Create a new application
a := gofr.New()
// Add migrations to run
a.Migrate(migrations.All())
// Add all the routes
a.GET("/employee", GetHandler)
a.POST("/employee", PostHandler)
// Run the application
a.Run()
}
type Employee struct {
ID int `json:"id"`
Name string `json:"name"`
Gender string `json:"gender"`
Phone int `json:"contact_number"`
DOB string `json:"dob"`
}
// GetHandler handles GET requests for retrieving employee information
func GetHandler(c *gofr.Context) (any, error) {
name := c.Param("name")
if name == "" {
return nil, errors.New("name can't be empty")
}
var emp Employee
err := c.SQL.QueryRowContext(c, queryGetEmployee, name).
Scan(&emp.ID, &emp.Name, &emp.Gender, &emp.Phone, &emp.DOB)
if err != nil {
return nil, errors.New(fmt.Sprintf("DB Error: %v", err))
}
return emp, nil
}
// PostHandler handles POST requests for creating new employees
func PostHandler(c *gofr.Context) (any, error) {
var emp Employee
if err := c.Bind(&emp); err != nil {
c.Logger.Errorf("error in binding: %v", err)
return nil, errors.New("invalid body")
}
// Execute the INSERT query
_, err := c.SQL.ExecContext(c, queryInsertEmployee, emp.ID, emp.Name, emp.Gender, emp.Phone, emp.DOB)
if err != nil {
return nil, errors.New(fmt.Sprintf("DB Error: %v", err))
}
return fmt.Sprintf("successfully posted entity: %v", emp.Name), nil
}
================================================
FILE: examples/using-migrations/main_test.go
================================================
package main
import (
"bytes"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestExampleMigration(t *testing.T) {
configs := testutil.NewServerConfigs(t)
go main()
time.Sleep(100 * time.Millisecond) // Giving some time to start the server
tests := []struct {
desc string
method string
path string
body []byte
statusCode int
}{
{"post new employee with valid data", http.MethodPost, "/employee",
[]byte(`{"id":2,"name":"John","gender":"Male","contact_number":1234567890,"dob":"2000-01-01"}`), 201},
{"get employee with valid name", http.MethodGet, "/employee?name=John", nil, 200},
{"get employee does not exist", http.MethodGet, "/employee?name=Invalid", nil, 500},
{"get employee with empty name", http.MethodGet, "/employee", nil, http.StatusInternalServerError},
{"post new employee with invalid data", http.MethodPost, "/employee", []byte(`{"id":2"}`),
http.StatusInternalServerError},
{"post new employee with invalid gender", http.MethodPost, "/employee",
[]byte(`{"id":2,"name":"John","gender":"Male123","contact_number":1234567890,"dob":"2000-01-01"}`), 500},
}
for i, tc := range tests {
req, _ := http.NewRequest(tc.method, configs.HTTPHost+tc.path, bytes.NewBuffer(tc.body))
req.Header.Set("content-type", "application/json")
c := http.Client{}
resp, err := c.Do(req)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
================================================
FILE: examples/using-migrations/migrations/1722507126_create_employee_table.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
const createTable = `CREATE TABLE IF NOT EXISTS employee
(
id int not null
primary key,
name varchar(50) not null,
gender varchar(6) not null,
contact_number varchar(10) not null
);`
const employee_date = `INSERT INTO employee (id, name, gender, contact_number) VALUES (1, 'Umang', "M", "0987654321");`
func createTableEmployee() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
_, err := d.SQL.Exec(createTable)
if err != nil {
return err
}
_, err = d.SQL.Exec(employee_date)
if err != nil {
return err
}
_, err = d.SQL.Exec("alter table employee add dob varchar(11) null;")
if err != nil {
return err
}
return nil
},
}
}
================================================
FILE: examples/using-migrations/migrations/1722507126_create_employee_table_test.go
================================================
package migrations
import (
"fmt"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/migration"
)
func TestCreateTableEmployee(t *testing.T) {
tests := []struct {
name string
mockBehaviors func(mock sqlmock.Sqlmock)
expectedError error
}{
{
name: "SuccessfulExecution",
mockBehaviors: func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(employee_date).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("alter table employee add dob varchar(11) null;").WillReturnResult(sqlmock.NewResult(0, 1))
},
expectedError: nil,
},
{
name: "ErrorOnCreateTable",
mockBehaviors: func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnError(fmt.Errorf("create table error"))
},
expectedError: fmt.Errorf("create table error"),
},
{
name: "ErrorOnInsert",
mockBehaviors: func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(employee_date).WillReturnError(fmt.Errorf("insert error"))
},
expectedError: fmt.Errorf("insert error"),
},
{
name: "ErrorOnAlterTable",
mockBehaviors: func(mock sqlmock.Sqlmock) {
mock.ExpectExec(createTable).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec(employee_date).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("alter table employee add dob varchar(11) null;").WillReturnError(fmt.Errorf("alter table error"))
},
expectedError: fmt.Errorf("alter table error"),
},
}
for i, tc := range tests {
// Create mock database and datasource
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
require.NoError(t, err)
defer db.Close()
datasource := migration.Datasource{SQL: db}
// Set mock expectations
tc.mockBehaviors(mock)
// Execute the migration
err = createTableEmployee().UP(datasource)
assert.Equal(t, tc.expectedError, err, "TEST[%d] failed! Desc : %v", i, tc.name)
require.NoError(t, mock.ExpectationsWereMet())
}
}
================================================
FILE: examples/using-migrations/migrations/1722507180_redis_add_employee_name.go
================================================
package migrations
import (
"context"
"gofr.dev/pkg/gofr/migration"
)
func addEmployeeInRedis() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
err := d.Redis.Set(context.Background(), "Umang", "0987654321", 0).Err()
if err != nil {
return err
}
return nil
},
}
}
================================================
FILE: examples/using-migrations/migrations/1722507180_redis_add_employee_name_test.go
================================================
package migrations
import (
"errors"
"testing"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/migration"
)
func TestAddEmployeeInRedis(t *testing.T) {
client, mock := redismock.NewClientMock()
datasource := migration.Datasource{Redis: client}
// Set expectations for the Set method
mock.ExpectSet("Umang", "0987654321", 0).SetVal("OK")
// Call the UP method of the migration
err := addEmployeeInRedis().UP(datasource)
require.NoError(t, err)
}
func TestAddEmployeeInRedis_Error(t *testing.T) {
client, mock := redismock.NewClientMock()
datasource := migration.Datasource{Redis: client}
mock.ExpectSet("Umang", "0987654321", 0).SetErr(errors.New("redis error"))
// Call the UP method of the migration
err := addEmployeeInRedis().UP(datasource)
if err == nil || err.Error() != "redis error" {
t.Errorf("TestAddEmployeeInRedis Error failed! unexpected error: %v", err)
}
}
================================================
FILE: examples/using-migrations/migrations/all.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
1722507126: createTableEmployee(),
1722507180: addEmployeeInRedis(),
}
}
================================================
FILE: examples/using-migrations/migrations/all_test.go
================================================
package migrations
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
func TestAll(t *testing.T) {
// Get the map of migrations
allMigrations := All()
expected := map[int64]migration.Migrate{
1722507126: createTableEmployee(),
1722507180: addEmployeeInRedis(),
}
// Check if the length of the maps match
assert.Equal(t, len(expected), len(allMigrations), "TestAll Failed!")
}
================================================
FILE: examples/using-migrations/readme.md
================================================
# Migrations Example
This GoFr example demonstrates the use of `migrations` through a simple HTTP server using MySQL, Redis and Kafka.
### To run the example follow the below steps:
- Run the docker image of MySQL, Redis and Kafka
```console
docker run --name gofr-mysql -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=test -p 2001:3306 -d mysql:8.0.30
docker run --name gofr-redis -p 2002:6379 -d redis:7.0.5
docker run --name kafka-1 -p 9092:9092 \
-e KAFKA_ENABLE_KRAFT=yes \
-e KAFKA_CFG_PROCESS_ROLES=broker,controller \
-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e ALLOW_PLAINTEXT_LISTENER=yes \
-e KAFKA_CFG_NODE_ID=1 \
-v kafka_data:/bitnami \
bitnami/kafka:3.4
```
- Now run the example using below command :
```console
go run main.go
```
================================================
FILE: examples/using-publisher/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 8100
CMD ["/main"]
================================================
FILE: examples/using-publisher/main.go
================================================
package main
import (
"encoding/json"
"gofr.dev/examples/using-publisher/migrations"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.Migrate(migrations.All())
app.POST("/publish-order", order)
app.POST("/publish-product", product)
app.Run()
}
func order(ctx *gofr.Context) (any, error) {
type orderStatus struct {
OrderId string `json:"orderId"`
Status string `json:"status"`
}
var data orderStatus
err := ctx.Bind(&data)
if err != nil {
return nil, err
}
msg, _ := json.Marshal(data)
err = ctx.GetPublisher().Publish(ctx, "order-logs", msg)
if err != nil {
return nil, err
}
return "Published", nil
}
func product(ctx *gofr.Context) (any, error) {
type productInfo struct {
ProductId string `json:"productId"`
Price string `json:"price"`
}
var data productInfo
err := ctx.Bind(&data)
if err != nil {
return nil, err
}
msg, _ := json.Marshal(data)
err = ctx.GetPublisher().Publish(ctx, "products", msg)
if err != nil {
return nil, err
}
return "Published", nil
}
================================================
FILE: examples/using-publisher/main_test.go
================================================
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"gofr.dev/pkg/gofr/testutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestExamplePublisherError(t *testing.T) {
t.Setenv("PUBSUB_BROKER", "localhost:1092")
configs := testutil.NewServerConfigs(t)
host := fmt.Sprint("http://localhost:", configs.HTTPPort)
go main()
time.Sleep(200 * time.Millisecond)
testCases := []struct {
desc string
path string
body []byte
expectedStatusCode int
}{
{"valid order", "/publish-order", []byte(`{"data":{"orderId":"123","status":"pending"}}`), http.StatusInternalServerError},
{"valid product", "/publish-product", []byte(`{"data":{"productId":"123","price":"599"}}`), http.StatusInternalServerError},
}
client := http.Client{}
for i, tc := range testCases {
req, _ := http.NewRequest(http.MethodPost, host+tc.path, bytes.NewBuffer(tc.body))
req.Header.Set("content-type", "application/json")
resp, err := client.Do(req)
require.NoError(t, err, "TEST[%d] %s failed", i, tc.desc)
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode, "TEST[%d] %s failed", i, tc.desc)
}
}
func TestOrderFunction(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tests := []struct {
name string
body string
expectError bool
publishError error
expectedResult interface{}
}{
{"valid order", `{"orderId":"123","status":"pending"}`, false, nil, "Published"},
{"invalid JSON", `{"orderId":,"status":"pending"}`, true, nil, nil},
{"publish error", `{"orderId":"123","status":"pending"}`, true, fmt.Errorf("publish failed"), nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
switch {
case tt.publishError != nil:
mocks.PubSub.EXPECT().Publish(gomock.Any(), "order-logs", gomock.Any()).Return(tt.publishError)
case !tt.expectError:
mocks.PubSub.EXPECT().Publish(gomock.Any(), "order-logs", gomock.Any()).Return(nil)
}
testHandler(t, tt.name, order, mockContainer, tt.body, tt.expectError, tt.expectedResult)
})
}
}
func TestProductFunction(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tests := []struct {
name string
body string
expectError bool
publishError error
expectedResult interface{}
}{
{"valid product", `{"productId":"123","price":"599"}`, false, nil, "Published"},
{"invalid JSON", `{"productId":,"price":"599"}`, true, nil, nil},
{"publish error", `{"productId":"123","price":"599"}`, true, fmt.Errorf("publish failed"), nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
switch {
case tt.publishError != nil:
mocks.PubSub.EXPECT().Publish(gomock.Any(), "products", gomock.Any()).Return(tt.publishError)
case !tt.expectError:
mocks.PubSub.EXPECT().Publish(gomock.Any(), "products", gomock.Any()).Return(nil)
}
testHandler(t, tt.name, product, mockContainer, tt.body, tt.expectError, tt.expectedResult)
})
}
}
func testHandler(t *testing.T, name string, handler func(*gofr.Context) (interface{}, error),
container *container.Container, body string, expectError bool, expectedResult interface{}) {
t.Run(name, func(t *testing.T) {
ctx := &gofr.Context{
Context: context.Background(),
Request: &testRequest{Request: httptest.NewRequest(http.MethodPost, "/", bytes.NewReader([]byte(body))), body: body},
Container: container,
ContextLogger: *logging.NewContextLogger(context.Background(), container.Logger),
}
result, err := handler(ctx)
assert.Equal(t, expectError, err != nil, "error presence mismatch")
assert.Equal(t, expectedResult, result, "result mismatch")
})
}
type testRequest struct {
*http.Request
body string
}
func (r *testRequest) Bind(v interface{}) error {
return json.Unmarshal([]byte(r.body), v)
}
func (r *testRequest) Param(key string) string { return r.URL.Query().Get(key) }
func (r *testRequest) PathParam(key string) string { return "" }
func (r *testRequest) HostName() string { return r.Host }
func (r *testRequest) Params(key string) []string { return r.URL.Query()[key] }
================================================
FILE: examples/using-publisher/migrations/1721801313_create_topics.go
================================================
package migrations
import (
"context"
"gofr.dev/pkg/gofr/migration"
)
func createTopics() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
err := d.PubSub.CreateTopic(context.Background(), "products")
if err != nil {
return err
}
return d.PubSub.CreateTopic(context.Background(), "order-logs")
},
}
}
================================================
FILE: examples/using-publisher/migrations/1721801313_create_topics_test.go
================================================
package migrations
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
// MockPubSub implements the PubSub interface for testing
type MockPubSub struct {
Calls []string
ErrOnTopic map[string]error
}
func (*MockPubSub) Query(_ context.Context, _ string, _ ...any) ([]byte, error) {
return []byte{}, nil
}
func (*MockPubSub) DeleteTopic(_ context.Context, _ string) error {
return nil
}
func (m *MockPubSub) CreateTopic(_ context.Context, topic string) error {
m.Calls = append(m.Calls, topic)
if err, ok := m.ErrOnTopic[topic]; ok {
return err
}
return nil
}
func TestCreateTopics(t *testing.T) {
tests := []struct {
name string
errOnProduct error
errOnOrder error
expectedErr error
expectedCalls []string
}{
{"success", nil, nil, nil, []string{"products", "order-logs"}},
{"error on products", errors.New("fail products"), nil,
errors.New("fail products"), []string{"products"}},
{"error on order-logs", nil, errors.New("fail order"), errors.New("fail order"), []string{"products", "order-logs"}},
}
for _, tt := range tests {
mockPubSub := &MockPubSub{
ErrOnTopic: map[string]error{
"products": tt.errOnProduct,
"order-logs": tt.errOnOrder,
},
}
ds := migration.Datasource{PubSub: mockPubSub}
err := createTopics().UP(ds)
assert.Equal(t, tt.expectedErr, err, tt.name)
assert.Equal(t, tt.expectedCalls, mockPubSub.Calls, tt.name)
}
}
================================================
FILE: examples/using-publisher/migrations/all.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
1721801313: createTopics(),
}
}
================================================
FILE: examples/using-publisher/migrations/all_test.go
================================================
package migrations
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
func TestAll(t *testing.T) {
// Get the map of migrations
allMigrations := All()
expected := map[int64]migration.Migrate{
1721801313: createTopics(),
}
// Check if the length of the maps match
assert.Equal(t, len(expected), len(allMigrations), "TestAll Failed!")
}
================================================
FILE: examples/using-publisher/readme.md
================================================
# Publisher Example
This GoFr example demonstrates a simple Publisher that publishes to the given topic when an HTTP request is made to it's
matching route.
### To run the example follow the below steps:
- Run the docker image of Kafka and ensure that your provided topics are created before publishing
```console
docker run --name kafka-1 -p 9092:9092 \
-e KAFKA_ENABLE_KRAFT=yes \
-e KAFKA_CFG_PROCESS_ROLES=broker,controller \
-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e ALLOW_PLAINTEXT_LISTENER=yes \
-e KAFKA_CFG_NODE_ID=1 \
-v kafka_data:/bitnami \
bitnami/kafka:3.4
```
- Now run the example using below command :
```console
go run main.go
```
================================================
FILE: examples/using-subscriber/Dockerfile
================================================
FROM golang:1.24
RUN mkdir /src/
WORKDIR /src/
COPY . .
RUN go get ./...
RUN go build -ldflags "-linkmode external -extldflags -static" -a main.go
FROM alpine:latest
RUN apk add --no-cache tzdata ca-certificates
COPY --from=0 /src/main /main
COPY --from=0 /src/configs /configs
EXPOSE 8200
CMD ["/main"]
================================================
FILE: examples/using-subscriber/main.go
================================================
package main
import (
"gofr.dev/examples/using-subscriber/migrations"
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.Migrate(migrations.All())
app.Subscribe("products", productHandler)
app.Subscribe("order-logs", orderHandler)
app.Run()
}
func productHandler(c *gofr.Context) error {
var productInfo struct {
ProductId string `json:"productId"`
Price string `json:"price"`
}
err := c.Bind(&productInfo)
if err != nil {
c.Logger.Error(err)
return nil
}
c.Logger.Info("Received product", productInfo)
return nil
}
func orderHandler(c *gofr.Context) error {
var orderStatus struct {
OrderId string `json:"orderId"`
Status string `json:"status"`
}
err := c.Bind(&orderStatus)
if err != nil {
c.Logger.Error(err)
return nil
}
c.Logger.Info("Received order", orderStatus)
return nil
}
================================================
FILE: examples/using-subscriber/main_test.go
================================================
package main
import (
"context"
"errors"
"os"
"strings"
"testing"
"time"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestMainInitialization(t *testing.T) {
log := testutil.StdoutOutputForFunc(func() {
go main()
time.Sleep(200 * time.Millisecond)
})
expectedLog := "connected to 1 Kafka brokers"
if !strings.Contains(log, expectedLog) {
t.Errorf("Expected log to contain %q, but got: %s", expectedLog, log)
}
}
type errorRequest struct{}
func (e *errorRequest) Context() context.Context {
return context.Background()
}
func (e *errorRequest) Bind(v interface{}) error { return errors.New("bind error") }
func (e *errorRequest) Param(key string) string { return "" }
func (e *errorRequest) PathParam(key string) string { return "" }
func (e *errorRequest) HostName() string { return "" }
func (e *errorRequest) Params(key string) []string { return nil }
func TestProductSubscribe_BindError(t *testing.T) {
mockContainer, _ := container.NewMockContainer(t)
ctx := &gofr.Context{
Request: &errorRequest{},
Container: mockContainer,
ContextLogger: *logging.NewContextLogger(context.Background(), mockContainer.Logger),
}
err := productHandler(ctx)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
}
func TestOrderSubscribe_BindError(t *testing.T) {
mockContainer, _ := container.NewMockContainer(t)
ctx := &gofr.Context{
Request: &errorRequest{},
Container: mockContainer,
ContextLogger: *logging.NewContextLogger(context.Background(), mockContainer.Logger),
}
err := orderHandler(ctx)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
}
type successProductRequest struct{}
func (r *successProductRequest) Context() context.Context {
return context.Background()
}
func (r *successProductRequest) Bind(v interface{}) error {
p := v.(*struct {
ProductId string `json:"productId"`
Price string `json:"price"`
})
p.ProductId = "123"
p.Price = "599"
return nil
}
func (r *successProductRequest) Param(string) string { return "" }
func (r *successProductRequest) PathParam(string) string { return "" }
func (r *successProductRequest) HostName() string { return "" }
func (r *successProductRequest) Params(string) []string { return nil }
func TestProductHandler_Success(t *testing.T) {
mockContainer, _ := container.NewMockContainer(t)
ctx := &gofr.Context{
Request: &successProductRequest{},
Container: mockContainer,
ContextLogger: *logging.NewContextLogger(context.Background(), mockContainer.Logger),
}
err := productHandler(ctx)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
}
type successOrderRequest struct{}
func (r *successOrderRequest) Context() context.Context {
return context.Background()
}
func (r *successOrderRequest) Bind(v interface{}) error {
o := v.(*struct {
OrderId string `json:"orderId"`
Status string `json:"status"`
})
o.OrderId = "456"
o.Status = "pending"
return nil
}
func (r *successOrderRequest) Param(string) string { return "" }
func (r *successOrderRequest) PathParam(string) string { return "" }
func (r *successOrderRequest) HostName() string { return "" }
func (r *successOrderRequest) Params(string) []string { return nil }
func TestOrderHandler_Success(t *testing.T) {
mockContainer, _ := container.NewMockContainer(t)
ctx := &gofr.Context{
Request: &successOrderRequest{},
Container: mockContainer,
ContextLogger: *logging.NewContextLogger(context.Background(), mockContainer.Logger),
}
err := orderHandler(ctx)
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
}
================================================
FILE: examples/using-subscriber/migrations/1721800255_create_topics.go
================================================
package migrations
import (
"context"
"gofr.dev/pkg/gofr/migration"
)
func createTopics() migration.Migrate {
return migration.Migrate{
UP: func(d migration.Datasource) error {
err := d.PubSub.CreateTopic(context.Background(), "products")
if err != nil {
return err
}
return d.PubSub.CreateTopic(context.Background(), "order-logs")
},
}
}
================================================
FILE: examples/using-subscriber/migrations/1721800255_create_topics_test.go
================================================
package migrations
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
// MockPubSub implements the PubSub interface for testing
type MockPubSub struct {
Calls []string
ErrOnTopic map[string]error
}
func (*MockPubSub) Query(_ context.Context, _ string, _ ...any) ([]byte, error) {
return []byte{}, nil
}
func (*MockPubSub) DeleteTopic(_ context.Context, _ string) error {
return nil
}
func (m *MockPubSub) CreateTopic(_ context.Context, topic string) error {
m.Calls = append(m.Calls, topic)
if err, ok := m.ErrOnTopic[topic]; ok {
return err
}
return nil
}
func TestCreateTopics(t *testing.T) {
tests := []struct {
name string
errOnProduct error
errOnOrder error
expectedErr error
expectedCalls []string
}{
{"success", nil, nil, nil, []string{"products", "order-logs"}},
{"error on products", errors.New("fail products"), nil,
errors.New("fail products"), []string{"products"}},
{"error on order-logs", nil, errors.New("fail order"),
errors.New("fail order"), []string{"products", "order-logs"}},
}
for _, tt := range tests {
mockPubSub := &MockPubSub{
ErrOnTopic: map[string]error{
"products": tt.errOnProduct,
"order-logs": tt.errOnOrder,
},
}
ds := migration.Datasource{PubSub: mockPubSub}
err := createTopics().UP(ds)
assert.Equal(t, tt.expectedErr, err, tt.name)
assert.Equal(t, tt.expectedCalls, mockPubSub.Calls, tt.name)
}
}
================================================
FILE: examples/using-subscriber/migrations/all.go
================================================
package migrations
import (
"gofr.dev/pkg/gofr/migration"
)
func All() map[int64]migration.Migrate {
return map[int64]migration.Migrate{
1721800255: createTopics(),
}
}
================================================
FILE: examples/using-subscriber/migrations/all_test.go
================================================
package migrations
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/migration"
)
func TestAll(t *testing.T) {
// Get the map of migrations
allMigrations := All()
expected := map[int64]migration.Migrate{
1721800255: createTopics(),
}
// Check if the length of the maps match
assert.Equal(t, len(expected), len(allMigrations), "TestAll Failed!")
}
================================================
FILE: examples/using-subscriber/readme.md
================================================
# Subscriber Example
This GoFr example demonstrates a simple Subscriber that subscribes asynchronously to a given topic and commits based
on the handler response.
### To run the example follow the below steps:
- Run the docker image of kafka and ensure that your provided topics are created before subscribing.
```console
docker run --name kafka-1 -p 9092:9092 \
-e KAFKA_ENABLE_KRAFT=yes \
-e KAFKA_CFG_PROCESS_ROLES=broker,controller \
-e KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER \
-e KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093 \
-e KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT \
-e KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092 \
-e KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true \
-e KAFKA_BROKER_ID=1 \
-e KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093 \
-e ALLOW_PLAINTEXT_LISTENER=yes \
-e KAFKA_CFG_NODE_ID=1 \
-v kafka_data:/bitnami \
bitnami/kafka:3.4
```
- Now run the example using below command :
```console
go run main.go
```
================================================
FILE: examples/using-web-socket/Readme.md
================================================
# Using WebSocket in GoFr
This example demonstrates how to create and handle a WebSocket connection using the GoFr framework.
It covers establishing the connection, receiving messages from the client, and sending responses back to the client in real time.
---
## Overview
The `/ws` endpoint in this example:
* Accepts incoming WebSocket connections.
* Reads and logs messages sent by the client.
* Sends a fixed greeting message back to the client (`"Hello! GoFr"`).
* Returns the received message as part of the response.
This is useful for building **real-time applications** such as chat systems, dashboards, and live notifications.
---
## How to Run
1. **Clone the repository** and navigate to the example folder:
```bash
git clone https://github.com/gofr-dev/gofr.git
cd gofr/examples/using-web-socket
```
2. **Start the application**:
```bash
go run main.go
```
This will start the server on:
```
ws://localhost:8001/ws
```
---
## How It Works
### main.go
```go
app := gofr.New()
app.WebSocket("/ws", WSHandler)
app.Run()
```
* Creates a new GoFr app.
* Registers the `/ws` route for WebSocket connections.
* Starts the server.
#### WSHandler
* Binds the incoming WebSocket message to a string.
* Logs the received message.
* Sends `"Hello! GoFr"` back to the client.
* Returns the received message.
---
## Testing
The example includes a test file `main_test.go` which:
* Starts the WebSocket server.
* Connects using a `gorilla/websocket` client.
* Sends a test message.
* Reads the server’s response.
* Asserts that the response matches the expected message.
Run the test:
```bash
go test -v
```
Expected output:
```
=== RUN Test_WebSocket_Success
--- PASS: Test_WebSocket_Success (0.10s)
PASS
```
---
## Example Usage
Using [wscat](https://github.com/websockets/wscat):
```bash
npm install -g wscat
wscat -c ws://localhost:8001/ws
> Hello from Client
< Hello! GoFr
```
================================================
FILE: examples/using-web-socket/main.go
================================================
package main
import (
"gofr.dev/pkg/gofr"
)
func main() {
app := gofr.New()
app.WebSocket("/ws", WSHandler)
app.Run()
}
func WSHandler(ctx *gofr.Context) (any, error) {
var message string
err := ctx.Bind(&message)
if err != nil {
ctx.Logger.Errorf("Error binding message: %v", err)
return nil, err
}
ctx.Logger.Infof("Received message: %s", message)
err = ctx.WriteMessageToSocket("Hello! GoFr")
if err != nil {
return nil, err
}
return message, nil
}
================================================
FILE: examples/using-web-socket/main_test.go
================================================
package main
import (
"fmt"
"os"
"testing"
"time"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_WebSocket_Success(t *testing.T) {
configs := testutil.NewServerConfigs(t)
wsURL := fmt.Sprintf("ws://localhost:%d/ws", configs.HTTPPort)
go main()
time.Sleep(100 * time.Millisecond)
testMessage := "Hello! GoFr"
dialer := &websocket.Dialer{}
conn, _, err := dialer.Dial(wsURL, nil)
assert.Nil(t, err, "Error dialing websocket : %v", err)
defer conn.Close()
// writing test message to websocket connection
err = conn.WriteMessage(websocket.TextMessage, []byte(testMessage))
assert.Nil(t, err, "Unexpected error while writing message : %v", err)
// Read response from server
_, message, err := conn.ReadMessage()
assert.Nil(t, err, "Unexpected error while reading message : %v", err)
assert.Equal(t, string(message), testMessage, "Test_WebSocket_Success Failed!")
}
================================================
FILE: go.mod
================================================
module gofr.dev
go 1.25.0
require (
cloud.google.com/go/pubsub v1.50.1
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/XSAM/otelsql v0.41.0
github.com/alicebob/miniredis/v2 v2.37.0
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/go-redis/redismock/v9 v9.2.0
github.com/go-sql-driver/mysql v1.9.3
github.com/gogo/protobuf v1.3.2
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.3
github.com/graphql-go/graphql v0.8.1
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.11.2
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/otlptranslator v1.0.0
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0
github.com/redis/go-redis/v9 v9.18.0
github.com/segmentio/kafka-go v0.4.50
github.com/stretchr/testify v1.11.1
github.com/vektah/gqlparser/v2 v2.5.32
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0
go.opentelemetry.io/otel/exporters/prometheus v0.64.0
go.opentelemetry.io/otel/exporters/zipkin v1.42.0
go.opentelemetry.io/otel/metric v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/sdk/metric v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sync v0.20.0
golang.org/x/term v0.41.0
golang.org/x/text v0.35.0
golang.org/x/time v0.15.0
google.golang.org/api v0.272.0
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.46.2
)
require (
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.einride.tech/aip v0.73.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sys v0.42.0 // indirect
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
================================================
FILE: go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8=
github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d h1:abDbP7XBVgwda+h0J5Qra5p2OQpidU2FdkXvzCKL+H8=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d/go.mod h1:wKFzULXAPj3U2BDAPWXhSbQQNC6FU1+1/5iika6IY7g=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0 h1:Z7ARHF7193vyVltPYcmuhSKPLf8dP5rtJZLtTQnbMH4=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0/go.mod h1:DW09+gaEg5kydlb9g8kp4Nos3yqo9YSA1uHXkeJihXc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
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/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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.2 h1:gkXQ6R0+AjxFC/fTDaeIVLbNLNrRoOK7YYVz5BKhTcE=
modernc.org/sqlite v1.46.2/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: go.work
================================================
go 1.25.1
use (
.
./examples/using-add-filestore
./pkg/gofr/datasource/arangodb
./pkg/gofr/datasource/cassandra
./pkg/gofr/datasource/clickhouse
./pkg/gofr/datasource/couchbase
./pkg/gofr/datasource/dbresolver
./pkg/gofr/datasource/dgraph
./pkg/gofr/datasource/elasticsearch
./pkg/gofr/datasource/file/azure
./pkg/gofr/datasource/file/ftp
./pkg/gofr/datasource/file/gcs
./pkg/gofr/datasource/file/s3
./pkg/gofr/datasource/file/sftp
./pkg/gofr/datasource/influxdb
./pkg/gofr/datasource/kv-store/badger
./pkg/gofr/datasource/kv-store/dynamodb
./pkg/gofr/datasource/kv-store/nats
./pkg/gofr/datasource/mongo
./pkg/gofr/datasource/opentsdb
./pkg/gofr/datasource/oracle
./pkg/gofr/datasource/pubsub/eventhub
./pkg/gofr/datasource/pubsub/nats
./pkg/gofr/datasource/pubsub/sqs
./pkg/gofr/datasource/scylladb
./pkg/gofr/datasource/solr
./pkg/gofr/datasource/surrealdb
)
================================================
FILE: go.work.sum
================================================
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1 h1:YhMSc48s25kr7kv31Z8vf7sPUIq5YJva9z1mn/hAt0M=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250425153114-8976f5be98c1.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U=
buf.build/go/protovalidate v0.12.0 h1:4GKJotbspQjRCcqZMGVSuC8SjwZ/FmgtSuKDpKUTZew=
buf.build/go/protovalidate v0.12.0/go.mod h1:q3PFfbzI05LeqxSwq+begW2syjy2Z6hLxZSkP1OH/D0=
cel.dev/expr v0.15.0/go.mod h1:TRSuuV7DlVCE/uwv5QbAiW/v8l5O8C4eEPHeu7gf7Sg=
cel.dev/expr v0.20.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
cel.dev/expr v0.23.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
cloud.google.com/go v0.118.3/go.mod h1:Lhs3YLnBlwJ4KA6nuObNMZ/fCbOQBPuWKPoE0Wa/9Vc=
cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q=
cloud.google.com/go/accessapproval v1.8.6 h1:UkmDPCKvj24bkGVrvgJPcgSDkmIPw/bAmOiDb9avOiE=
cloud.google.com/go/accessapproval v1.8.6/go.mod h1:FfmTs7Emex5UvfnnpMkhuNkRCP85URnBFt5ClLxhZaQ=
cloud.google.com/go/accessapproval v1.8.7 h1:Sc9ZjxFBEM/PoAxNlUwVGDcv8DYyjLYWDxHlzPG0q5I=
cloud.google.com/go/accessapproval v1.8.7/go.mod h1:BFvZOW4GJjJnl6aA/YDEg0TGViFHyusa/bMdcVFmh8A=
cloud.google.com/go/accessapproval v1.8.8 h1:gq8OS+rQWgGRo91D2qztN+ion6AZ2T1CxBIu0ifCmVo=
cloud.google.com/go/accessapproval v1.8.8/go.mod h1:RFwPY9JDKseP4gJrX1BlAVsP5O6kI8NdGlTmaeDefmk=
cloud.google.com/go/accesscontextmanager v1.9.6 h1:2LnncRqfYB8NEdh9+FeYxAt9POTW/0zVboktnRlO11w=
cloud.google.com/go/accesscontextmanager v1.9.6/go.mod h1:884XHwy1AQpCX5Cj2VqYse77gfLaq9f8emE2bYriilk=
cloud.google.com/go/accesscontextmanager v1.9.7 h1:aKIfg7Jyc73pe8bzx0zypNdS5gfFdSvFvB8YNA9k2kA=
cloud.google.com/go/accesscontextmanager v1.9.7/go.mod h1:i6e0nd5CPcrh7+YwGq4bKvju5YB9sgoAip+mXU73aMM=
cloud.google.com/go/aiplatform v1.89.0 h1:niSJYc6ldWWVM9faXPo1Et1MVSQoLvVGriD7fwbJdtE=
cloud.google.com/go/aiplatform v1.89.0/go.mod h1:TzZtegPkinfXTtXVvZZpxx7noINFMVDrLkE7cEWhYEk=
cloud.google.com/go/aiplatform v1.102.0 h1:UWw1hrxIFoXeooNdJSjTJyHAcIf67OwyVoqcpdScVoA=
cloud.google.com/go/aiplatform v1.102.0/go.mod h1:4rwKOMdubQOND81AlO3EckcskvEFCYSzXKfn42GMm8k=
cloud.google.com/go/aiplatform v1.109.0 h1:A1on/tr2Y7EwhW4M1dtuVMWioxFD5oiacZ85MOi9HH8=
cloud.google.com/go/aiplatform v1.109.0/go.mod h1:4rwKOMdubQOND81AlO3EckcskvEFCYSzXKfn42GMm8k=
cloud.google.com/go/aiplatform v1.114.0 h1:TCrSLci+NFEAx0PZMv8btGe5j68RivArmDJbBLIc/3o=
cloud.google.com/go/aiplatform v1.114.0/go.mod h1:W5yMrpIuHG/CSK8iF7XnwIfCJu6dcLRQ0cTqGR5vwwE=
cloud.google.com/go/analytics v0.28.1 h1:W2ft49J/LeEj9A07Jsd5Q2kAzajK0j0IffOyyzbxw04=
cloud.google.com/go/analytics v0.28.1/go.mod h1:iPaIVr5iXPB3JzkKPW1JddswksACRFl3NSHgVHsuYC4=
cloud.google.com/go/analytics v0.30.0 h1:9PvoT9SvNIHRqTZye7+WudvO2vr4PiZ9mLhiOtEE7Eo=
cloud.google.com/go/analytics v0.30.0/go.mod h1:dneJtsGmmK6EkEPg59vRlncKFWt3xzmKNOc9aKXCTrI=
cloud.google.com/go/analytics v0.30.1 h1:souLxu9tQHzF+0NDpKoIw4pl2WQ9K2JfkdPPs36BfXw=
cloud.google.com/go/analytics v0.30.1/go.mod h1:V/FnINU5kMOsttZnKPnXfKi6clJUHTEXUKQjHxcNK8A=
cloud.google.com/go/apigateway v1.7.6 h1:do+u3rjDYuTxD2ypRfv4uwTMoy/VHFLclvaYcb5Mv6I=
cloud.google.com/go/apigateway v1.7.6/go.mod h1:SiBx36VPjShaOCk8Emf63M2t2c1yF+I7mYZaId7OHiA=
cloud.google.com/go/apigateway v1.7.7 h1:ehKUTy+QFsb3n07fEi18S2dpDDjCV4UlRyrbwfZV3Zk=
cloud.google.com/go/apigateway v1.7.7/go.mod h1:j1bCmrUK1BzVHpiIyTApxB7cRyhivKzltqLmp6j6i7U=
cloud.google.com/go/apigeeconnect v1.7.6 h1:ijEJSni5xROOn1YyiHgqcW0B0TWr0di9VgIi2gvyNjY=
cloud.google.com/go/apigeeconnect v1.7.6/go.mod h1:zqDhHY99YSn2li6OeEjFpAlhXYnXKl6DFb/fGu0ye2w=
cloud.google.com/go/apigeeconnect v1.7.7 h1:S6s2zojwMymx0fyZYKm0eK1TdDxrriIBAlNVvRAOzug=
cloud.google.com/go/apigeeconnect v1.7.7/go.mod h1:ftGK3nca0JePiVLl0A6alaMjKdOc5C+sAkFMyH2RH8U=
cloud.google.com/go/apigeeregistry v0.9.6 h1:TgdjAoGoRY81DEc2LYsYvi/OqCFImMzAk/TVKiSRsQw=
cloud.google.com/go/apigeeregistry v0.9.6/go.mod h1:AFEepJBKPtGDfgabG2HWaLH453VVWWFFs3P4W00jbPs=
cloud.google.com/go/apigeeregistry v0.10.0 h1:QziFVsuPU2lhy40Ht9uWEyciV23SH9GETWiwcu3qzdg=
cloud.google.com/go/apigeeregistry v0.10.0/go.mod h1:SAlF5OhKvyLDuwWAaFAIVJjrEqKRrGTPkJs+TWNnSqg=
cloud.google.com/go/appengine v1.9.6 h1:JJyY8icMmQeWfQ+d36IhkGvd3Guzvw0UAkvxT0wmUx8=
cloud.google.com/go/appengine v1.9.6/go.mod h1:jPp9T7Opvzl97qytaRGPwoH7pFI3GAcLDaui1K8PNjY=
cloud.google.com/go/appengine v1.9.7 h1:IxGz6j5xv0nTJX285wu95Vn6KEi2CeV9vbyRgCSEAoU=
cloud.google.com/go/appengine v1.9.7/go.mod h1:y1XpGVeAhbsNzHida79cHbr3pFRsym0ob8xnC8yphbo=
cloud.google.com/go/area120 v0.9.6 h1:iJrZ6AleZr4l+q0/fWVANFOhs90KiSB1Ccait5OYyNg=
cloud.google.com/go/area120 v0.9.6/go.mod h1:qKSokqe0iTmwBDA3tbLWonMEnh0pMAH4YxiceiHUed4=
cloud.google.com/go/area120 v0.9.7 h1:BbpzLwaIXVPorrrzTH+ni7P5mLemmPPfSZ7o39k7zQc=
cloud.google.com/go/area120 v0.9.7/go.mod h1:5nJ0yksmjOMfc4Zpk+okWfJ3A1004FvB82rfia+ZLaY=
cloud.google.com/go/artifactregistry v1.17.1 h1:A20kj2S2HO9vlyBVyVFHPxArjxkXvLP5LjcdE7NhaPc=
cloud.google.com/go/artifactregistry v1.17.1/go.mod h1:06gLv5QwQPWtaudI2fWO37gfwwRUHwxm3gA8Fe568Hc=
cloud.google.com/go/artifactregistry v1.17.2 h1:Gx5vsnIFEx+obM1VdtMF2AuTraYESRCrBxvc9+6jBZg=
cloud.google.com/go/artifactregistry v1.17.2/go.mod h1:h4CIl9TJZskg9c9u1gC9vTsOTo1PrAnnxntprqS3AjM=
cloud.google.com/go/artifactregistry v1.19.0 h1:DaOHWeURq93K27/6Sa2fy3rJoftrVXKeT3tonM4fxtI=
cloud.google.com/go/artifactregistry v1.19.0/go.mod h1:UEAPCgHDFC1q+A8nnVxXHPEy9KCVOeavFBF1fEChQvU=
cloud.google.com/go/asset v1.21.1 h1:i55wWC/EwVdHMyJgRfbLp/L6ez4nQuOpZwSxkuqN9ek=
cloud.google.com/go/asset v1.21.1/go.mod h1:7AzY1GCC+s1O73yzLM1IpHFLHz3ws2OigmCpOQHwebk=
cloud.google.com/go/asset v1.22.0 h1:81Ru5hjHfiGtk+u/Ix69eaWieKpvm7Ce7UHtcZhOLbk=
cloud.google.com/go/asset v1.22.0/go.mod h1:q80JP2TeWWzMCazYnrAfDf36aQKf1QiKzzpNLflJwf8=
cloud.google.com/go/assuredworkloads v1.12.6 h1:ip/shfJYx6lrHBWYADjrrrubcm7uZzy50TTF5tPG7ek=
cloud.google.com/go/assuredworkloads v1.12.6/go.mod h1:QyZHd7nH08fmZ+G4ElihV1zoZ7H0FQCpgS0YWtwjCKo=
cloud.google.com/go/assuredworkloads v1.13.0 h1:NQXyyGLksPmiapE1Oc64a3cMwYIBAoDBg6cWR+B3eaY=
cloud.google.com/go/assuredworkloads v1.13.0/go.mod h1:o/oHEOnUlribR+uJWTKQo8A5RhSl9K9FNeMOew4TJ3M=
cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI=
cloud.google.com/go/auth v0.16.3/go.mod h1:NucRGjaXfzP1ltpcQ7On/VTZ0H4kWB5Jy+Y9Dnm76fA=
cloud.google.com/go/auth v0.16.4/go.mod h1:j10ncYwjX/g3cdX7GpEzsdM+d+ZNsXAbb6qXA7p1Y5M=
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
cloud.google.com/go/automl v1.14.7 h1:ZLj48Ur2Qcso4M3bgOtjsOmeV5Ee92N14wuOc8OW+L0=
cloud.google.com/go/automl v1.14.7/go.mod h1:8a4XbIH5pdvrReOU72oB+H3pOw2JBxo9XTk39oljObE=
cloud.google.com/go/automl v1.15.0 h1:YRwLbsBv4yApX64pkrdyy4emhWE6lHEnljX4b1aTQC4=
cloud.google.com/go/automl v1.15.0/go.mod h1:U9zOtQb8zVrFNGTuW3BfxeqmLyeleLgT9B12EaXfODg=
cloud.google.com/go/baremetalsolution v1.3.6 h1:9bdGlpY1LgLONQjFsDwrkjLzdPTlROpfU+GhA97YpOk=
cloud.google.com/go/baremetalsolution v1.3.6/go.mod h1:7/CS0LzpLccRGO0HL3q2Rofxas2JwjREKut414sE9iM=
cloud.google.com/go/baremetalsolution v1.4.0 h1:g67fjVdrNCHZl8jDWdZvo+6zGTTMMuvNWO7HSgG8lnI=
cloud.google.com/go/baremetalsolution v1.4.0/go.mod h1:K6C6g4aS8LW95I0fEHZiBsBlh0UxwDLGf+S/vyfXbvg=
cloud.google.com/go/batch v1.12.2 h1:gWQdvdPplptpvrkqF6ibtxZkOsYKLTFbxYawHa/TvCg=
cloud.google.com/go/batch v1.12.2/go.mod h1:tbnuTN/Iw59/n1yjAYKV2aZUjvMM2VJqAgvUgft6UEU=
cloud.google.com/go/batch v1.13.0 h1:6gmnNhJhm+Y598CZE6x+CeNZNIPCkYa4vkF58S5abHo=
cloud.google.com/go/batch v1.13.0/go.mod h1:yHFeqBn8wUjmJs4sYbwZ7N3HdeGA+FkPAXjoCKMwGak=
cloud.google.com/go/batch v1.14.0 h1:r5DEMPNXZk1as36Le3DaNQTRhhnR+E95a99SFxwF52o=
cloud.google.com/go/batch v1.14.0/go.mod h1:oeQveyG6NDS/ks2ilOP4LzKRmuIaI7GLe0CkR7WF6pk=
cloud.google.com/go/beyondcorp v1.1.6 h1:4FcR+4QmcNGkhVij6TrYS4AQVNLBo7PBXKxNrKzpclQ=
cloud.google.com/go/beyondcorp v1.1.6/go.mod h1:V1PigSWPGh5L/vRRmyutfnjAbkxLI2aWqJDdxKbwvsQ=
cloud.google.com/go/beyondcorp v1.2.0 h1:mre997ya7QHFWSU+O5cT/FhBKTMy6Riqf1EXFxN46zw=
cloud.google.com/go/beyondcorp v1.2.0/go.mod h1:sszcgxpPPBEfLzbI0aYCTg6tT1tyt3CmKav3NZIUcvI=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/bigquery v1.69.0 h1:rZvHnjSUs5sHK3F9awiuFk2PeOaB8suqNuim21GbaTc=
cloud.google.com/go/bigquery v1.69.0/go.mod h1:TdGLquA3h/mGg+McX+GsqG9afAzTAcldMjqhdjHTLew=
cloud.google.com/go/bigquery v1.70.0 h1:V1OIhhOSionCOXWMmypXOvZu/ogkzosa7s1ArWJO/Yg=
cloud.google.com/go/bigquery v1.70.0/go.mod h1:6lEAkgTJN+H2JcaX1eKiuEHTKyqBaJq5U3SpLGbSvwI=
cloud.google.com/go/bigquery v1.72.0 h1:D/yLju+3Ens2IXx7ou1DJ62juBm+/coBInn4VVOg5Cw=
cloud.google.com/go/bigquery v1.72.0/go.mod h1:GUbRtmeCckOE85endLherHD9RsujY+gS7i++c1CqssQ=
cloud.google.com/go/bigtable v1.37.0 h1:Q+x7y04lQ0B+WXp03wc1/FLhFt4CwcQdkwWT0M4Jp3w=
cloud.google.com/go/bigtable v1.37.0/go.mod h1:HXqddP6hduwzrtiTCqZPpj9ij4hGZb4Zy1WF/dT+yaU=
cloud.google.com/go/bigtable v1.39.0 h1:NF0aaSend+Z5CKND2vWY9fgDwaeZ4bDgzUdgw8rk75Y=
cloud.google.com/go/bigtable v1.39.0/go.mod h1:zgL2Vxux9Bx+TcARDJDUxVyE+BCUfP2u4Zm9qeHF+g0=
cloud.google.com/go/bigtable v1.40.1 h1:k8HfpUOvn7sQwc6oNKqjvD/yjkwynf4qBuyKwh5cU08=
cloud.google.com/go/bigtable v1.40.1/go.mod h1:LtPzCcrAFaGRZ82Hs8xMueUeYW9Jw12AmNdUTMfDnh4=
cloud.google.com/go/bigtable v1.41.0 h1:99KOWShm/MUyuIbXBeVscdWJFV7GdgiYwFUrB5Iu4BI=
cloud.google.com/go/bigtable v1.41.0/go.mod h1:JlaltP06LEFXaxQdZiarGR9tKsX/II0IkNAKMDrWspI=
cloud.google.com/go/billing v1.20.4 h1:pqM5/c9UGydB9H90IPCxSvfCNLUPazAOSMsZkz5q5P4=
cloud.google.com/go/billing v1.20.4/go.mod h1:hBm7iUmGKGCnBm6Wp439YgEdt+OnefEq/Ib9SlJYxIU=
cloud.google.com/go/billing v1.21.0 h1:nbQjTXkpgB/E4XnYZQwcZnR63QFsbFwJ9DGsNg61Ghg=
cloud.google.com/go/billing v1.21.0/go.mod h1:ZGairB3EVnb3i09E2SxFxo50p5unPaMTuo1jh6jW9js=
cloud.google.com/go/binaryauthorization v1.9.5 h1:T0zYEroXT+y0O/x/yZd5SwQdFv4UbUINjvJyJKzDm0Q=
cloud.google.com/go/binaryauthorization v1.9.5/go.mod h1:CV5GkS2eiY461Bzv+OH3r5/AsuB6zny+MruRju3ccB8=
cloud.google.com/go/binaryauthorization v1.10.0 h1:YYK0BwiZv9uA6z+Ict908AykX4OBfDECMTE476OnS3A=
cloud.google.com/go/binaryauthorization v1.10.0/go.mod h1:WOuiaQkI4PU/okwrcREjSAr2AUtjQgVe+PlrXKOmKKw=
cloud.google.com/go/certificatemanager v1.9.5 h1:+ZPglfDurCcsv4azizDFpBucD1IkRjWjbnU7zceyjfY=
cloud.google.com/go/certificatemanager v1.9.5/go.mod h1:kn7gxT/80oVGhjL8rurMUYD36AOimgtzSBPadtAeffs=
cloud.google.com/go/certificatemanager v1.9.6 h1:v5X8X+THKrS9OFZb6k0GRDP1WQxLXTdMko7OInBliw4=
cloud.google.com/go/certificatemanager v1.9.6/go.mod h1:vWogV874jKZkSRDFCMM3r7wqybv8WXs3XhyNff6o/Zo=
cloud.google.com/go/channel v1.19.5 h1:UI+ZsRkS15hi9DRF+WAvTVLVuSeZiRmvCU8cjkjOwUU=
cloud.google.com/go/channel v1.19.5/go.mod h1:vevu+LK8Oy1Yuf7lcpDbkQQQm5I7oiY5fFTn3uwfQLY=
cloud.google.com/go/channel v1.20.0 h1:EeUa6SnD3+EL9B06G6N9Ud5/p/NtT6PC7lv5kmaUiHs=
cloud.google.com/go/channel v1.20.0/go.mod h1:nBR1Lz+/1TjSA16HTllvW9Y+QULODj3o3jEKrNNeOp4=
cloud.google.com/go/channel v1.21.0 h1:ThoAmHBd9WkX2SSuF6n6uEOvbBNoTuhBT7Rk6bFS5ho=
cloud.google.com/go/channel v1.21.0/go.mod h1:8v3TwHtgLmFxTpL2U+e10CLFOQN8u/Vr9RhYcJUS3y8=
cloud.google.com/go/cloudbuild v1.22.2 h1:4LlrIFa3IFLgD1mGEXmUE4cm9fYoU71OLwTvjM7Dg3c=
cloud.google.com/go/cloudbuild v1.22.2/go.mod h1:rPyXfINSgMqMZvuTk1DbZcbKYtvbYF/i9IXQ7eeEMIM=
cloud.google.com/go/cloudbuild v1.23.0 h1:ycLO1q8CdpDqWKpcqlcP+RMbkUguXqwvO//Q0vC1/jE=
cloud.google.com/go/cloudbuild v1.23.0/go.mod h1:BkxnZUIHUHkl+oNpEbwc7n9id4pZRDQRVKIa6sDCuJI=
cloud.google.com/go/cloudbuild v1.23.1 h1:Kl4QBrOPXcHVTic6XeRMp9YgLCy3b/ifGx8i29A3pYs=
cloud.google.com/go/cloudbuild v1.23.1/go.mod h1:Gh/k1NnFRw1DkhekO2BaR4MTg30Op6EQQHCUZCIyTAg=
cloud.google.com/go/cloudbuild v1.25.0 h1:Fkg+iJdN7bfICZJzLr/XV+k9aVxXS/hakIlhjDIRIDw=
cloud.google.com/go/cloudbuild v1.25.0/go.mod h1:lCu+T6IPkobPo2Nw+vCE7wuaAl9HbXLzdPx/tcF+oWo=
cloud.google.com/go/clouddms v1.8.7 h1:IWJbQBEECTaNanDRN1XdR7FU53MJ1nylTl3s9T3MuyI=
cloud.google.com/go/clouddms v1.8.7/go.mod h1:DhWLd3nzHP8GoHkA6hOhso0R9Iou+IGggNqlVaq/KZ4=
cloud.google.com/go/clouddms v1.8.8 h1:YWsmRXTyK6Ba0hm4qTBak5g1oLhryuM8rSBxHWC8iq4=
cloud.google.com/go/clouddms v1.8.8/go.mod h1:QtCyw+a73dlkDb2q20aTAPvfaTZCepDDi6Gb1AKq0a4=
cloud.google.com/go/cloudtasks v1.13.6 h1:Fwan19UiNoFD+3KY0MnNHE5DyixOxNzS1mZ4ChOdpy0=
cloud.google.com/go/cloudtasks v1.13.6/go.mod h1:/IDaQqGKMixD+ayM43CfsvWF2k36GeomEuy9gL4gLmU=
cloud.google.com/go/cloudtasks v1.13.7 h1:H2v8GEolNtMFfYzUpZBaZbydqU7drpyo99GtAgA+m4I=
cloud.google.com/go/cloudtasks v1.13.7/go.mod h1:H0TThOUG+Ml34e2+ZtW6k6nt4i9KuH3nYAJ5mxh7OM4=
cloud.google.com/go/compute v1.38.0 h1:MilCLYQW2m7Dku8hRIIKo4r0oKastlD74sSu16riYKs=
cloud.google.com/go/compute v1.38.0/go.mod h1:oAFNIuXOmXbK/ssXm3z4nZB8ckPdjltJ7xhHCdbWFZM=
cloud.google.com/go/compute v1.47.0 h1:SG1vwa2xaAzz6XN/qytL3Z8hzKeZzWxV1o5wYLc+TEk=
cloud.google.com/go/compute v1.47.0/go.mod h1:1uoZvP8Avyfhe3Y4he7sMOR16ZiAm2Q+Rc2P5rrJM28=
cloud.google.com/go/compute v1.49.1 h1:KYKIG0+pfpAWaAYayFkE/KPrAVCge0Hu82bPraAmsCk=
cloud.google.com/go/compute v1.49.1/go.mod h1:1uoZvP8Avyfhe3Y4he7sMOR16ZiAm2Q+Rc2P5rrJM28=
cloud.google.com/go/compute v1.54.0 h1:4CKmnpO+40z44bKG5bdcKxQ7ocNpRtOc9SCLLUzze1w=
cloud.google.com/go/compute v1.54.0/go.mod h1:RfBj0L1x/pIM84BrzNX2V21oEv16EKRPBiTcBRRH1Ww=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw=
cloud.google.com/go/contactcenterinsights v1.17.3 h1:lenyU3uzHwKDveCwmpfNxHYvLS3uEBWdn+O7+rSxy+Q=
cloud.google.com/go/contactcenterinsights v1.17.3/go.mod h1:7Uu2CpxS3f6XxhRdlEzYAkrChpR5P5QfcdGAFEdHOG8=
cloud.google.com/go/contactcenterinsights v1.17.4 h1:wA4j99BhsoeYlLx6xEIqrNN1aOTtUme0wimHZegg80s=
cloud.google.com/go/contactcenterinsights v1.17.4/go.mod h1:kZe6yOnKDfpPz2GphDHynxk/Spx+53UX/pGf+SmWAKM=
cloud.google.com/go/container v1.43.0 h1:A6J92FJPfxTvyX7MHF+w4t2W9WCqvHOi9UB5SAeSy3w=
cloud.google.com/go/container v1.43.0/go.mod h1:ETU9WZ1KM9ikEKLzrhRVao7KHtalDQu6aPqM34zDr/U=
cloud.google.com/go/container v1.44.0 h1:JEHeW535svvNwJrjrlQ/cdjd15LCWrPKnHsulrufd3A=
cloud.google.com/go/container v1.44.0/go.mod h1:tVK2o4UZUTkg9WpBcgj4qRzwGA1dSFdWA3mil3YkLIQ=
cloud.google.com/go/container v1.45.0 h1:i1No5obpPxlIFLGHdUF6h2YjRR1qN9t/ZkA8KA5B//o=
cloud.google.com/go/container v1.45.0/go.mod h1:eB6jUfJLjne9VsTDGcH7mnj6JyZK+KOUIA6KZnYE/ds=
cloud.google.com/go/containeranalysis v0.14.1 h1:1SoHlNqL3XrhqcoozB+3eoHif2sRUFtp/JeASQTtGKo=
cloud.google.com/go/containeranalysis v0.14.1/go.mod h1:28e+tlZgauWGHmEbnI5UfIsjMmrkoR1tFN0K2i71jBI=
cloud.google.com/go/containeranalysis v0.14.2 h1:OW2dlMPtR5VnjQGyAP+uJlZahc1l+JFxFlH/J3+l7gw=
cloud.google.com/go/containeranalysis v0.14.2/go.mod h1:FjppROiUtP9cyMegdWdY/TsBSGc6kqh1GjA2NOJXXL8=
cloud.google.com/go/datacatalog v1.26.0 h1:eFgygb3DTufTWWUB8ARk+dSuXz+aefNJXTlkWlQcWwE=
cloud.google.com/go/datacatalog v1.26.0/go.mod h1:bLN2HLBAwB3kLTFT5ZKLHVPj/weNz6bR0c7nYp0LE14=
cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCblhgC2k=
cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg=
cloud.google.com/go/dataflow v0.11.0 h1:AdhB4cAkMOC9NtrHJxpKOVvO/VqBLaIyk0tEEhbGjYM=
cloud.google.com/go/dataflow v0.11.0/go.mod h1:gNHC9fUjlV9miu0hd4oQaXibIuVYTQvZhMdPievKsPk=
cloud.google.com/go/dataflow v0.11.1 h1:Z+UYlGrE+IoB+5IAN4/qWdPKO0IpIK9bs2Dy40HK6lg=
cloud.google.com/go/dataflow v0.11.1/go.mod h1:3s6y/h5Qz7uuxTmKJKBifkYZ3zs63jS+6VGtSu8Cf7Y=
cloud.google.com/go/dataform v0.12.0 h1:0eCPTPUC/RZ863aVfXTJLkg0tEpdpn62VD6ywSmmzxM=
cloud.google.com/go/dataform v0.12.0/go.mod h1:PuDIEY0lSVuPrZqcFji1fmr5RRvz3DGz4YP/cONc8g4=
cloud.google.com/go/dataform v0.12.1 h1:yf6Up6m1FUt+YB5CBgNtIZfz2OzjuNBdzZWV3SLSVNE=
cloud.google.com/go/dataform v0.12.1/go.mod h1:atGS8ReRjfNDUQib0X/o/7Gi2bqHI2G7/J86LKiGimE=
cloud.google.com/go/datafusion v1.8.6 h1:GZ6J+CR8CEeWAj8luRCtr8GvImSQRkArIIqGiZOnzBA=
cloud.google.com/go/datafusion v1.8.6/go.mod h1:fCyKJF2zUKC+O3hc2F9ja5EUCAbT4zcH692z8HiFZFw=
cloud.google.com/go/datafusion v1.8.7 h1:tLCV+xYuOrSjdrRTkc9Cqsb5mBSQEsNfFmuTNYl5/rA=
cloud.google.com/go/datafusion v1.8.7/go.mod h1:4dkFb1la41qCEXh1AzYtFwl842bu2ikTUXyKhjvFCb0=
cloud.google.com/go/datalabeling v0.9.6 h1:VOZ5U+78ttnhNCEID7qdeogqZQzK5N+LPHIQ9Q3YDsc=
cloud.google.com/go/datalabeling v0.9.6/go.mod h1:n7o4x0vtPensZOoFwFa4UfZgkSZm8Qs0Pg/T3kQjXSM=
cloud.google.com/go/datalabeling v0.9.7 h1:wwoct7mw38s75XvEmLoItQ2TY0RFsGiRDb0iNbXUcX4=
cloud.google.com/go/datalabeling v0.9.7/go.mod h1:EEUVn+wNn3jl19P2S13FqE1s9LsKzRsPuuMRq2CMsOk=
cloud.google.com/go/dataplex v1.25.3 h1:Xr0Toh6wyBlmL3H4EPu1YKwxUtkDSzzq+IP0iLc88kk=
cloud.google.com/go/dataplex v1.25.3/go.mod h1:wOJXnOg6bem0tyslu4hZBTncfqcPNDpYGKzed3+bd+E=
cloud.google.com/go/dataplex v1.27.1 h1:renSEYTQZMQ3ag7lM0BDmSj4FWqaTGW60YQ/lvAE5iA=
cloud.google.com/go/dataplex v1.27.1/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA=
cloud.google.com/go/dataplex v1.28.0 h1:rROI3iqMVI9nXT701ULoFRETQVAOAPC3mPSWFDxXFl0=
cloud.google.com/go/dataplex v1.28.0/go.mod h1:VB+xlYJiJ5kreonXsa2cHPj0A3CfPh/mgiHG4JFhbUA=
cloud.google.com/go/dataproc/v2 v2.11.2 h1:KhC8wdLILpAs17yeTG6Miwg1v0nOP/OXD+9QNg3w6AQ=
cloud.google.com/go/dataproc/v2 v2.11.2/go.mod h1:xwukBjtfiO4vMEa1VdqyFLqJmcv7t3lo+PbLDcTEw+g=
cloud.google.com/go/dataproc/v2 v2.14.1 h1:Kxq0iomU0H4MlVP4HYeYPNJnV+YxNctf/hFrprmGy5Y=
cloud.google.com/go/dataproc/v2 v2.14.1/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8=
cloud.google.com/go/dataproc/v2 v2.15.0 h1:I/Yux/d8uaxf3W+d59kolGTOc52+VZaL6RzJw7oDOeg=
cloud.google.com/go/dataproc/v2 v2.15.0/go.mod h1:tSdkodShfzrrUNPDVEL6MdH9/mIEvp/Z9s9PBdbsZg8=
cloud.google.com/go/dataqna v0.9.7 h1:qTRAG/E3T63Xj1orefRlwupfwH9c9ERUAnWSRGp75so=
cloud.google.com/go/dataqna v0.9.7/go.mod h1:4ac3r7zm7Wqm8NAc8sDIDM0v7Dz7d1e/1Ka1yMFanUM=
cloud.google.com/go/dataqna v0.9.8 h1:3FREvU+sjaEHSjlKrKF6KjUmafdOvM8CbZ897rttxNs=
cloud.google.com/go/dataqna v0.9.8/go.mod h1:2lHKmGPOqzzuqCc5NI0+Xrd5om4ulxGwPpLB4AnFgpA=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/datastore v1.20.0 h1:NNpXoyEqIJmZFc0ACcwBEaXnmscUpcG4NkKnbCePmiM=
cloud.google.com/go/datastore v1.20.0/go.mod h1:uFo3e+aEpRfHgtp5pp0+6M0o147KoPaYNaPAKpfh8Ew=
cloud.google.com/go/datastore v1.21.0 h1:dUrYq47ysCA4nM7u8kRT0WnbfXc6TzX49cP3TCwIiA0=
cloud.google.com/go/datastore v1.21.0/go.mod h1:9l+KyAHO+YVVcdBbNQZJu8svF17Nw5sMKuFR0LYf1nY=
cloud.google.com/go/datastream v1.14.1 h1:j+y0lUKm9pbDjJn0YcWxPI/hXNGUQ80GE6yrFuJC/JA=
cloud.google.com/go/datastream v1.14.1/go.mod h1:JqMKXq/e0OMkEgfYe0nP+lDye5G2IhIlmencWxmesMo=
cloud.google.com/go/datastream v1.15.1 h1:7PKeDpksi8nbOR4gspmNokzsr0q/uRzDIt20bR3BtRs=
cloud.google.com/go/datastream v1.15.1/go.mod h1:aV1Grr9LFon0YvqryE5/gF1XAhcau2uxN2OvQJPpqRw=
cloud.google.com/go/deploy v1.27.2 h1:C0VqBhFyQFp6+xgPHZAD7LeRA4XGy5YLzGmPQ2NhlLk=
cloud.google.com/go/deploy v1.27.2/go.mod h1:4NHWE7ENry2A4O1i/4iAPfXHnJCZ01xckAKpZQwhg1M=
cloud.google.com/go/deploy v1.27.3 h1:QU8gLXsXDRqLyEWNrI6zJiVzuuOBX/WpMi4p0oexV+c=
cloud.google.com/go/deploy v1.27.3/go.mod h1:7LFIYYTSSdljYRqY3n+JSmIFdD4lv6aMD5xg0crB5iw=
cloud.google.com/go/dialogflow v1.68.2 h1:bXpoqPRf37KKxB79PKr20B/TAU/Z5iA0FnB6C5N2jrA=
cloud.google.com/go/dialogflow v1.68.2/go.mod h1:E0Ocrhf5/nANZzBju8RX8rONf0PuIvz2fVj3XkbAhiY=
cloud.google.com/go/dialogflow v1.69.1 h1:R69CCEgx9RMHWjS2eP7aw5sE1Ajo5buQTzTdBe2o13w=
cloud.google.com/go/dialogflow v1.69.1/go.mod h1:mP4XrpgDvPYBP+cdLxFC1WJJlkwuy0H8L1Lada9No/M=
cloud.google.com/go/dialogflow v1.71.0 h1:P2VLpdyxu1gxsocyVWGfLzxvZ8Esxh2KULHhNuZ3FXI=
cloud.google.com/go/dialogflow v1.71.0/go.mod h1:mP4XrpgDvPYBP+cdLxFC1WJJlkwuy0H8L1Lada9No/M=
cloud.google.com/go/dialogflow v1.74.0 h1:YtSrYLd6CpRS1NZ67r4Drvm1gafUEGDm/8vVBRYEv+I=
cloud.google.com/go/dialogflow v1.74.0/go.mod h1:jlKHmd3/KdvWWhGZjoCnWQAQNOMHOhDK6DQ430p3T1I=
cloud.google.com/go/dlp v1.23.0 h1:3xWRKylXxhysaQaV+DLev1YcIywFUCc7yJEE6R7ZGDQ=
cloud.google.com/go/dlp v1.23.0/go.mod h1:vVT4RlyPMEMcVHexdPT6iMVac3seq3l6b8UPdYpgFrg=
cloud.google.com/go/dlp v1.25.0 h1:283+PJFk72SIj3apDTT/cHnKPBtuBUnduLBpbz1diFw=
cloud.google.com/go/dlp v1.25.0/go.mod h1:PY4DMzV7lqRC5JvpxL05fXNeL8dknxYpFp4WjxmE22M=
cloud.google.com/go/dlp v1.27.0 h1:rBQCAcIdWxyLRbFsOkyLig0OFhcKhCThtnro+b3dvEc=
cloud.google.com/go/dlp v1.27.0/go.mod h1:PY4DMzV7lqRC5JvpxL05fXNeL8dknxYpFp4WjxmE22M=
cloud.google.com/go/dlp v1.28.0 h1:+aMQYODOxCCZHpdzKvv/rIc9CbKd6XVmjVBRjaF8UvQ=
cloud.google.com/go/dlp v1.28.0/go.mod h1:C3od1fIK8lf7Kr62aU1Uh0z4OL5Z8s3do3znAiEupAw=
cloud.google.com/go/documentai v1.37.0 h1:7fla8GcarupO15eatRTUveXCob6DOSW1Wa+1i63CM3Q=
cloud.google.com/go/documentai v1.37.0/go.mod h1:qAf3ewuIUJgvSHQmmUWvM3Ogsr5A16U2WPHmiJldvLA=
cloud.google.com/go/documentai v1.38.1 h1:6uWDoi/wAT8D9belR9Q1n2fFtY6YJ+/3aj6rR9mAlbY=
cloud.google.com/go/documentai v1.38.1/go.mod h1:KmlLO93F7GRU8dENXRxvt+7V8o7eCG6Y6WDitKbcYJs=
cloud.google.com/go/documentai v1.39.0 h1:ngPNmVUb3xxj6/j7ruK4vmOMm+XTJO0Vi7Jkm0R3AdU=
cloud.google.com/go/documentai v1.39.0/go.mod h1:KmlLO93F7GRU8dENXRxvt+7V8o7eCG6Y6WDitKbcYJs=
cloud.google.com/go/domains v0.10.6 h1:TI+Aavwc31KD8huOquJz0ISchCq1zSEWc9M+JcPJyxc=
cloud.google.com/go/domains v0.10.6/go.mod h1:3xzG+hASKsVBA8dOPc4cIaoV3OdBHl1qgUpAvXK7pGY=
cloud.google.com/go/domains v0.10.7 h1:G3kUq0vKBMhyOj5GqAfEYbVuez05U+ENHZUAtrEp/pI=
cloud.google.com/go/domains v0.10.7/go.mod h1:T3WG/QUAO/52z4tUPooKS8AY7yXaFxPYn1V3F0/JbNQ=
cloud.google.com/go/edgecontainer v1.4.3 h1:9tfGCicvrki927T+hGMB0yYmwIbRuZY6JR1/awrKiZ0=
cloud.google.com/go/edgecontainer v1.4.3/go.mod h1:q9Ojw2ox0uhAvFisnfPRAXFTB1nfRIOIXVWzdXMZLcE=
cloud.google.com/go/edgecontainer v1.4.4 h1:6KTQo6Qf0iEtfPVotlG7orazEO1I93Ham0PMlkHYpdQ=
cloud.google.com/go/edgecontainer v1.4.4/go.mod h1:yyNVHsCKtsX/0mqFdbljQw0Uo660q2dlMPaiqYiC2Tg=
cloud.google.com/go/errorreporting v0.3.2 h1:isaoPwWX8kbAOea4qahcmttoS79+gQhvKsfg5L5AgH8=
cloud.google.com/go/errorreporting v0.3.2/go.mod h1:s5kjs5r3l6A8UUyIsgvAhGq6tkqyBCUss0FRpsoVTww=
cloud.google.com/go/errorreporting v0.4.0 h1:uLcasn2hKpj6iSPvHrzRjkJcaNVaKx8yKQcP3VTS6aI=
cloud.google.com/go/errorreporting v0.4.0/go.mod h1:dZGEhqzdHZSRxxWLVjC3Ue5CVaROzvP58D9rU6zbBfw=
cloud.google.com/go/essentialcontacts v1.7.6 h1:ysHZ4gr4plW1CL1Ur/AucUUfh20hDjSFbfjxSK0q/sk=
cloud.google.com/go/essentialcontacts v1.7.6/go.mod h1:/Ycn2egr4+XfmAfxpLYsJeJlVf9MVnq9V7OMQr9R4lA=
cloud.google.com/go/essentialcontacts v1.7.7 h1:v9sO4IHFuwplaOuDnEXZFtfOrjw2bi11TSIVp5PnAU4=
cloud.google.com/go/essentialcontacts v1.7.7/go.mod h1:ytycWAEn/aKUMRKQPMVgMrAtphEMgjbzL8vFwM3tqXs=
cloud.google.com/go/eventarc v1.15.5 h1:bZW7ZMM+XXNErg6rOZcgxUzAgz4vpReRDP3ZiGf7/sI=
cloud.google.com/go/eventarc v1.15.5/go.mod h1:vDCqGqyY7SRiickhEGt1Zhuj81Ya4F/NtwwL3OZNskg=
cloud.google.com/go/eventarc v1.16.1 h1:1wIofFPViIgTABbIryIkx6uXJ0ima8UE2xIyo0nBesE=
cloud.google.com/go/eventarc v1.16.1/go.mod h1:wB3NTIQ+l4QPirJiTMeU+YpSc5+iyoDYWV4n2/Vmh78=
cloud.google.com/go/eventarc v1.17.0 h1:cog4hh0RNbVC7KVwibCqBPhBKklj4wnIJVz8Qrf2hVI=
cloud.google.com/go/eventarc v1.17.0/go.mod h1:wB3NTIQ+l4QPirJiTMeU+YpSc5+iyoDYWV4n2/Vmh78=
cloud.google.com/go/eventarc v1.18.0 h1:8WWG1/ogInYur1NQjML6EMHQ0ZBzAdMDGlUVpLD56cI=
cloud.google.com/go/eventarc v1.18.0/go.mod h1:/6SDoqh5+9QNUqCX4/oQcJVK16fG/snHBSXu7lrJtO8=
cloud.google.com/go/filestore v1.10.2 h1:LjoAyp9TvVNBns3sUUzPaNsQiGpR2BReGmTS3bUCuBE=
cloud.google.com/go/filestore v1.10.2/go.mod h1:w0Pr8uQeSRQfCPRsL0sYKW6NKyooRgixCkV9yyLykR4=
cloud.google.com/go/filestore v1.10.3 h1:3KZifUVTqGhNNv6MLeONYth1HjlVM4vDhaH+xrdPljU=
cloud.google.com/go/filestore v1.10.3/go.mod h1:94ZGyLTx9j+aWKozPQ6Wbq1DuImie/L/HIdGMshtwac=
cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s=
cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU=
cloud.google.com/go/firestore v1.20.0 h1:JLlT12QP0fM2SJirKVyu2spBCO8leElaW0OOtPm6HEo=
cloud.google.com/go/firestore v1.20.0/go.mod h1:jqu4yKdBmDN5srneWzx3HlKrHFWFdlkgjgQ6BKIOFQo=
cloud.google.com/go/firestore v1.21.0 h1:BhopUsx7kh6NFx77ccRsHhrtkbJUmDAxNY3uapWdjcM=
cloud.google.com/go/firestore v1.21.0/go.mod h1:1xH6HNcnkf/gGyR8udd6pFO4Z7GWJSwLKQMx/u6UrP4=
cloud.google.com/go/functions v1.19.6 h1:vJgWlvxtJG6p/JrbXAkz83DbgwOyFhZZI1Y32vUddjY=
cloud.google.com/go/functions v1.19.6/go.mod h1:0G0RnIlbM4MJEycfbPZlCzSf2lPOjL7toLDwl+r0ZBw=
cloud.google.com/go/functions v1.19.7 h1:7LcOD18euIVGRUPaeCmgO6vfWSLNIsi6STWRQcdANG8=
cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA=
cloud.google.com/go/gkebackup v1.8.0 h1:eBqOt61yEChvj7I/GDPBbdCCRdUPudD1qrQYfYWV3Ok=
cloud.google.com/go/gkebackup v1.8.0/go.mod h1:FjsjNldDilC9MWKEHExnK3kKJyTDaSdO1vF0QeWSOPU=
cloud.google.com/go/gkebackup v1.8.1 h1:gUgI3lZJYALZsHXE7YJOKI8bMpoAX/tF6jnNugvzT1g=
cloud.google.com/go/gkebackup v1.8.1/go.mod h1:GAaAl+O5D9uISH5MnClUop2esQW4pDa2qe/95A4l7YQ=
cloud.google.com/go/gkeconnect v0.12.4 h1:67/rnPmF/I1Wmf7jWyKH+z4OWjU8ZUI0Vmzxvmzf3KY=
cloud.google.com/go/gkeconnect v0.12.4/go.mod h1:bvpU9EbBpZnXGo3nqJ1pzbHWIfA9fYqgBMJ1VjxaZdk=
cloud.google.com/go/gkeconnect v0.12.5 h1:EFql3zRaFw74yATt5lf+mcPDqPZ4EeLvoIJ+0NaEkag=
cloud.google.com/go/gkeconnect v0.12.5/go.mod h1:wMD2RXcsAWlkREZWJDVeDV70PYka1iEb9stFmgpw+5o=
cloud.google.com/go/gkehub v0.15.6 h1:9iogrmNNa+drDPf/zkLH/6KGgUf7FuuyokmithoGwMQ=
cloud.google.com/go/gkehub v0.15.6/go.mod h1:sRT0cOPAgI1jUJrS3gzwdYCJ1NEzVVwmnMKEwrS2QaM=
cloud.google.com/go/gkehub v0.16.0 h1:Jk5pAXG54FlQzTRXhuKyym/NzOgS8oWRs0XNatZYDf4=
cloud.google.com/go/gkehub v0.16.0/go.mod h1:ADp27Ucor8v81wY+x/5pOxTorxkPj/xswH3AUpN62GU=
cloud.google.com/go/gkemulticloud v1.5.3 h1:334aZmOzIt3LVBpguCof8IHaLaftcZlx+L0TGBukYkY=
cloud.google.com/go/gkemulticloud v1.5.3/go.mod h1:KPFf+/RcfvmuScqwS9/2MF5exZAmXSuoSLPuaQ98Xlk=
cloud.google.com/go/gkemulticloud v1.5.4 h1:AKuOmr5QBCPLJCyZhBABP3lIz+h3jxAy53LVMrEuvlg=
cloud.google.com/go/gkemulticloud v1.5.4/go.mod h1:7l9+6Tp4jySSGj4PStO8CE6RrHFdcRARK4ScReHX1bU=
cloud.google.com/go/gkemulticloud v1.6.0 h1:m0FX9o7t7xVmSZhqzm/m8nEZn8LnC5Kh60Wg4Yx1lyQ=
cloud.google.com/go/gkemulticloud v1.6.0/go.mod h1:bGpd4o/Z5Z/XFlaojkgdVisHRwb+fLJvUPzsmV0I9ok=
cloud.google.com/go/grafeas v0.3.15 h1:lBjwKmhpiqOAFaE0xdqF8CqO74a99s8tUT5mCkBBxPs=
cloud.google.com/go/grafeas v0.3.15/go.mod h1:irwcwIQOBlLBotGdMwme8PipnloOPqILfIvMwlmu8Pk=
cloud.google.com/go/grafeas v0.3.16 h1:0R6n4WSJXQ3rHj6xl80hQjsniIPgGmFlHRLQmHZw9HU=
cloud.google.com/go/grafeas v0.3.16/go.mod h1:I/yrRMOEsLasrmZXQzmDXwrJ3ZPn3dQWLaWt4lXmYvE=
cloud.google.com/go/gsuiteaddons v1.7.7 h1:sk0SxpCGIA7tIO//XdiiG29f2vrF6Pq/dsxxyBGiRBY=
cloud.google.com/go/gsuiteaddons v1.7.7/go.mod h1:zTGmmKG/GEBCONsvMOY2ckDiEsq3FN+lzWGUiXccF9o=
cloud.google.com/go/gsuiteaddons v1.7.8 h1:Dayrv57XW8kZIvmQjAc89Tp7Kr3O9Am/hf6pXkTjYFY=
cloud.google.com/go/gsuiteaddons v1.7.8/go.mod h1:DBKNHH4YXAdd/rd6zVvtOGAJNGo0ekOh+nIjTUDEJ5U=
cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE=
cloud.google.com/go/iap v1.11.2 h1:VIioCrYsyWiRGx7Y8RDNylpI6d4t1Qx5ZgSLUVmWWPo=
cloud.google.com/go/iap v1.11.2/go.mod h1:Bh99DMUpP5CitL9lK0BC8MYgjjYO4b3FbyhgW1VHJvg=
cloud.google.com/go/iap v1.11.3 h1:Nheb77nO0/pECm/thoE3wHVAbkQSI+G8KBWviqBepiA=
cloud.google.com/go/iap v1.11.3/go.mod h1:+gXO0ClH62k2LVlfhHzrpiHQNyINlEVmGAE3+DB4ShU=
cloud.google.com/go/ids v1.5.6 h1:uKGuaWozDcjg3wyf54Gd7tCH2YK8BFeH9qo1xBNiPKE=
cloud.google.com/go/ids v1.5.6/go.mod h1:y3SGLmEf9KiwKsH7OHvYYVNIJAtXybqsD2z8gppsziQ=
cloud.google.com/go/ids v1.5.7 h1:V0pSk+KKW+5/AVpeQMhM9D1VI7aMZkayj5jddNETJos=
cloud.google.com/go/ids v1.5.7/go.mod h1:N3ZQOIgIBwwOu2tzyhmh3JDT+kt8PcoKkn2BRT9Qe4A=
cloud.google.com/go/iot v1.8.6 h1:A3AhugnIViAZkC3/lHAQDaXBIk2ZOPBZS0XQCyZsjjc=
cloud.google.com/go/iot v1.8.6/go.mod h1:MThnkiihNkMysWNeNje2Hp0GSOpEq2Wkb/DkBCVYa0U=
cloud.google.com/go/iot v1.8.7 h1:PDUtxCzlFwFHODEFAgaGJy/Zv4tdvLbZ+lvZ1mKQXE4=
cloud.google.com/go/iot v1.8.7/go.mod h1:HvVcypV8LPv1yTXSLCNK+YCtqGHhq+p0F3BXETfpN+U=
cloud.google.com/go/kms v1.23.0 h1:WaqAZsUptyHwOo9II8rFC1Kd2I+yvNsNP2IJ14H2sUw=
cloud.google.com/go/kms v1.23.0/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g=
cloud.google.com/go/language v1.14.5 h1:BVJ/POtlnJ55LElvnQY19UOxpMVtHoHHkFJW2uHJsVU=
cloud.google.com/go/language v1.14.5/go.mod h1:nl2cyAVjcBct1Hk73tzxuKebk0t2eULFCaruhetdZIA=
cloud.google.com/go/language v1.14.6 h1:/0Fbd3/T4oNmpPqIq5/hrWdHc/eoYGtVH5lDNkuHH3k=
cloud.google.com/go/language v1.14.6/go.mod h1:7y3J9OexQsfkWNGCxhT+7lb64pa60e12ZCoWDOHxJ1M=
cloud.google.com/go/lifesciences v0.10.6 h1:Vu7XF4s5KJ8+mSLIL4eaQM6JTyWXvSB54oqC+CUZH20=
cloud.google.com/go/lifesciences v0.10.6/go.mod h1:1nnZwaZcBThDujs9wXzECnd1S5d+UiDkPuJWAmhRi7Q=
cloud.google.com/go/lifesciences v0.10.7 h1:MO5aBahcYv7JeuCpHbg/11h7KL/BYt1+PpgHhleLDbI=
cloud.google.com/go/lifesciences v0.10.7/go.mod h1:v3AbTki9iWttEls/Wf4ag3EqeLRHofploOcpsLnu7iY=
cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=
cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
cloud.google.com/go/longrunning v0.6.7/go.mod h1:EAFV3IZAKmM56TyiE6VAP3VoTzhZzySwI/YI1s/nRsY=
cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=
cloud.google.com/go/managedidentities v1.7.6 h1:zrZVWXZJlmHnfpyCrTQIbDBGUBHrcOOvrsjMjoXRxrk=
cloud.google.com/go/managedidentities v1.7.6/go.mod h1:pYCWPaI1AvR8Q027Vtp+SFSM/VOVgbjBF4rxp1/z5p4=
cloud.google.com/go/managedidentities v1.7.7 h1:vC/q7D+97PZfb0UNf7r/+/clHauuaf1PqWwP7neuaeg=
cloud.google.com/go/managedidentities v1.7.7/go.mod h1:nwNlMxtBo2YJMvsKXRtAD1bL41qiCI9npS7cbqrsJUs=
cloud.google.com/go/maps v1.21.0 h1:El61AfMxC1sU/RU8Wzs9dkZEgltyunKM86aKF9aDlaE=
cloud.google.com/go/maps v1.21.0/go.mod h1:cqzZ7+DWUKKbPTgqE+KuNQtiCRyg/o7WZF9zDQk+HQs=
cloud.google.com/go/maps v1.23.0 h1:NGQM1vBXHZ7SgrlJ5q+KEoSw1B9pgYiFTNfuPa+2wOQ=
cloud.google.com/go/maps v1.23.0/go.mod h1:8tjxLplMV7FEoR9FIwqoY7siDnaOdE7FBWnjaXK/xts=
cloud.google.com/go/maps v1.26.0 h1:tcdo9oB3Ap4N9JJJFOhxRFldKUok4Mesd3ta7Rm79r0=
cloud.google.com/go/maps v1.26.0/go.mod h1:+auempdONAP8emtm48aCfNo1ZC+3CJniRA1h8J4u7bY=
cloud.google.com/go/mediatranslation v0.9.6 h1:SDGatA73TgZ8iCvILVXpk/1qhTK5DJyufUDEWgbmbV8=
cloud.google.com/go/mediatranslation v0.9.6/go.mod h1:WS3QmObhRtr2Xu5laJBQSsjnWFPPthsyetlOyT9fJvE=
cloud.google.com/go/mediatranslation v0.9.7 h1:JXbjms+JxgaWkj/YuaQm1OeCzuF+IZCDV17uUcZgFOU=
cloud.google.com/go/mediatranslation v0.9.7/go.mod h1:mz3v6PR7+Fd/1bYrRxNFGnd+p4wqdc/fyutqC5QHctw=
cloud.google.com/go/memcache v1.11.6 h1:33IVqQEmFiITsBXwGHeTkUhWz0kLNKr90nV3e22uLPs=
cloud.google.com/go/memcache v1.11.6/go.mod h1:ZM6xr1mw3F8TWO+In7eq9rKlJc3jlX2MDt4+4H+/+cc=
cloud.google.com/go/memcache v1.11.7 h1:ZDIfIMZsKKPzwdbvTMOL1il0shX24J7B9DC+sEt4Yj4=
cloud.google.com/go/memcache v1.11.7/go.mod h1:AU1jYlUqCihxapcJ1GGMtlMWDVhzjbfUWBXqsXa4rBg=
cloud.google.com/go/metastore v1.14.7 h1:dLm59AHHZCorveCylj7c2iWhkQsmMIeWTsV+tG/BXtY=
cloud.google.com/go/metastore v1.14.7/go.mod h1:0dka99KQofeUgdfu+K/Jk1KeT9veWZlxuZdJpZPtuYU=
cloud.google.com/go/metastore v1.14.8 h1:nfyUDD9AeKIs6btY5buQ1No0OVco20WpX9wIruL8UOA=
cloud.google.com/go/metastore v1.14.8/go.mod h1:h1XI2LpD4ohJhQYn9TwXqKb5sVt6KSo47ft96SiFF1s=
cloud.google.com/go/monitoring v1.24.0/go.mod h1:Bd1PRK5bmQBQNnuGwHBfUamAV1ys9049oEPHnn4pcsc=
cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=
cloud.google.com/go/networkconnectivity v1.17.1 h1:RQcG1rZNCNV5Dn3tnINs4TYswDXk2hKH+85eh+JvoWU=
cloud.google.com/go/networkconnectivity v1.17.1/go.mod h1:DTZCq8POTkHgAlOAAEDQF3cMEr/B9k1ZbpklqvHEBtg=
cloud.google.com/go/networkconnectivity v1.19.1 h1:n0IzhdgSNzIKQygWwDV8yKRXkZpX3FsjCYFbO9iNHPU=
cloud.google.com/go/networkconnectivity v1.19.1/go.mod h1:Q5v6uNNNz8BP232uuXM66XgWML9m379xhwv58Y+8Kb0=
cloud.google.com/go/networkconnectivity v1.20.0 h1:A0uRcZJdq7F6LYWcc2NIea3h0i7p6kY1/CyLavOTG0I=
cloud.google.com/go/networkconnectivity v1.20.0/go.mod h1:9MzGwD4ljiq+Z2Pg3ue27OEewCuHz7IUfw1fITrIdSw=
cloud.google.com/go/networkmanagement v1.19.1 h1:ecukgArkYCVcK5w2h7WDDd+nHgmBAp9Bst7ClmVKz5A=
cloud.google.com/go/networkmanagement v1.19.1/go.mod h1:icgk265dNnilxQzpr6rO9WuAuuCmUOqq9H6WBeM2Af4=
cloud.google.com/go/networkmanagement v1.20.1 h1:W5zdnH332yJFADTXlsHWLVkiXLKGWHjrsg7vXLGd+Ws=
cloud.google.com/go/networkmanagement v1.20.1/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU=
cloud.google.com/go/networkmanagement v1.21.0 h1:041d0RqmwP/H7AWkF/AoAwFvuCkz8shUMA4EoQt0lOA=
cloud.google.com/go/networkmanagement v1.21.0/go.mod h1:clG/5Yt0wQ57qSH6Yh7oehQYlobHw3F6nb3Pn4ig5hU=
cloud.google.com/go/networksecurity v0.10.6 h1:6b6fcCG9BFNcmtNO+VuPE04vkZb5TKNX9+7ZhYMgstE=
cloud.google.com/go/networksecurity v0.10.6/go.mod h1:FTZvabFPvK2kR/MRIH3l/OoQ/i53eSix2KA1vhBMJec=
cloud.google.com/go/networksecurity v0.10.7 h1:J5gdG7mHdRLrsyM7yy4nKFgbN8+geaOo/4Zpeh4DWrg=
cloud.google.com/go/networksecurity v0.10.7/go.mod h1:FgoictpfaJkeBlM1o2m+ngPZi8mgJetbFDH4ws1i2fQ=
cloud.google.com/go/networksecurity v0.11.0 h1:+ahtCqEqwHw3a3UIeG21vT817xt9kkDDAO6k9+LCc18=
cloud.google.com/go/networksecurity v0.11.0/go.mod h1:JLgDsg4tOyJ3eMO8lypjqMftbfd60SJ+P7T+DUmWBsM=
cloud.google.com/go/notebooks v1.12.6 h1:nCfZwVihArMPP2atRoxRrXOXJ/aC9rAgpBQGCc2zpYw=
cloud.google.com/go/notebooks v1.12.6/go.mod h1:3Z4TMEqAKP3pu6DI/U+aEXrNJw9hGZIVbp+l3zw8EuA=
cloud.google.com/go/notebooks v1.12.7 h1:g5LTI1LHa/86abDTWd8nrq7/4qq8oFhVx1SmnNpZLVg=
cloud.google.com/go/notebooks v1.12.7/go.mod h1:uR9pxAkKmlNloibMr9Q1t8WhIu4P2JeqJs7c064/0Mo=
cloud.google.com/go/optimization v1.7.6 h1:jDvIuSxDsXI2P7l2sYXm6CoX1YBIIT6Khm5m0hq0/KQ=
cloud.google.com/go/optimization v1.7.6/go.mod h1:4MeQslrSJGv+FY4rg0hnZBR/tBX2awJ1gXYp6jZpsYY=
cloud.google.com/go/optimization v1.7.7 h1:dMtxINB6G7wULbdm8nZ/x1NMa579Q+GfJc5gaN8VeDw=
cloud.google.com/go/optimization v1.7.7/go.mod h1:OY2IAlX23o52qwMAZ0w65wibKuV12a4x6IHDTCq6kcU=
cloud.google.com/go/orchestration v1.11.9 h1:PnlZ/O4R/eiounpxUkhI9ZXRMWbG7vFqxc6L6sR+31k=
cloud.google.com/go/orchestration v1.11.9/go.mod h1:KKXK67ROQaPt7AxUS1V/iK0Gs8yabn3bzJ1cLHw4XBg=
cloud.google.com/go/orchestration v1.11.10 h1:TVWDiZyvcflLFeTQH2GexHmtJ6iUSjzr0zsSiT338dA=
cloud.google.com/go/orchestration v1.11.10/go.mod h1:tz7m1s4wNEvhNNIM3JOMH0lYxBssu9+7si5MCPw/4/0=
cloud.google.com/go/orgpolicy v1.15.0 h1:uQziDu3UKYk9ZwUgneZAW5aWxZFKgOXXsuVKFKh0z7Y=
cloud.google.com/go/orgpolicy v1.15.0/go.mod h1:NTQLwgS8N5cJtdfK55tAnMGtvPSsy95JJhESwYHaJVs=
cloud.google.com/go/orgpolicy v1.15.1 h1:0hq12wxNwcfUMojr5j3EjWECSInIuyYDhkAWXTomRhc=
cloud.google.com/go/orgpolicy v1.15.1/go.mod h1:bpvi9YIyU7wCW9WiXL/ZKT7pd2Ovegyr2xENIeRX5q0=
cloud.google.com/go/osconfig v1.14.6 h1:4uJrA1obzMBp1I+DF15y/MvsXKIODevuANpq3QhvX30=
cloud.google.com/go/osconfig v1.14.6/go.mod h1:LS39HDBH0IJDFgOUkhSZUHFQzmcWaCpYXLrc3A4CVzI=
cloud.google.com/go/osconfig v1.15.1 h1:QQzK5njfsfO2rdOWYVDyLQktqSq9gKf2ohRYeKUuA10=
cloud.google.com/go/osconfig v1.15.1/go.mod h1:NegylQQl0+5m+I+4Ey/g3HGeQxKkncQ1q+Il4DZ8PME=
cloud.google.com/go/oslogin v1.14.6 h1:BDKVcxo1OO4ZT+PbuFchZjnbrlUGfChilt6+pITY1VI=
cloud.google.com/go/oslogin v1.14.6/go.mod h1:xEvcRZTkMXHfNSKdZ8adxD6wvRzeyAq3cQX3F3kbMRw=
cloud.google.com/go/oslogin v1.14.7 h1:YQ8P/+MLwH0tpENYU9QOgwKQxe8DYfAKxIfm6y+OBtA=
cloud.google.com/go/oslogin v1.14.7/go.mod h1:NB6NqBHfDMwznePdBVX+ILllc1oPCdNSGp5u/WIyndY=
cloud.google.com/go/phishingprotection v0.9.6 h1:yl572bBQbPjflX250SOflN6gwO2uYoddN2uRp36fDTo=
cloud.google.com/go/phishingprotection v0.9.6/go.mod h1:VmuGg03DCI0wRp/FLSvNyjFj+J8V7+uITgHjCD/x4RQ=
cloud.google.com/go/phishingprotection v0.9.7 h1:ZJqHirY2/H6s+uTq1y1iiVASzm3ZuDiMglT5NXywPBE=
cloud.google.com/go/phishingprotection v0.9.7/go.mod h1:JTI4HNGyAbWolBoNOoCyCF0e3cqPNrYnlievHU49EwE=
cloud.google.com/go/policytroubleshooter v1.11.6 h1:Z8+tO2z21MY1arBBuJjwrOjbw8fbZb13AZTHXdzkl2U=
cloud.google.com/go/policytroubleshooter v1.11.6/go.mod h1:jdjYGIveoYolk38Dm2JjS5mPkn8IjVqPsDHccTMu3mY=
cloud.google.com/go/policytroubleshooter v1.11.7 h1:Bbj1EiVh96u9mfO2p+JNoHrvvyC0Ms6zP+vxqQnsaG8=
cloud.google.com/go/policytroubleshooter v1.11.7/go.mod h1:JP/aQ+bUkt4Gz6lQXBi/+A/6nyNRZ0Pvxui5Xl9ieyk=
cloud.google.com/go/privatecatalog v0.10.7 h1:R951ikhxIanXEijBCu0xnoUAOteS5m/Xplek0YvsNTE=
cloud.google.com/go/privatecatalog v0.10.7/go.mod h1:Fo/PF/B6m4A9vUYt0nEF1xd0U6Kk19/Je3eZGrQ6l60=
cloud.google.com/go/privatecatalog v0.10.8 h1:yOdy85WDvSCPxAMixkhs5X0Z96D74kosgOTp7aJEYvU=
cloud.google.com/go/privatecatalog v0.10.8/go.mod h1:BkLHi+rtAGYBt5DocXLytHhF0n6F03Tegxgty40Y7aA=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/pubsublite v1.8.2 h1:jLQozsEVr+c6tOU13vDugtnaBSUy/PD5zK6mhm+uF1Y=
cloud.google.com/go/pubsublite v1.8.2/go.mod h1:4r8GSa9NznExjuLPEJlF1VjOPOpgf3IT6k8x/YgaOPI=
cloud.google.com/go/recaptchaenterprise/v2 v2.20.4 h1:P4QMryKcWdi4LIe1Sx0b2ZOAQv5gVfdzPt2peXcN32Y=
cloud.google.com/go/recaptchaenterprise/v2 v2.20.4/go.mod h1:3H8nb8j8N7Ss2eJ+zr+/H7gyorfzcxiDEtVBDvDjwDQ=
cloud.google.com/go/recaptchaenterprise/v2 v2.20.5 h1:Q2CcYGxcvnvng2q3o1SaOpV+rjE/AbFVYGTJomxlG4g=
cloud.google.com/go/recaptchaenterprise/v2 v2.20.5/go.mod h1:TCHn8+vtwgygBOwwbUJgRi6R9qglIpTeImsWsWDr5Lo=
cloud.google.com/go/recaptchaenterprise/v2 v2.21.0 h1:zHaPdgmV3LmzaUfn9Xiiqp5zE1Y16f0O8XCwERrAs2E=
cloud.google.com/go/recaptchaenterprise/v2 v2.21.0/go.mod h1:HxQYqZC2/zl2CvKN7jJEv71vEdDi1GMGNUiZxnpiuVI=
cloud.google.com/go/recommendationengine v0.9.6 h1:slN7h23vswGccW8x3f+xUXCu9Yo18/GNkazH93LJbFk=
cloud.google.com/go/recommendationengine v0.9.6/go.mod h1:nZnjKJu1vvoxbmuRvLB5NwGuh6cDMMQdOLXTnkukUOE=
cloud.google.com/go/recommendationengine v0.9.7 h1:NH89CyKQP8e98kpdKLwV0jXkQGzSEEZia0V867vkoy8=
cloud.google.com/go/recommendationengine v0.9.7/go.mod h1:snZ/FL147u86Jqpv1j95R+CyU5NvL/UzYiyDo6UByTM=
cloud.google.com/go/recommender v1.13.5 h1:cIsyRKGNw4LpCfY5c8CCQadhlp54jP4fHtP+d5Sy2xE=
cloud.google.com/go/recommender v1.13.5/go.mod h1:v7x/fzk38oC62TsN5Qkdpn0eoMBh610UgArJtDIgH/E=
cloud.google.com/go/recommender v1.13.6 h1:ZVZg4wr1G7yzjIPcYUNSUJAaz9+2o78rmBU4QJgC7kg=
cloud.google.com/go/recommender v1.13.6/go.mod h1:y5/5womtdOaIM3xx+76vbsiA+8EBTIVfWnxHDFHBGJM=
cloud.google.com/go/redis v1.18.2 h1:JlHLceAOILEmbn+NIS7l+vmUKkFuobLToCWTxL7NGcQ=
cloud.google.com/go/redis v1.18.2/go.mod h1:q6mPRhLiR2uLf584Lcl4tsiRn0xiFlu6fnJLwCORMtY=
cloud.google.com/go/redis v1.18.3 h1:6LI8zSt+vmE3WQ7hE5GsJ13CbJBLV1qUw6B7CY31Wcw=
cloud.google.com/go/redis v1.18.3/go.mod h1:x8HtXZbvMBDNT6hMHaQ022Pos5d7SP7YsUH8fCJ2Wm4=
cloud.google.com/go/resourcemanager v1.10.6 h1:LIa8kKE8HF71zm976oHMqpWFiaDHVw/H1YMO71lrGmo=
cloud.google.com/go/resourcemanager v1.10.6/go.mod h1:VqMoDQ03W4yZmxzLPrB+RuAoVkHDS5tFUUQUhOtnRTg=
cloud.google.com/go/resourcemanager v1.10.7 h1:oPZKIdjyVTuag+D4HF7HO0mnSqcqgjcuA18xblwA0V0=
cloud.google.com/go/resourcemanager v1.10.7/go.mod h1:rScGkr6j2eFwxAjctvOP/8sqnEpDbQ9r5CKwKfomqjs=
cloud.google.com/go/resourcesettings v1.8.3 h1:13HOFU7v4cEvIHXSAQbinF4wp2Baybbq7q9FMctg1Ek=
cloud.google.com/go/resourcesettings v1.8.3/go.mod h1:BzgfXFHIWOOmHe6ZV9+r3OWfpHJgnqXy8jqwx4zTMLw=
cloud.google.com/go/retail v1.21.0 h1:8jgWgtAg1mk91WmaoWRTlL9CcvazPwqZ3YT9n6Gva9U=
cloud.google.com/go/retail v1.21.0/go.mod h1:LuG+QvBdLfKfO+7nnF3eA3l1j4TQw3Sg+UqlUorquRc=
cloud.google.com/go/retail v1.25.0 h1:zG08ikvKBst67AIQ7am0AraIuTT8uJsZ87U65Il4xe4=
cloud.google.com/go/retail v1.25.0/go.mod h1:J75G8pd+DH0SHueL9IJw7Y5d2VhTsjFsk+F1t9f8jXc=
cloud.google.com/go/retail v1.25.1 h1:S9Rl1sB6Dmdak5y9I2Pwn9hcWA5ubk33Vnevxw+CSrI=
cloud.google.com/go/retail v1.25.1/go.mod h1:J75G8pd+DH0SHueL9IJw7Y5d2VhTsjFsk+F1t9f8jXc=
cloud.google.com/go/run v1.10.0 h1:CDhz0PPzI/cVpmNFyHe3Yp21jNpiAqtkfRxuoLi+JU0=
cloud.google.com/go/run v1.10.0/go.mod h1:z7/ZidaHOCjdn5dV0eojRbD+p8RczMk3A7Qi2L+koHg=
cloud.google.com/go/run v1.12.0 h1:l4tpqhzJ75uOugXl2BQ15uEM5gLamVH5M70tBv70ZCU=
cloud.google.com/go/run v1.12.0/go.mod h1:/APJ89UqgGdIdaD1yaTiSYXozx3fNoqKR/cueDFRueI=
cloud.google.com/go/run v1.12.1 h1:zoXZ+vavS6k8wzEPlxMuh5rGkhQb5CAzrcfSFBInlS4=
cloud.google.com/go/run v1.12.1/go.mod h1:DdMsf2m0/n3WHNDcyoqZmfE+LMd/uEJ7j1yIooDrgXU=
cloud.google.com/go/run v1.15.0 h1:4cwyNv9SUQEsQOf5/DfPKyMWYSA52p38/o119BgMhO4=
cloud.google.com/go/run v1.15.0/go.mod h1:rgFHMdAopLl++57vzeqA+a1o2x0/ILZnEacRD6nC0EA=
cloud.google.com/go/scheduler v1.11.7 h1:zkMEJ0UbEJ3O7NwEUlKLIp6eXYv1L7wHjbxyxznajKM=
cloud.google.com/go/scheduler v1.11.7/go.mod h1:gqYs8ndLx2M5D0oMJh48aGS630YYvC432tHCnVWN13s=
cloud.google.com/go/scheduler v1.11.8 h1:BoXY2BvBsaRw3ggVMzC9tborZqJBu+NcJcD9PqeC5Kc=
cloud.google.com/go/scheduler v1.11.8/go.mod h1:bNKU7/f04eoM6iKQpwVLvFNBgGyJNS87RiFN73mIPik=
cloud.google.com/go/secretmanager v1.14.7 h1:VkscIRzj7GcmZyO4z9y1EH7Xf81PcoiAo7MtlD+0O80=
cloud.google.com/go/secretmanager v1.14.7/go.mod h1:uRuB4F6NTFbg0vLQ6HsT7PSsfbY7FqHbtJP1J94qxGc=
cloud.google.com/go/secretmanager v1.15.0 h1:RtkCMgTpaBMbzozcRUGfZe46jb9a3qh5EdEtVRUATF8=
cloud.google.com/go/secretmanager v1.15.0/go.mod h1:1hQSAhKK7FldiYw//wbR/XPfPc08eQ81oBsnRUHEvUc=
cloud.google.com/go/secretmanager v1.16.0 h1:19QT7ZsLJ8FSP1k+4esQvuCD7npMJml6hYzilxVyT+k=
cloud.google.com/go/secretmanager v1.16.0/go.mod h1://C/e4I8D26SDTz1f3TQcddhcmiC3rMEl0S1Cakvs3Q=
cloud.google.com/go/security v1.18.5 h1:6hqzvuwC8za9jyCTxygmEHnp4vZ8hfhwKVArxSCAVCo=
cloud.google.com/go/security v1.18.5/go.mod h1:D1wuUkDwGqTKD0Nv7d4Fn2Dc53POJSmO4tlg1K1iS7s=
cloud.google.com/go/security v1.19.1 h1:+uE0ZTv/CpGoBc7zuGnJTnLuOUTs3m1HrOcX8ng8S7Q=
cloud.google.com/go/security v1.19.1/go.mod h1:+T4yyeDXqBYESnCzswqbq/Oip+IYkIrTfRF4UmeT4Bk=
cloud.google.com/go/security v1.19.2 h1:cF3FkCRRbRC1oXuaGZFl3qU2sdu2gP3iOAHKzL5y04Y=
cloud.google.com/go/security v1.19.2/go.mod h1:KXmf64mnOsLVKe8mk/bZpU1Rsvxqc0Ej0A6tgCeN93w=
cloud.google.com/go/securitycenter v1.36.2 h1:hLA58IBYmWrNiXDIONvuCUQ4sHLVPy8JvDo2j1wSYCw=
cloud.google.com/go/securitycenter v1.36.2/go.mod h1:80ocoXS4SNWxmpqeEPhttYrmlQzCPVGaPzL3wVcoJvE=
cloud.google.com/go/securitycenter v1.38.0 h1:sU+tckApsBLZHrTALVvetgz4XcPsgbL0TXREjcPM3qM=
cloud.google.com/go/securitycenter v1.38.0/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0=
cloud.google.com/go/securitycenter v1.38.1 h1:D9zpeguY4frQU35GBw8+M6Gw79CiuTF9iVs4sFm3FDY=
cloud.google.com/go/securitycenter v1.38.1/go.mod h1:Ge2D/SlG2lP1FrQD7wXHy8qyeloRenvKXeB4e7zO6z0=
cloud.google.com/go/servicedirectory v1.12.6 h1:pl/KUNvFzlXpxgnPgzQjyTQQcv5WsQ97zCHaPrLQlYA=
cloud.google.com/go/servicedirectory v1.12.6/go.mod h1:OojC1KhOMDYC45oyTn3Mup08FY/S0Kj7I58dxUMMTpg=
cloud.google.com/go/servicedirectory v1.12.7 h1:je2yZlVcVFI/TshPXjjF9ZAlWedj0s5EbO2kozJrzBo=
cloud.google.com/go/servicedirectory v1.12.7/go.mod h1:gOtN+qbuCMH6tj2dqlDY3qQL7w3V0+nkWaZElnJK8Ps=
cloud.google.com/go/shell v1.8.6 h1:jLWyztGlNWBx55QXBM4HbWvfv7aiRjPzRKTUkZA8dXk=
cloud.google.com/go/shell v1.8.6/go.mod h1:GNbTWf1QA/eEtYa+kWSr+ef/XTCDkUzRpV3JPw0LqSk=
cloud.google.com/go/shell v1.8.7 h1:K1C9sh9EuNNhGpyCoqRdeudcU9zmfYTA95bhF5cokK8=
cloud.google.com/go/shell v1.8.7/go.mod h1:OTke7qc3laNEW5Jr5OV9VR3IwU5x5VqGOE6705zFex4=
cloud.google.com/go/spanner v1.82.0 h1:w9uO8RqEoBooBLX4nqV1RtgudyU2ZX780KTLRgeVg60=
cloud.google.com/go/spanner v1.82.0/go.mod h1:BzybQHFQ/NqGxvE/M+/iU29xgutJf7Q85/4U9RWMto0=
cloud.google.com/go/spanner v1.85.1 h1:cJx1ZD//C2QIfFQl8hSTn4twL8amAXtnayyflRIjj40=
cloud.google.com/go/spanner v1.85.1/go.mod h1:bbwCXbM+zljwSPLZ44wZOdzcdmy89hbUGmM/r9sD0ws=
cloud.google.com/go/spanner v1.86.1 h1:lSeVPwUotuKTpf8K6BPitzneQfGu73QcDFIca2lshG8=
cloud.google.com/go/spanner v1.86.1/go.mod h1:bbwCXbM+zljwSPLZ44wZOdzcdmy89hbUGmM/r9sD0ws=
cloud.google.com/go/spanner v1.87.0 h1:M9RGcj/4gJk6yY1lRLOz1Ze+5ufoWhbIiurzXLOOfcw=
cloud.google.com/go/spanner v1.87.0/go.mod h1:tcj735Y2aqphB6/l+X5MmwG4NnV+X1NJIbFSZGaHYXw=
cloud.google.com/go/speech v1.27.1 h1:+OktATNlQc+4WH78OrQadIP4CzXb9mBucdDGCO1NrlI=
cloud.google.com/go/speech v1.27.1/go.mod h1:efCfklHFL4Flxcdt9gpEMEJh9MupaBzw3QiSOVeJ6ck=
cloud.google.com/go/speech v1.28.0 h1:9AuiAxDTmh/aeREtw+/0e7aI27T5QN4fK5lhssc9MxA=
cloud.google.com/go/speech v1.28.0/go.mod h1:hJf6oa+1rzCW/CeDE/qCXedV20B2TXEUje5iaGwW+JI=
cloud.google.com/go/speech v1.28.1 h1:L8kq/CypGn6y/FbipyAPyn1L9JDW4CK3zkcAe4oDN7U=
cloud.google.com/go/speech v1.28.1/go.mod h1:+EN8Zuy6y2BKe9P1RAmMaFPAgBns6m+XMgXAfkYtSSE=
cloud.google.com/go/speech v1.29.0 h1:ehOzN/IsAhjjAtWg4fI8A3iNtonb1N8yWjofVhSTv+c=
cloud.google.com/go/speech v1.29.0/go.mod h1:wtUmIS/h0ZYU6cPA9klcyST3f6i2FdnvNDqENjrRDds=
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
cloud.google.com/go/storage v1.53.0/go.mod h1:7/eO2a/srr9ImZW9k5uufcNahT2+fPb8w5it1i5boaA=
cloud.google.com/go/storage v1.56.0/go.mod h1:Tpuj6t4NweCLzlNbw9Z9iwxEkrSem20AetIeH/shgVU=
cloud.google.com/go/storagetransfer v1.13.0 h1:uqKX3OgcYzR1W1YI943ZZ45id0RqA2eXXoCBSPstlbw=
cloud.google.com/go/storagetransfer v1.13.0/go.mod h1:+aov7guRxXBYgR3WCqedkyibbTICdQOiXOdpPcJCKl8=
cloud.google.com/go/storagetransfer v1.13.1 h1:Sjukr1LtUt7vLTHNvGc2gaAqlXNFeDFRIRmWGrFaJlY=
cloud.google.com/go/storagetransfer v1.13.1/go.mod h1:S858w5l383ffkdqAqrAA+BC7KlhCqeNieK3sFf5Bj4Y=
cloud.google.com/go/talent v1.8.3 h1:wDP+++O/P1cTJBMkYlSY46k0a6atSoyO+UkBGuU9+Ao=
cloud.google.com/go/talent v1.8.3/go.mod h1:oD3/BilJpJX8/ad8ZUAxlXHCslTg2YBbafFH3ciZSLQ=
cloud.google.com/go/talent v1.8.4 h1:1kJJ+WCY5LZ1A4rCa32zKh3N2xT3I8koiS63+vV0WC4=
cloud.google.com/go/talent v1.8.4/go.mod h1:3yukBXUTVFNyKcJpUExW/k5gqEy8qW6OCNj7WdN0MWo=
cloud.google.com/go/texttospeech v1.13.0 h1:oWWFQp0yFl4EJOr3opDkKH9304wUsZjgPjrTDS6S1a8=
cloud.google.com/go/texttospeech v1.13.0/go.mod h1:g/tW/m0VJnulGncDrAoad6WdELMTes8eb77Idz+4HCo=
cloud.google.com/go/texttospeech v1.14.0 h1:ArOelKEIHCA0St/svzpl668gittbg9CZ1+DYCBRvJmQ=
cloud.google.com/go/texttospeech v1.14.0/go.mod h1:l25ywjIgXS+mSE2f5LQdXdU7r3MOLwVOGaYZQMiYIWE=
cloud.google.com/go/texttospeech v1.16.0 h1:Ra4w+6qmaeb12ozlPBqGw8Jzdge1yfzhvZgcXWdXw30=
cloud.google.com/go/texttospeech v1.16.0/go.mod h1:AeSkoH3ziPvapsuyI07TWY4oGxluAjntX+pF4PJ2jy0=
cloud.google.com/go/tpu v1.8.3 h1:S4Ptq+yFIPNLEzQ/OQwiIYDNzk5I2vYmhf0SmFQOmWo=
cloud.google.com/go/tpu v1.8.3/go.mod h1:Do6Gq+/Jx6Xs3LcY2WhHyGwKDKVw++9jIJp+X+0rxRE=
cloud.google.com/go/tpu v1.8.4 h1:5DDheA1f7yZ/KUbVT/9lL+Yhgd3IqHDSVVrSqDVkAFY=
cloud.google.com/go/tpu v1.8.4/go.mod h1:ul0cyWSHr6jHGZYElZe6HvQn35VY93RAlwpDiSBRnPA=
cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=
cloud.google.com/go/translate v1.12.5 h1:QPMNi4WCtHwc2PPfxbyUMwdN/0+cyCGLaKi2tig41J8=
cloud.google.com/go/translate v1.12.5/go.mod h1:o/v+QG/bdtBV1d1edmtau0PwTfActvxPk/gtqdSDBi4=
cloud.google.com/go/translate v1.12.6 h1:QHcszWZvBLEZHM2WJ6IDg2BUTWzEPMiHhbJAd15yKGU=
cloud.google.com/go/translate v1.12.6/go.mod h1:nB3AXuX+iHbV8ZURmElcW85qkEDWZw68sf4kqMT/E5o=
cloud.google.com/go/translate v1.12.7 h1:aSxMbfJ3MVmEdQzu5jGXmPPxCAb1ySsor2yBMCI5MT4=
cloud.google.com/go/translate v1.12.7/go.mod h1:wwJp14NZyWvcrFANhIXutXj0pOBkYciBHwSlUOykcjI=
cloud.google.com/go/video v1.24.0 h1:KTB2BEXjGm2K/JcKxQXEgx3nSoMTByepnPZa4kln064=
cloud.google.com/go/video v1.24.0/go.mod h1:h6Bw4yUbGNEa9dH4qMtUMnj6cEf+OyOv/f2tb70G6Fk=
cloud.google.com/go/video v1.26.0 h1:EbzycFpb8jCbAhtfdQoVRxqVv6pXXabTdsGvUNoXqos=
cloud.google.com/go/video v1.26.0/go.mod h1:iqsrblPUfkxvyH31rnS02Z0dp9p5lySdq7+I0XzozQI=
cloud.google.com/go/video v1.27.1 h1:Hp+2AeM7b3AagdHcyh2820UTzSbGyqpFJVMu0nHbBcw=
cloud.google.com/go/video v1.27.1/go.mod h1:xzfAC77B4vtnbi/TT3UUxEjCa/+Ehy5EA8w470ytOig=
cloud.google.com/go/videointelligence v1.12.6 h1:heq7jEO39sH5TycBh8TGFJ827XCxK0tIWatmBY/n0jI=
cloud.google.com/go/videointelligence v1.12.6/go.mod h1:/l34WMndN5/bt04lHodxiYchLVuWPQjCU6SaiTswrIw=
cloud.google.com/go/videointelligence v1.12.7 h1:FisUrSZ+y3oLuGdlFQQgZoNTDm7FAfb2hwSTsSqX+9g=
cloud.google.com/go/videointelligence v1.12.7/go.mod h1:XAk5hCMY+GihxJ55jNoMdwdXSNZnCl3wGs2+94gK7MA=
cloud.google.com/go/vision/v2 v2.9.5 h1:UJZ0H6UlOaYKgCn6lWG2iMAOJIsJZLnseEfzBR8yIqQ=
cloud.google.com/go/vision/v2 v2.9.5/go.mod h1:1SiNZPpypqZDbOzU052ZYRiyKjwOcyqgGgqQCI/nlx8=
cloud.google.com/go/vision/v2 v2.9.6 h1:9UtOINPF8p9VACQ6KAyR/ZtkpuBHGmJsprutYupDcN0=
cloud.google.com/go/vision/v2 v2.9.6/go.mod h1:lJC+vP15D5znJvHQYjEoTKnpToX1L93BUlvBmzM0gyg=
cloud.google.com/go/vmmigration v1.8.6 h1:68hOQDhs1DOITrCrhritrwr8xy6s8QMdwDyMzMiFleU=
cloud.google.com/go/vmmigration v1.8.6/go.mod h1:uZ6/KXmekwK3JmC8PzBM/cKQmq404TTfWtThF6bbf0U=
cloud.google.com/go/vmmigration v1.9.0 h1:iekipb1hzN4qxVaFKvd4iM3IjMGDuu/CHb2PRfj5GCk=
cloud.google.com/go/vmmigration v1.9.0/go.mod h1:jI3lBlhQn9+BKIWE/MmMsOzGekCXCc34b1M0CihL3zY=
cloud.google.com/go/vmmigration v1.9.1 h1:7IED5P1BmZIDy0U9Co0OYGQkFK/19/7Y6xlCaitmIcs=
cloud.google.com/go/vmmigration v1.9.1/go.mod h1:jI3lBlhQn9+BKIWE/MmMsOzGekCXCc34b1M0CihL3zY=
cloud.google.com/go/vmmigration v1.10.0 h1:6AvttGxASQTiuIsNKUKOKsRiQG4qTMOY4KMyBhdZa1w=
cloud.google.com/go/vmmigration v1.10.0/go.mod h1:LDztCWEb+RwS1bPg4Xzt0fcJS9kVrFxa3ejhH7OW9vg=
cloud.google.com/go/vmwareengine v1.3.5 h1:OsGd1SB91y9fDuzdzFngMv4UcT4cqmRxjsCsS4Xmcu8=
cloud.google.com/go/vmwareengine v1.3.5/go.mod h1:QuVu2/b/eo8zcIkxBYY5QSwiyEcAy6dInI7N+keI+Jg=
cloud.google.com/go/vmwareengine v1.3.6 h1:TKvULKbk44QrIx674cnoVjcZueXhyCAm2sNAJu/S1ds=
cloud.google.com/go/vmwareengine v1.3.6/go.mod h1:ps0rb+Skgpt9ppHYC0o5DqtJ5ld2FyS8sAqtbHH8t9s=
cloud.google.com/go/vpcaccess v1.8.6 h1:RYtUB9rQEijX9Tc6lQcGst58ZOzPgaYTkz6+2pyPQTM=
cloud.google.com/go/vpcaccess v1.8.6/go.mod h1:61yymNplV1hAbo8+kBOFO7Vs+4ZHYI244rSFgmsHC6E=
cloud.google.com/go/vpcaccess v1.8.7 h1:K6siDR1T4HgSTv6sy6CAwupY7UGza6TQ1O8jtvEYoX4=
cloud.google.com/go/vpcaccess v1.8.7/go.mod h1:9RYw5bVvk4Z51Rc8vwXT63yjEiMD/l7XyEaDyrNHgmk=
cloud.google.com/go/webrisk v1.11.1 h1:yZKNB7zRxOMriLrhP5WDE+BjxXVl0wJHHZSdaYzbdVU=
cloud.google.com/go/webrisk v1.11.1/go.mod h1:+9SaepGg2lcp1p0pXuHyz3R2Yi2fHKKb4c1Q9y0qbtA=
cloud.google.com/go/webrisk v1.11.2 h1:q6zEdVgD8Ka+4fQl3azDcSNRug8clNnQ9iVS2iLh+MM=
cloud.google.com/go/webrisk v1.11.2/go.mod h1:yH44GeXz5iz4HFsIlGeoVvnjwnmfbni7Lwj1SelV4f0=
cloud.google.com/go/websecurityscanner v1.7.6 h1:cIPKJKZA3l7D8DfL4nxce8HGOWXBw3WAUBF0ymOW9GQ=
cloud.google.com/go/websecurityscanner v1.7.6/go.mod h1:ucaaTO5JESFn5f2pjdX01wGbQ8D6h79KHrmO2uGZeiY=
cloud.google.com/go/websecurityscanner v1.7.7 h1:udhvvDDRryM3nrITJk/eQe74D06KK2N3SF60/FH2njQ=
cloud.google.com/go/websecurityscanner v1.7.7/go.mod h1:ng/PzARaus3Bj4Os4LpUnyYHsbtJky1HbBDmz148v1o=
cloud.google.com/go/workflows v1.14.2 h1:phBz5TOAES0YGogxZ6Q7ISSudaf618lRhE3euzBpE9U=
cloud.google.com/go/workflows v1.14.2/go.mod h1:5nqKjMD+MsJs41sJhdVrETgvD5cOK3hUcAs8ygqYvXQ=
cloud.google.com/go/workflows v1.14.3 h1:FGF6QEl3rtOSIHPOMZofWRVy3KNx26jDdgoYzJZ6ZhY=
cloud.google.com/go/workflows v1.14.3/go.mod h1:CC9+YdVI2Kvp0L58WajHpEfKJxhrtRh3uQ0SYWcmAk4=
code.cloudfoundry.org/clock v1.2.0 h1:1swXS7yPmQmhAdkTb1nJ2c0geOdf4LvibUleNCo2HjA=
code.cloudfoundry.org/clock v1.2.0/go.mod h1:foDbmVp5RIuIGlota90ot4FkJtx5m4+oKoWiVuu2FDg=
codeberg.org/go-fonts/liberation v0.5.0 h1:SsKoMO1v1OZmzkG2DY+7ZkCL9U+rrWI09niOLfQ5Bo0=
codeberg.org/go-fonts/liberation v0.5.0/go.mod h1:zS/2e1354/mJ4pGzIIaEtm/59VFCFnYC7YV6YdGl5GU=
codeberg.org/go-latex/latex v0.1.0 h1:hoGO86rIbWVyjtlDLzCqZPjNykpWQ9YuTZqAzPcfL3c=
codeberg.org/go-latex/latex v0.1.0/go.mod h1:LA0q/AyWIYrqVd+A9Upkgsb+IqPcmSTKc9Dny04MHMw=
codeberg.org/go-pdf/fpdf v0.10.0 h1:u+w669foDDx5Ds43mpiiayp40Ov6sZalgcPMDBcZRd4=
codeberg.org/go-pdf/fpdf v0.10.0/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
contrib.go.opencensus.io/exporter/stackdriver v0.13.15-0.20230702191903-2de6d2748484 h1:xRc46S76eyn4ZF3jWX8I+aUSKVLw5EQ1aDvHwfV5W1o=
contrib.go.opencensus.io/exporter/stackdriver v0.13.15-0.20230702191903-2de6d2748484/go.mod h1:uxw+4/0SiKbbVSD/F2tk5pJTdVcfIBBcsQ8gwcu4X+E=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9 h1:VpgP7xuJadIUuKccphEpTJnWhS2jkQyMt6Y7pJCD7fY=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
git.sr.ht/~sbinet/gg v0.6.0 h1:RIzgkizAk+9r7uPzf/VfbJHBMKUr0F5hRFxTUGMnt38=
git.sr.ht/~sbinet/gg v0.6.0/go.mod h1:uucygbfC9wVPQIfrmwM2et0imr8L7KQWywX0xpFMm94=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.1/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.0/go.mod h1:J7MUC/wtRpfGVbQ5sIItY5/FuVWmvzlY21WAOfQnq/I=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw=
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.53.0/go.mod h1:ZPpqegjbE99EPKsu3iUWV22A04wzGPcAY/ziSIQEEgs=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.53.0/go.mod h1:cSgYe11MCNYunTnRXrKiR/tHc0eoKjICUuWpNZoVCOo=
github.com/IBM/sarama v1.43.1 h1:Z5uz65Px7f4DhI/jQqEm/tV9t8aU+JUdTyW/K/fCXpA=
github.com/IBM/sarama v1.43.1/go.mod h1:GG5q1RURtDNPz8xxJs3mgX6Ytak8Z9eLhAkJPObe2xE=
github.com/Joker/jade v1.1.3 h1:Qbeh12Vq6BxURXT1qZBRHsDxeURB8ztcL6f3EXSGeHk=
github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0 h1:t527LHHE3HmiHrq74QMpNPZpGCIJzTx+apLkMKt4HC0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 h1:KkH3I3sJuOLP3TjA/dfr4NAY8bghDwnXiU7cTKxQqo0=
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0=
github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302 h1:uvdUDbHQHO85qeSydJtItA4T55Pw6BtAejd0APRJOCE=
github.com/alicebob/gopher-json v0.0.0-20230218143504-906a9b012302/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE=
github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA=
github.com/aws/aws-sdk-go-v2 v1.40.0/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1 h1:BDgIUYGEo5TkayOWv/oBLPphWwNm/A91AebUjAu5L5g=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.1/go.mod h1:iS6EPmNeqCsGo+xQmXv0jIMjyYtQfnwg36zl2FwEouk=
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/bmatcuk/doublestar v1.1.1 h1:YroD6BJCZBYx06yYFEWvUuKVWQn3vLLQAVmDmvTSaiQ=
github.com/bytedance/sonic v1.10.0-rc3 h1:uNSnscRapXTwUgTyOF0GVljYD08p9X/Lbr9MweSV3V0=
github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/campoy/embedmd v1.0.0 h1:V4kI2qTJJLf4J29RzI/MAt2c3Bl4dQSYPuflzwFH2hY=
github.com/campoy/embedmd v1.0.0/go.mod h1:oxyr9RCiSXg0M3VJ3ks0UGfp98BpSSGr0kpiX3MzVl8=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f h1:WBZRG4aNOuI15bLRrCgN8fCq8E5Xuty6jGbmSNEvSsU=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/dmarkham/enumer v1.6.1 h1:aSc9awYtZL07TUueWs40QcHtxTvHTAwG0EqrNsK45w4=
github.com/dmarkham/enumer v1.6.1/go.mod h1:yixql+kDDQRYqcuBM2n9Vlt7NoT9ixgXhaXry8vmRg8=
github.com/dmarkham/enumer v1.6.3 h1:B4aV4OsfzbrS5rvjILt4mMjiWBA//cKxJUMsvHZ8mEI=
github.com/dmarkham/enumer v1.6.3/go.mod h1:DyjXaqCglj4GhELF73oWiparNkYkXvmOBLza/o4kO74=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30=
github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws=
github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0=
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/envoyproxy/protoc-gen-validate v1.1.0/go.mod h1:sXRDRVmzEbkM7CVcM06s9shE/m23dg3wzjl0UWqJ2q4=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/flosch/pongo2/v4 v4.0.2 h1:gv+5Pe3vaSVmiJvh/BZa82b7/00YUGm0PIyVVLop0Hw=
github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4 h1:WtGNWLvXpe6ZudgnXrq0barxBImvnnJoMEhXAzcbM0I=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=
github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198 h1:FSii2UQeSLngl3jFoR4tUKZLprO7qUlh/TKKticc0BM=
github.com/goccmack/gocc v0.0.0-20230228185258-2292f9e40198/go.mod h1:DTh/Y2+NbnOVVoypCCQrovMPDKUGp4yZpSbWg5D0XIM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 h1:uK3X/2mt4tbSGoHvbLBHUny7CKiuwUip3MArtukol4E=
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY=
github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-pkcs11 v0.3.0 h1:PVRnTgtArZ3QQqTGtbtjtnIkzl2iY2kt24yqbrf7td8=
github.com/google/go-pkcs11 v0.3.0/go.mod h1:6eQoGcuNJpa7jnd5pMGdkSaQpNDYvPlXWMcjXXThLlY=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba h1:qJEJcuLzH5KDR0gKc0zcktin6KSAwL7+jWKBYceddTc=
github.com/google/go-tpm-tools v0.3.13-0.20230620182252-4639ecce2aba/go.mod h1:EFYHy8/1y2KfgTAsx7Luu7NGhoxtuVHnNo8jE7FikKc=
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/cloud-bigtable-clients-test v0.0.4 h1:9f+NjgCZHLx7t2lyL+Zhyg5f9/eFNI4RryGWlbcTwqk=
github.com/googleapis/cloud-bigtable-clients-test v0.0.4/go.mod h1:NNHPqSxC2OBSLmt1j/qofCRRzL0OYZxk24CsicIe8MA=
github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8 h1:tlyzajkF3030q6M8SvmJSemC9DTHL/xaMa18b65+JM4=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248=
github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/iris-contrib/schema v0.0.6 h1:CPSBLyx2e91H2yJzPuhGuifVRnZBBJ3pCOMbOvPZaTw=
github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d h1:c93kUJDtVAXFEhsCh5jSxyOJmFHuzcihnslQiX8Urwo=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kataras/blocks v0.0.7 h1:cF3RDY/vxnSRezc7vLFlQFTYXG/yAr1o7WImJuZbzC4=
github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I=
github.com/kataras/golog v0.1.9 h1:vLvSDpP7kihFGKFAvBSofYo7qZNULYSHOH2D7rPTKJk=
github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY=
github.com/kataras/iris/v12 v12.2.5 h1:R5UzUW4MIByBM6tKMG3UqJ7hL1JCEE+dkqQ8L72f6PU=
github.com/kataras/iris/v12 v12.2.5/go.mod h1:bf3oblPF8tQmRgyPCzPZr0mLazvEDFgImdaGZYuN4hw=
github.com/kataras/pio v0.0.12 h1:o52SfVYauS3J5X08fNjlGS5arXHjW/ItLkyLcKjoH6w=
github.com/kataras/pio v0.0.12/go.mod h1:ODK/8XBhhQ5WqrAhKy+9lTPS7sBf6O3KcLhc9klfRcY=
github.com/kataras/sitemap v0.0.6 h1:w71CRMMKYMJh6LR2wTgnk5hSgjVNB9KL60n5e2KHvLY=
github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4=
github.com/kataras/tunnel v0.0.4 h1:sCAqWuJV7nPzGrlb0os3j49lk2JhILT0rID38NHNLpA=
github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY=
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46 h1:veS9QfglfvqAw2e+eeNT/SbGySq8ajECXJ9e4fPoLhY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4=
github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4 h1:sIXJOMrYnQZJu7OB7ANSF4MYri2fTEGIsRLz6LwI4xE=
github.com/lyft/protoc-gen-star/v2 v2.0.4-0.20230330145011-496ad1ac90a4/go.mod h1:amey7yeodaJhXSbf/TlLvWiqQfLOSpEk//mLlc+axEk=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg=
github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE=
github.com/microsoft/ApplicationInsights-Go v0.4.4 h1:G4+H9WNs6ygSCe6sUyxRc2U81TI5Es90b2t/MwX5KqY=
github.com/microsoft/ApplicationInsights-Go v0.4.4/go.mod h1:fKRUseBqkw6bDiXTs3ESTiU/4YTIHsQS4W3fP2ieF4U=
github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615 h1:/mD+ABZyXD39BzJI2XyRJlqdZG11gXFo0SSynL+OFeU=
github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pascaldekloe/name v1.0.1 h1:9lnXOHeqeHHnWLbKfH6X98+4+ETVqFqxN09UXSjcMb0=
github.com/pascaldekloe/name v1.0.1/go.mod h1:Z//MfYJnH4jVpQ9wkclwu2I2MkHmXTlT9wR5UZScttM=
github.com/paulmach/protoscan v0.2.1 h1:rM0FpcTjUMvPUNk2BhPJrreDKetq43ChnL+x1sRg8O8=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0=
github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo=
github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM=
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/schollz/closestmatch v2.1.0+incompatible h1:Uel2GXEpJqOWBrlyI+oY9LTiyyjYS17cCYRqP13/SHk=
github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY=
github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad h1:fiWzISvDn0Csy5H0iwgAuJGQTUpVfEMJJd4nRFXogbc=
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.12.8 h1:Q2BqOTmlMjoutkuD/OPCnJUpIqrzT3nRPkw+q+KpXS0=
github.com/tdewolff/minify/v2 v2.12.8/go.mod h1:YRgk7CC21LZnbuke2fmYnCTq+zhCgpb0yJACOTUNJ1E=
github.com/tdewolff/parse/v2 v2.6.7 h1:WrFllrqmzAcrKHzoYgMupqgUBIfBVOb0yscFzDf8bBg=
github.com/tdewolff/parse/v2 v2.6.7/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
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.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
github.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA=
go.opentelemetry.io/contrib/detectors/gcp v1.36.0/go.mod h1:IbBN8uAIIx734PTonTPxAxnjc2pQTxWNkwfstZ+6H2k=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/contrib/zpages v0.62.0 h1:9fUYTLmrK0x/lweM2uM+BOx069jLx8PxVqWhegGJ9Bo=
go.opentelemetry.io/contrib/zpages v0.62.0/go.mod h1:C8kXoiC1Ytvereztus2R+kqdSa6W/MZ8FfS8Zwj+LiM=
go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028 h1:4+4C/Iv2U4fMZBiMCc98MG1In4gJY5YRhtpDNeDeHWs=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/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-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8 h1:LvzTn0GQhWuvKH/kVRS3R3bVAsdQWI7hvfLHGgh9+lU=
golang.org/x/telemetry v0.0.0-20251008203120-078029d740a8/go.mod h1:Pi4ztBfryZoJEkyFTI5/Ocsu2jXyDr6iSdgJiYE/uwE=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2 h1:O1cMQHRfwNpDfDJerqRoE2oD+AFlyid87D40L/OkkJo=
golang.org/x/telemetry v0.0.0-20260109210033-bd525da824e2/go.mod h1:b7fPSJ0pKZ3ccUh8gnTONJxhn3c/PS6tyzQvyqw4iA8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM=
golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM=
golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
gonum.org/v1/plot v0.15.2 h1:Tlfh/jBk2tqjLZ4/P8ZIwGrLEWQSPDLRm/SNWKNXiGI=
gonum.org/v1/plot v0.15.2/go.mod h1:DX+x+DWso3LTha+AdkJEv5Txvi+Tql3KAGkehP0/Ubg=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0=
google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY=
google.golang.org/api v0.235.0/go.mod h1:QpeJkemzkFKe5VCE/PMv7GsUfn9ZF+u+q1Q7w6ckxTg=
google.golang.org/api v0.239.0/go.mod h1:cOVEm2TpdAGHL2z+UwyS+kmlGr3bVWQQ6sYEqkKje50=
google.golang.org/api v0.246.0/go.mod h1:dMVhVcylamkirHdzEBAIQWUCgqY885ivNeZYd7VAVr8=
google.golang.org/api v0.247.0/go.mod h1:r1qZOPmxXffXg6xS5uhx16Fa/UFY8QU/K4bfKrnvovM=
google.golang.org/api v0.249.0/go.mod h1:dGk9qyI0UYPwO/cjt2q06LG/EhUpwZGdAbYF14wHHrQ=
google.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=
google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4=
google.golang.org/api v0.264.0/go.mod h1:fAU1xtNNisHgOF5JooAs8rRaTkl2rT3uaoNGo9NS3R8=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20240711142825-46eb208f015d/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e/go.mod h1:085qFyf2+XaZlRdCgKNCIZ3afY2p4HHZdoIRpId8F4A=
google.golang.org/genproto/googleapis/api v0.0.0-20250425173222-7b384671a197/go.mod h1:Cd8IzgPo5Akum2c9R6FsXNaZbH3Jpa2gpHlW89FqlyQ=
google.golang.org/genproto/googleapis/api v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:W3S/3np0/dPWsWLi1h/UymYctGXaGBM2StwzD0y140U=
google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw=
google.golang.org/genproto/googleapis/api v0.0.0-20250721164621-a45f3dfb1074/go.mod h1:vYFwMYFbmA8vl6Z/krj/h7+U/AqpHknwJX4Uqgfyc7I=
google.golang.org/genproto/googleapis/api v0.0.0-20250728155136-f173205681a0/go.mod h1:8ytArBbtOy2xfht+y2fqKd5DRDJRUQhqbyEnQ4bDChs=
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
google.golang.org/genproto/googleapis/api v0.0.0-20250908214217-97024824d090/go.mod h1:U8EXRNSd8sUYyDfs/It7KVWodQr+Hf9xtxyxWudSwEw=
google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9/go.mod h1:LmwNphe5Afor5V3R5BppOULHOnt2mCIf+NxMd4XiygE=
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=
google.golang.org/genproto/googleapis/api v0.0.0-20251124214823-79d6a2a48846/go.mod h1:Fk4kyraUvqD7i5H6S43sj2W98fbZa75lpZz/eUyhfO0=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20250929231259-57b25ae835d4/go.mod h1:YUQUKndxDbAanQC0ln4pZ3Sis3N5sqgDte2XQqufkJc=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251103181224-f26f9409b101 h1:yPJt1QyhbMgVYk1uHU1fzFDusVK69zmYfO7uupO0/QE=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251103181224-f26f9409b101/go.mod h1:ejCb7yLmK6GCVHp5qpeKbm4KZew/ldg+9b8kq5MONgk=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251124214823-79d6a2a48846 h1:7FlucM2tFADtEDnIlDrR12KdRqV48B1GSTU1U6uKSiY=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251124214823-79d6a2a48846/go.mod h1:G3Q0qS3k/oFEmVMddPsSYcFnm2+Mq2XRmxujrtu5hr0=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251213004720-97cd9d5aeac2 h1:kfqUJmmEHUMbtqqlGQ6YOcLVJmmGGHXXnIWZP56/41Q=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251213004720-97cd9d5aeac2/go.mod h1:G3Q0qS3k/oFEmVMddPsSYcFnm2+Mq2XRmxujrtu5hr0=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251222181119-0a764e51fe1b h1:pcwUBl8sRRgljKGbSYn4Riy/iVzEiuNBRZnDyrBSHVE=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260122232226-8e98ce8d340d h1:Q9v92SXbvCsk89QPHVik5fAtq93/x/R8/KNWeS3numk=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260203192932-546029d2fa20 h1:zQTtWukWCqGTV6Pt60SqvPGnEi2CE3PeeIRlu4SYgAc=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260203192932-546029d2fa20/go.mod h1:Tej9lWiwVvQJP+b43pjJIsr/3mZycXWCIyoiXmbFf40=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260226221140-a57be14db171 h1:zeSmMyGeCh9H7xJRBK+IS3b272WQpcvXUnLPhNuYLWI=
google.golang.org/genproto/googleapis/bytestream v0.0.0-20260226221140-a57be14db171/go.mod h1:9amqk/8LQWEC4RjyUxMx1DebyQ7hZB9gvl67bHmgZ2E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240826202546-f6391c0de4c7/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250512202823-5a2f75b736a9/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250728155136-f173205681a0/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20 h1:MLBCGN1O7GzIx+cBiwfYPwtmZ41U3Mn/cotLJciaArI=
google.golang.org/grpc/examples v0.0.0-20230224211313-3775f633ce20/go.mod h1:Nr5H8+MlGWr5+xX/STzdoEqJrO+YteqFbMyCsrb6mH0=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6 h1:ExN12ndbJ608cboPYflpTny6mXSzPrDLh0iTaVrRrds=
google.golang.org/grpc/examples v0.0.0-20250407062114-b368379ef8f6/go.mod h1:6ytKWczdvnpnO+m+JiG9NjEDzR1FJfsnmJdG7B8QVZ8=
google.golang.org/grpc/gcp/observability v1.0.1 h1:2IQ7szW1gobfZaS/sDSAu2uxO0V/aTryMZvlcyqKqQA=
google.golang.org/grpc/gcp/observability v1.0.1/go.mod h1:yM0UcrYRMe/B+Nu0mDXeTJNDyIMJRJnzuxqnJMz7Ewk=
google.golang.org/grpc/security/advancedtls v1.0.0 h1:/KQ7VP/1bs53/aopk9QhuPyFAp9Dm9Ejix3lzYkCrDA=
google.golang.org/grpc/security/advancedtls v1.0.0/go.mod h1:o+s4go+e1PJ2AjuQMY5hU82W7lDlefjJA6FqEHRVHWk=
google.golang.org/grpc/stats/opencensus v1.0.0 h1:evSYcRZaSToQp+borzWE52+03joezZeXcKJvZDfkUJA=
google.golang.org/grpc/stats/opencensus v1.0.0/go.mod h1:FhdkeYvN43wLYUnapVuRJJ9JXkNwe403iLUW2LKSnjs=
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA=
modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I=
modernc.org/ccorpus2 v1.5.8 h1:b3TgPK4oshQNlpuaR3SJeeE/1/ktsLIDPkuG1PPTE8E=
modernc.org/ccorpus2 v1.5.8/go.mod h1:Wifvo4Q/qS/h1aRoC2TffcHsnxwTikmi1AuLANuucJQ=
modernc.org/ebnf v1.1.0 h1:ilLq2kO1xGezeg75RyKffLsCLdamQHEmjv0CVq1QEQU=
modernc.org/ebnf v1.1.0/go.mod h1:CNIo7vuji3SyjIP/VhEumIKlAguC1g64mcdk/+VJW/w=
modernc.org/ebnfutil v1.1.0 h1:8AZ7iHDSIV6lrlgtexrIgmsey6wuSnB8s642ASDaTkc=
modernc.org/ebnfutil v1.1.0/go.mod h1:hdAyhM1jZSq9ygKhEeYgerbagyuLxyxzXcakBPyNqUI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/lex v1.1.1 h1:prSCNTLw1R4rn7M/RzwsuMtAuOytfyR3cnyM07P+Pas=
modernc.org/lex v1.1.1/go.mod h1:6r8o8DLJkAnOsQaGi8fMoi+Vt6LTbDaCrkUK729D8xM=
modernc.org/lexer v1.0.4 h1:hU7xVbZsqwPphyzChc7nMSGrsuaD2PDNOmzrzkS5AlE=
modernc.org/lexer v1.0.4/go.mod h1:tOajb8S4sdfOYitzCgXDFmbVJ/LE0v1fNJ7annTw36U=
modernc.org/scannertest v1.0.2 h1:JPtfxcVdbRvzmRf2YUvsDibJsQRw8vKA/3jb31y7cy0=
modernc.org/scannertest v1.0.2/go.mod h1:RzTm5RwglF/6shsKoEivo8N91nQIoWtcWI7ns+zPyGA=
rsc.io/binaryregexp v0.2.0 h1:HfqmD5MEmC0zvwBuF187nq9mdnXjXsSivRiXN7SmRkE=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/pdf v0.1.1 h1:k1MczvYDUvJBe93bYd7wrZLLUEcLZAuF824/I4e5Xr4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
================================================
FILE: pkg/gofr/auth.go
================================================
package gofr
import (
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc"
"gofr.dev/pkg/gofr/container"
grpcMiddleware "gofr.dev/pkg/gofr/grpc/middleware"
"gofr.dev/pkg/gofr/http/middleware"
)
// EnableBasicAuth enables basic authentication for the application.
//
// It takes a variable number of credentials as alternating username and password strings.
// An error is logged if an odd number of arguments is provided.
func (a *App) EnableBasicAuth(credentials ...string) {
if len(credentials) == 0 {
a.container.Error("No credentials provided for EnableBasicAuth. Proceeding without Authentication")
return
}
if len(credentials)%2 != 0 {
a.container.Error("Invalid number of arguments for EnableBasicAuth. Proceeding without Authentication")
return
}
users := make(map[string]string)
for i := 0; i < len(credentials); i += 2 {
users[credentials[i]] = credentials[i+1]
}
provider := grpcMiddleware.BasicAuthProvider{Users: users}
a.addAuthMiddleware(middleware.BasicAuthMiddleware(middleware.BasicAuthProvider{Users: users}),
grpcMiddleware.BasicAuthUnaryInterceptor(provider), grpcMiddleware.BasicAuthStreamInterceptor(provider))
}
// EnableBasicAuthWithFunc enables basic authentication for the HTTP server with a custom validation function.
//
// Deprecated: This method is deprecated and will be removed in future releases, users must use
// [App.EnableBasicAuthWithValidator] as it has access to application datasources.
func (a *App) EnableBasicAuthWithFunc(validateFunc func(username, password string) bool) {
provider := grpcMiddleware.BasicAuthProvider{ValidateFunc: validateFunc, Container: a.container}
a.addAuthMiddleware(middleware.BasicAuthMiddleware(middleware.BasicAuthProvider{ValidateFunc: validateFunc, Container: a.container}),
grpcMiddleware.BasicAuthUnaryInterceptor(provider), grpcMiddleware.BasicAuthStreamInterceptor(provider))
}
// EnableBasicAuthWithValidator enables basic authentication for the HTTP server with a custom validator.
//
// The provided `validateFunc` is invoked for each authentication attempt. It receives a container instance,
// username, and password. The function should return `true` if the credentials are valid, `false` otherwise.
func (a *App) EnableBasicAuthWithValidator(validateFunc func(c *container.Container, username, password string) bool) {
provider := grpcMiddleware.BasicAuthProvider{ValidateFuncWithDatasources: validateFunc, Container: a.container}
a.addAuthMiddleware(middleware.BasicAuthMiddleware(middleware.BasicAuthProvider{
ValidateFuncWithDatasources: validateFunc, Container: a.container}),
grpcMiddleware.BasicAuthUnaryInterceptor(provider), grpcMiddleware.BasicAuthStreamInterceptor(provider))
}
// EnableAPIKeyAuth enables API key authentication for the application.
//
// It requires at least one API key to be provided. The provided API keys will be used to authenticate requests.
func (a *App) EnableAPIKeyAuth(apiKeys ...string) {
provider := grpcMiddleware.APIKeyAuthProvider{APIKeys: apiKeys}
a.addAuthMiddleware(middleware.APIKeyAuthMiddleware(middleware.APIKeyAuthProvider{}, apiKeys...),
grpcMiddleware.APIKeyAuthUnaryInterceptor(provider), grpcMiddleware.APIKeyAuthStreamInterceptor(provider))
}
// EnableAPIKeyAuthWithFunc enables API key authentication for the application with a custom validation function.
//
// Deprecated: This method is deprecated and will be removed in future releases, users must use
// [App.EnableAPIKeyAuthWithValidator] as it has access to application datasources.
func (a *App) EnableAPIKeyAuthWithFunc(validateFunc func(apiKey string) bool) {
provider := grpcMiddleware.APIKeyAuthProvider{ValidateFunc: validateFunc, Container: a.container}
a.addAuthMiddleware(middleware.APIKeyAuthMiddleware(middleware.APIKeyAuthProvider{
ValidateFunc: validateFunc,
Container: a.container,
}), grpcMiddleware.APIKeyAuthUnaryInterceptor(provider), grpcMiddleware.APIKeyAuthStreamInterceptor(provider))
}
// EnableAPIKeyAuthWithValidator enables API key authentication for the application with a custom validation function.
//
// The provided `validateFunc` is used to determine the validity of an API key. It receives the request container
// and the API key as arguments and should return `true` if the key is valid, `false` otherwise.
func (a *App) EnableAPIKeyAuthWithValidator(validateFunc func(c *container.Container, apiKey string) bool) {
provider := grpcMiddleware.APIKeyAuthProvider{ValidateFuncWithDatasources: validateFunc, Container: a.container}
a.addAuthMiddleware(middleware.APIKeyAuthMiddleware(middleware.APIKeyAuthProvider{
ValidateFuncWithDatasources: validateFunc,
Container: a.container,
}), grpcMiddleware.APIKeyAuthUnaryInterceptor(provider), grpcMiddleware.APIKeyAuthStreamInterceptor(provider))
}
// EnableOAuth configures OAuth middleware for the application.
//
// It registers a new HTTP service for fetching JWKS and sets up OAuth middleware
// with the given JWKS endpoint and refresh interval.
//
// The JWKS endpoint is used to retrieve JSON Web Key Sets for verifying tokens.
// The refresh interval specifies how often to refresh the token cache.
// We can define optional JWT claim validation settings, including issuer, audience, and expiration checks.
// Accepts jwt.ParserOption for additional parsing options:
// https://pkg.go.dev/github.com/golang-jwt/jwt/v4#ParserOption
func (a *App) EnableOAuth(jwksEndpoint string,
refreshInterval int,
options ...jwt.ParserOption,
) {
a.AddHTTPService("gofr_oauth", jwksEndpoint)
oauthOption := middleware.OauthConfigs{
Provider: a.container.GetHTTPService("gofr_oauth"),
RefreshInterval: time.Second * time.Duration(refreshInterval),
}
publicKeyProvider := middleware.NewOAuth(oauthOption)
a.addAuthMiddleware(middleware.OAuth(publicKeyProvider, options...),
grpcMiddleware.OAuthUnaryInterceptor(publicKeyProvider, options...),
grpcMiddleware.OAuthStreamInterceptor(publicKeyProvider, options...))
}
func (a *App) addAuthMiddleware(httpMW func(http.Handler) http.Handler,
grpcUnary grpc.UnaryServerInterceptor, grpcStream grpc.StreamServerInterceptor) {
if a.httpServer != nil {
a.httpServer.router.Use(httpMW)
}
if a.grpcServer != nil {
a.grpcServer.addUnaryInterceptors(grpcUnary)
a.grpcServer.addStreamInterceptors(grpcStream)
}
}
================================================
FILE: pkg/gofr/cmd/request.go
================================================
package cmd
import (
"context"
"os"
"reflect"
"strconv"
"strings"
)
// Request is an abstraction over the actual command with flags. This abstraction is useful because it allows us
// to create cmd applications in same way we would create a HTTP server application.
// Gofr's http.Request is another such abstraction.
type Request struct {
flags map[string]bool
params map[string]string
}
const trueString = "true"
// TODO - use statement to parse the request to populate the flags and params.
// NewRequest creates a Request from a list of arguments. This way we can simulate running a command without actually
// doing it. It makes the code more testable this way.
func NewRequest(args []string) *Request {
r := Request{
flags: make(map[string]bool),
params: make(map[string]string),
}
const (
argsLen1 = 1
argsLen2 = 2
)
for _, arg := range args {
if arg == "" {
continue // This takes cares of cases where command has multiple space in between.
}
if arg[0] != '-' {
continue
}
if len(arg) == 1 {
continue
}
a := ""
if arg[1] == '-' {
a = arg[2:]
} else {
a = arg[1:]
}
switch values := strings.Split(a, "="); len(values) {
case argsLen1:
// Support -t -a etc.
r.params[values[0]] = trueString
case argsLen2:
// Support -a=b
r.params[values[0]] = values[1]
}
}
return &r
}
// Param returns the value of the parameter for key.
func (r *Request) Param(key string) string {
return r.params[key]
}
// PathParam returns the value of the parameter for key. This is equivalent to Param.
func (r *Request) PathParam(key string) string {
return r.params[key]
}
func (*Request) Context() context.Context {
return context.Background()
}
func (*Request) HostName() (hostname string) {
hostname, _ = os.Hostname()
return hostname
}
// Params retrieves all values for a given query parameter key, including comma-separated values.
func (r *Request) Params(key string) []string {
value, exists := r.params[key]
if !exists {
return []string{}
}
return strings.Split(value, ",")
}
func (r *Request) Bind(i any) error {
// pointer to struct - addressable
ps := reflect.ValueOf(i)
// struct
s := ps.Elem()
if s.Kind() != reflect.Struct {
return nil
}
for k, v := range r.params {
f := s.FieldByName(k)
// A Value can be changed only if it is addressable and not unexported struct field
if !f.IsValid() || !f.CanSet() {
continue
}
//nolint:exhaustive // Bind supports only basic field kinds.
switch f.Kind() {
case reflect.String:
f.SetString(v)
case reflect.Bool:
if v == trueString {
f.SetBool(true)
}
case reflect.Int:
n, _ := strconv.Atoi(v)
f.SetInt(int64(n))
}
}
return nil
}
================================================
FILE: pkg/gofr/cmd/request_test.go
================================================
package cmd
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestRequest_Bind(t *testing.T) {
// TODO: Only fields starting with Capital letter can be 'bind' right now.
r := NewRequest([]string{"command", "-Name=gofr", "-Valid=true", "-Value=12", "-test", "--name=Gofr", ""})
assert.Equal(t, "gofr", r.Param("Name"), "TEST Failed.\n Unable to read param from request")
assert.Equal(t, "true", r.Param("test"), "TEST Failed.\n Unable to read param from request")
assert.Equal(t, "12", r.PathParam("Value"), "TEST Failed.\n Unable to read param from request")
assert.Equal(t, "Gofr", r.PathParam("name"), "TEST Failed.\n Unable to read param from request")
// Testing string, bool, int
a := struct {
Name string
Valid bool
Value int
}{}
_ = r.Bind(&a)
if a.Name != "gofr" || !a.Valid || a.Value != 12 {
t.Errorf("TEST Failed.\nGot: %v\n%s", a, "Request Bind error")
}
hostName := r.HostName()
ctx := r.Context()
osHostName, _ := os.Hostname()
assert.NotNil(t, ctx, "TEST Failed.\n context should not be nil.")
assert.Equal(t, osHostName, hostName, "TEST Failed.\n Hostname did not match.")
}
func TestRequest_WithOneArg(t *testing.T) {
r := NewRequest([]string{"-"})
req := &Request{
flags: make(map[string]bool),
params: make(map[string]string),
}
assert.Equal(t, req, r, "TEST Failed.\n Hostname did not match.")
}
func TestHostName(t *testing.T) {
r := &Request{}
// Get the hostname using os.Hostname()
hostname, err := os.Hostname()
if err != nil {
t.Fatalf("Error getting hostname: %v", err)
}
// Get the hostname from the mock request
result := r.HostName()
assert.Equal(t, hostname, result, "TestHostName Failed!")
}
func Test_Params(t *testing.T) {
args := []string{"--category=books,electronics", "--tag=tech,science"}
r := NewRequest(args)
expectedCategories := []string{"books", "electronics"}
expectedTags := []string{"tech", "science"}
assert.ElementsMatch(t, expectedCategories, r.Params("category"), "expected all values of 'category' to match")
assert.ElementsMatch(t, expectedTags, r.Params("tag"), "expected all values of 'tag' to match")
assert.Empty(t, r.Params("nonexistent"), "expected empty slice for none-existent query param")
}
================================================
FILE: pkg/gofr/cmd/responder.go
================================================
package cmd
import (
"fmt"
"os"
)
type Responder struct{}
func (*Responder) Respond(data any, err error) {
// TODO - provide proper exit codes here. Using os.Exit directly is a problem for tests.
if data != nil {
fmt.Fprintln(os.Stdout, data)
}
if err != nil {
fmt.Fprintln(os.Stderr, err)
}
}
================================================
FILE: pkg/gofr/cmd/responder_test.go
================================================
package cmd
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
)
func TestResponder_Respond(t *testing.T) {
r := Responder{}
out := testutil.StdoutOutputForFunc(func() {
r.Respond("data", nil)
})
err := testutil.StderrOutputForFunc(func() {
r.Respond(nil, errors.New("error")) //nolint:err113 // We are testing if a dynamic error would work.
})
assert.Equal(t, "data\n", out, "TEST Failed.\n", "Responder stdout output")
assert.Equal(t, "error\n", err, "TEST Failed.\n", "Responder stderr output")
}
================================================
FILE: pkg/gofr/cmd/terminal/colors.go
================================================
package terminal
const (
Black = iota
Red
Green
Yellow
Blue
Magenta
Cyan
White
BrightBlack
BrightRed
BrightGreen
BrightYellow
BrightBlue
BrightMagenta
BrightCyan
BrightWhite
)
func (o *Out) SetColor(colorCode int) {
o.Printf(csi+"38;5;%d"+"m", colorCode)
}
func (o *Out) ResetColor() {
o.Print(csi + "0m")
}
================================================
FILE: pkg/gofr/cmd/terminal/output.go
================================================
package terminal
import (
"fmt"
"io"
"os"
"strings"
"golang.org/x/term"
)
type Output interface {
AltScreen()
ChangeScrollingRegion(top int, bottom int)
ClearLine()
ClearLineLeft()
ClearLineRight()
ClearLines(n int)
ClearScreen()
CursorBack(n int)
CursorDown(n int)
CursorForward(n int)
CursorNextLine(n int)
CursorPrevLine(n int)
CursorUp(n int)
DeleteLines(n int)
ExitAltScreen()
HideCursor()
InsertLines(n int)
MoveCursor(row int, column int)
Print(messages ...any)
Printf(format string, args ...any)
Println(messages ...any)
Reset()
ResetColor()
RestoreCursorPosition()
RestoreScreen()
SaveCursorPosition()
SaveScreen()
SetColor(colorCode int)
SetWindowTitle(title string)
ShowCursor()
getSize() (int, int, error)
}
// terminal stores the UNIX file descriptor and isTerminal check for the tty.
type terminal struct {
fd uintptr
isTerminal bool
}
// Out manages the cli outputs that is user facing with many functionalities
// to manage and control the TUI (Terminal User Interface).
type Out struct {
terminal
out io.Writer
}
func New() *Out {
o := &Out{out: os.Stdout}
o.fd, o.isTerminal = getTerminalInfo(o.out)
return o
}
func getTerminalInfo(in io.Writer) (inFd uintptr, isTerminalIn bool) {
if file, ok := in.(*os.File); ok {
inFd = file.Fd()
isTerminalIn = term.IsTerminal(int(inFd))
}
return inFd, isTerminalIn
}
func (o *Out) getSize() (width, height int, err error) {
return term.GetSize(int(o.fd))
}
const (
// escape character to start any control or escape sequence.
escape = string('\x1b')
// csi Control Sequence Introducer.
csi = escape + "["
// osc Operating System Command.
osc = escape + "]"
)
// Sequence definitions.
const (
moveCursorUp = iota + 1
clearScreen
// Cursor positioning.
cursorUpSeq = "%dA"
cursorDownSeq = "%dB"
cursorForwardSeq = "%dC"
cursorBackSeq = "%dD"
cursorNextLineSeq = "%dE"
cursorPreviousLineSeq = "%dF"
cursorPositionSeq = "%d;%dH"
eraseDisplaySeq = "%dJ"
eraseLineSeq = "%dK"
saveCursorPositionSeq = "s"
restoreCursorPositionSeq = "u"
changeScrollingRegionSeq = "%d;%dr"
insertLineSeq = "%dL"
deleteLineSeq = "%dM"
// Explicit values erasing lines.
eraseLineRightSeq = "0K"
eraseLineLeftSeq = "1K"
eraseEntireLineSeq = "2K"
// Screen.
restoreScreenSeq = "?47l"
saveScreenSeq = "?47h"
altScreenSeq = "?1049h"
exitAltScreenSeq = "?1049l"
setWindowTitleSeq = "2;%s"
showCursorSeq = "?25h"
hideCursorSeq = "?25l"
)
// Reset the terminal to its default style, removing any active styles.
func (o *Out) Reset() {
fmt.Fprint(o.out, csi+"0"+"m")
}
// RestoreScreen restores a previously saved screen state.
func (o *Out) RestoreScreen() {
fmt.Fprint(o.out, csi+restoreScreenSeq)
}
// SaveScreen saves the screen state.
func (o *Out) SaveScreen() {
fmt.Fprint(o.out, csi+saveScreenSeq)
}
// AltScreen switches to the alternate screen buffer. The former view can be
// restored with ExitAltScreen().
func (o *Out) AltScreen() {
fmt.Fprint(o.out, csi+altScreenSeq)
}
// ExitAltScreen exits the alternate screen buffer and returns to the former
// terminal view.
func (o *Out) ExitAltScreen() {
fmt.Fprint(o.out, csi+exitAltScreenSeq)
}
// ClearScreen clears the visible portion of the terminal.
func (o *Out) ClearScreen() {
fmt.Fprintf(o.out, csi+eraseDisplaySeq, clearScreen)
o.MoveCursor(1, 1)
}
// MoveCursor moves the cursor to a given position.
func (o *Out) MoveCursor(row, column int) {
fmt.Fprintf(o.out, csi+cursorPositionSeq, row, column)
}
// HideCursor hides the cursor.
func (o *Out) HideCursor() {
fmt.Fprint(o.out, csi+hideCursorSeq)
}
// ShowCursor shows the cursor.
func (o *Out) ShowCursor() {
fmt.Fprint(o.out, csi+showCursorSeq)
}
// SaveCursorPosition saves the cursor position.
func (o *Out) SaveCursorPosition() {
fmt.Fprint(o.out, csi+saveCursorPositionSeq)
}
// RestoreCursorPosition restores a saved cursor position.
func (o *Out) RestoreCursorPosition() {
fmt.Fprint(o.out, csi+restoreCursorPositionSeq)
}
// CursorUp moves the cursor up a given number of lines.
func (o *Out) CursorUp(n int) {
fmt.Fprintf(o.out, csi+cursorUpSeq, n)
}
// CursorDown moves the cursor down a given number of lines.
func (o *Out) CursorDown(n int) {
fmt.Fprintf(o.out, csi+cursorDownSeq, n)
}
// CursorForward moves the cursor up a given number of lines.
func (o *Out) CursorForward(n int) {
fmt.Fprintf(o.out, csi+cursorForwardSeq, n)
}
// CursorBack moves the cursor backwards a given number of cells.
func (o *Out) CursorBack(n int) {
fmt.Fprintf(o.out, csi+cursorBackSeq, n)
}
// CursorNextLine moves the cursor down a given number of lines and places it at
// the beginning of the line.
func (o *Out) CursorNextLine(n int) {
fmt.Fprintf(o.out, csi+cursorNextLineSeq, n)
}
// CursorPrevLine moves the cursor up a given number of lines and places it at
// the beginning of the line.
func (o *Out) CursorPrevLine(n int) {
fmt.Fprintf(o.out, csi+cursorPreviousLineSeq, n)
}
// ClearLine clears the current line.
func (o *Out) ClearLine() {
fmt.Fprint(o.out, csi+eraseEntireLineSeq)
}
// ClearLineLeft clears the line to the left of the cursor.
func (o *Out) ClearLineLeft() {
fmt.Fprint(o.out, csi+eraseLineLeftSeq)
}
// ClearLineRight clears the line to the right of the cursor.
func (o *Out) ClearLineRight() {
fmt.Fprint(o.out, csi+eraseLineRightSeq)
}
// ClearLines clears a given number of lines.
func (o *Out) ClearLines(n int) {
clearLine := fmt.Sprintf(csi+eraseLineSeq, clearScreen)
cursorUp := fmt.Sprintf(csi+cursorUpSeq, moveCursorUp)
fmt.Fprint(o.out, clearLine+strings.Repeat(cursorUp+clearLine, n))
}
// ChangeScrollingRegion sets the scrolling region of the terminal.
func (o *Out) ChangeScrollingRegion(top, bottom int) {
fmt.Fprintf(o.out, csi+changeScrollingRegionSeq, top, bottom)
}
// InsertLines inserts the given number of lines at the top of the scrollable
// region, pushing lines below down.
func (o *Out) InsertLines(n int) {
fmt.Fprintf(o.out, csi+insertLineSeq, n)
}
// DeleteLines deletes the given number of lines, pulling any lines in
// the scrollable region below up.
func (o *Out) DeleteLines(n int) {
fmt.Fprintf(o.out, csi+deleteLineSeq, n)
}
// SetWindowTitle sets the terminal window title.
func (o *Out) SetWindowTitle(title string) {
fmt.Fprintf(o.out, osc+setWindowTitleSeq, title)
}
================================================
FILE: pkg/gofr/cmd/terminal/output_test.go
================================================
package terminal
import (
"bytes"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewOutput(t *testing.T) {
// initialize a new standard output stream.
o := New()
assert.Equal(t, os.Stdout, o.out)
assert.Equal(t, uintptr(1), o.fd)
// for tests, the os.Stdout do not directly outputs to the terminal.
assert.False(t, o.isTerminal)
}
func tempOutput(t *testing.T) *Out {
t.Helper()
var b bytes.Buffer
return &Out{out: &b}
}
func validate(t *testing.T, o *Out, exp string) {
t.Helper()
out := o.out.(*bytes.Buffer)
b := out.Bytes()
assert.Equal(t, exp, string(b),
"output does not match, expected %s, got %s",
strings.ReplaceAll(exp, "\x1b", "\\x1b"),
string(bytes.ReplaceAll(b, []byte("\x1b"), []byte("\\x1b"))))
}
func TestReset(t *testing.T) {
o := tempOutput(t)
o.Reset()
validate(t, o, "\x1b[0m")
}
func TestRestoreScreen(t *testing.T) {
o := tempOutput(t)
o.RestoreScreen()
validate(t, o, "\x1b[?47l")
}
func TestSaveScreen(t *testing.T) {
o := tempOutput(t)
o.SaveScreen()
validate(t, o, "\x1b[?47h")
}
func TestAltScreen(t *testing.T) {
o := tempOutput(t)
o.AltScreen()
validate(t, o, "\x1b[?1049h")
}
func TestExitAltScreen(t *testing.T) {
o := tempOutput(t)
o.ExitAltScreen()
validate(t, o, "\x1b[?1049l")
}
func TestClearScreen(t *testing.T) {
o := tempOutput(t)
o.ClearScreen()
validate(t, o, "\x1b[2J\x1b[1;1H")
}
func TestMoveCursor(t *testing.T) {
o := tempOutput(t)
o.MoveCursor(2, 2)
validate(t, o, "\x1b[2;2H")
}
func TestHideCursor(t *testing.T) {
o := tempOutput(t)
o.HideCursor()
validate(t, o, "\x1b[?25l")
}
func TestShowCursor(t *testing.T) {
o := tempOutput(t)
o.ShowCursor()
validate(t, o, "\x1b[?25h")
}
func TestSaveCursorPosition(t *testing.T) {
o := tempOutput(t)
o.SaveCursorPosition()
validate(t, o, "\x1b[s")
}
func TestRestoreCursorPosition(t *testing.T) {
o := tempOutput(t)
o.RestoreCursorPosition()
validate(t, o, "\x1b[u")
}
func TestCursorUp(t *testing.T) {
o := tempOutput(t)
o.CursorUp(2)
validate(t, o, "\x1b[2A")
}
func TestCursorDown(t *testing.T) {
o := tempOutput(t)
o.CursorDown(2)
validate(t, o, "\x1b[2B")
}
func TestCursorForward(t *testing.T) {
o := tempOutput(t)
o.CursorForward(2)
validate(t, o, "\x1b[2C")
}
func TestCursorBack(t *testing.T) {
o := tempOutput(t)
o.CursorBack(2)
validate(t, o, "\x1b[2D")
}
func TestCursorNextLine(t *testing.T) {
o := tempOutput(t)
o.CursorNextLine(2)
validate(t, o, "\x1b[2E")
}
func TestCursorPrevLine(t *testing.T) {
o := tempOutput(t)
o.CursorPrevLine(2)
validate(t, o, "\x1b[2F")
}
func TestClearLine(t *testing.T) {
o := tempOutput(t)
o.ClearLine()
validate(t, o, "\x1b[2K")
}
func TestClearLineLeft(t *testing.T) {
o := tempOutput(t)
o.ClearLineLeft()
validate(t, o, "\x1b[1K")
}
func TestClearLineRight(t *testing.T) {
o := tempOutput(t)
o.ClearLineRight()
validate(t, o, "\x1b[0K")
}
func TestClearLines(t *testing.T) {
o := tempOutput(t)
o.ClearLines(2)
validate(t, o, "\x1b[2K\x1b[1A\x1b[2K\x1b[1A\x1b[2K")
}
func TestChangeScrollingRegion(t *testing.T) {
o := tempOutput(t)
o.ChangeScrollingRegion(2, 1)
validate(t, o, "\x1b[2;1r")
}
func TestInsertLines(t *testing.T) {
o := tempOutput(t)
o.InsertLines(2)
validate(t, o, "\x1b[2L")
}
func TestDeleteLines(t *testing.T) {
o := tempOutput(t)
o.DeleteLines(2)
validate(t, o, "\x1b[2M")
}
func TestSetWindowTitle(t *testing.T) {
o := tempOutput(t)
o.SetWindowTitle("test title")
validate(t, o, "\x1b]2;test title")
}
================================================
FILE: pkg/gofr/cmd/terminal/printers.go
================================================
package terminal
import (
"fmt"
)
func (o *Out) Printf(format string, args ...any) {
fmt.Fprintf(o.out, format, args...)
}
func (o *Out) Print(messages ...any) {
fmt.Fprint(o.out, messages...)
}
func (o *Out) Println(messages ...any) {
fmt.Fprintln(o.out, messages...)
}
================================================
FILE: pkg/gofr/cmd/terminal/printers_test.go
================================================
package terminal
import (
"bytes"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestOutput_Printf(t *testing.T) {
var buf bytes.Buffer
output := Out{out: &buf}
format := "Hello, %s!"
args := "world"
output.Printf(format, args)
expectedString := fmt.Sprintf(format, args)
assert.Equal(t, expectedString, buf.String(), "Printf: unexpected written string. Expected: %s, got: %s", expectedString, buf.String())
}
func TestOutput_Print(t *testing.T) {
var buf bytes.Buffer
output := Out{out: &buf}
message := "Hello, world!"
output.Print(message)
assert.Equal(t, message, buf.String(), "Printf: unexpected written string. Expected: %s, got: %s", message, buf.String())
}
func TestOutput_Println(t *testing.T) {
var buf bytes.Buffer
output := Out{out: &buf}
message := "Hello, world!"
output.Println(message)
expectedString := message + "\n"
assert.Equal(t, expectedString, buf.String(), "Printf: unexpected written string. Expected: %s, got: %s", expectedString, buf.String())
}
================================================
FILE: pkg/gofr/cmd/terminal/progress.go
================================================
package terminal
import (
"errors"
"fmt"
"strings"
"sync"
)
var (
errTermSize = errors.New("error getting terminal size, could not initialize progress bar")
errInvalidTotal = errors.New("error initializing progress bar, total should be > 0")
)
type ProgressBar struct {
stream Output
current int64
total int64
tWidth int
mu sync.Mutex
}
type Term interface {
IsTerminal(fd int) bool
GetSize(fd int) (width, height int, err error)
}
func NewProgressBar(out Output, total int64) (*ProgressBar, error) {
p := &ProgressBar{
stream: out,
total: total,
tWidth: 0,
current: 0,
}
w, _, err := out.getSize()
if err != nil {
return p, errTermSize
}
if total < 0 {
p.total = 0
return p, errInvalidTotal
}
p.tWidth = w
return p, nil
}
func (p *ProgressBar) Incr(i int64) bool {
// acquiring locks to synchronize the painting of the progress bar
// and incrementing the current progress by the increment value.
p.mu.Lock()
defer p.mu.Unlock()
if p.current < p.total {
p.current += i
p.current = min(p.current, p.total)
p.updateProgressBar()
}
return p.current != p.total
}
func (p *ProgressBar) updateProgressBar() {
// perform the TUI update of the progress bar
p.stream.Print("\r")
pString := p.getString()
p.stream.Print(pString)
if p.current >= p.total {
p.stream.Print("\n")
}
}
const (
// max rounded percentage.
maxRP = 50
// minimum terminal width required to render a progress bar.
minTermWidth = 110
)
func (p *ProgressBar) getString() string {
if p.current <= 0 && p.total <= 0 {
return ""
}
percentage := float64(p.current) / float64(p.total) * 100
numbersBox := fmt.Sprintf("%.3f%c", percentage, '%')
if p.tWidth < minTermWidth {
return numbersBox
}
return getProgressBox(percentage) + numbersBox
}
func getProgressBox(percentage float64) string {
var pbBox string
roundedPercent := int(percentage) / 2
numSpaces := 0
if maxRP-roundedPercent > 0 {
numSpaces = maxRP - roundedPercent
}
if roundedPercent > 0 && roundedPercent < 50 {
pbBox = fmt.Sprintf("[%s%s%s] ", strings.Repeat("█", roundedPercent-1), "░", strings.Repeat(" ", numSpaces))
} else if roundedPercent <= 0 {
pbBox = fmt.Sprintf("[%s] ", strings.Repeat(" ", numSpaces))
} else {
pbBox = fmt.Sprintf("[%s%s] ", strings.Repeat("█", roundedPercent), strings.Repeat(" ", numSpaces))
}
return pbBox
}
================================================
FILE: pkg/gofr/cmd/terminal/progress_test.go
================================================
package terminal
import (
"bytes"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestProgressBar_SuccessCases(t *testing.T) {
total := int64(100)
var out bytes.Buffer
stream := &Out{terminal{isTerminal: true, fd: 1}, &out}
bar := ProgressBar{
stream: stream,
current: 0,
total: 100,
mu: sync.Mutex{},
}
// Mock terminal size
bar.tWidth = 120
// Increment the progress bar
bar.Incr(10)
// Verify the output
expectedOutput := "\r[████░ ] 10.000%"
assert.Equal(t, expectedOutput, out.String())
// Increment the progress bar to completion
bar.Incr(total - 10)
// Verify the completion output
expectedCompletion := "\r[████░ ] 10.000%\r" +
"[██████████████████████████████████████████████████] 100.000%\n"
assert.Equal(t, expectedCompletion, out.String())
}
func TestProgressBar_Fail(t *testing.T) {
var out bytes.Buffer
stream := &Out{terminal{isTerminal: true, fd: 1}, &out}
bar, err := NewProgressBar(stream, int64(-1))
require.Error(t, err)
require.ErrorIs(t, err, errTermSize)
assert.NotNil(t, bar)
}
func TestProgressBar_Incr(t *testing.T) {
var out bytes.Buffer
stream := &Out{terminal{isTerminal: true, fd: 1}, &out}
bar := ProgressBar{stream: stream, current: 0, total: 100, mu: sync.Mutex{}}
// doing this as while calculating terminal size, the code will not
// be able to determine its width since we are not attaching an actual
// terminal for testing
bar.tWidth = 120
// Increment the progress by 20
b := bar.Incr(int64(20))
expectedOut := "\r[█████████░ ] 20.000%"
assert.True(t, b)
assert.Equal(t, int64(20), bar.current)
assert.Equal(t, expectedOut, out.String())
// increment the progress by 100 units.
b = bar.Incr(int64(100))
expectedOut = "\r[█████████░ ] 20.000%\r" +
"[██████████████████████████████████████████████████] 100.000%\n"
assert.False(t, b)
assert.Equal(t, expectedOut, out.String())
assert.Equal(t, int64(100), bar.current)
}
func TestProgressBar_getString(t *testing.T) {
testCases := []struct {
desc string
current int64
tWidth int
total int64
expectedOut string
}{
{
desc: "current and total negative",
current: -1,
total: -1,
tWidth: 120,
expectedOut: "",
},
{
desc: "terminal width < 110",
current: 20,
total: 100,
expectedOut: "20.000%",
},
{
desc: "0% progress, 50 spaces",
tWidth: 120,
current: 0,
total: 100,
expectedOut: "[ ] 0.000%",
},
{
desc: "100% progress",
tWidth: 120,
current: 100,
total: 100,
expectedOut: "[██████████████████████████████████████████████████] 100.000%",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
p := ProgressBar{
current: tc.current,
total: tc.total,
tWidth: tc.tWidth,
}
out := p.getString()
assert.Equal(t, tc.expectedOut, out)
})
}
}
================================================
FILE: pkg/gofr/cmd/terminal/spinner.go
================================================
package terminal
import (
"context"
"time"
)
// Spinner is a TUI component that displays a loading spinner which can be used to
// denote a running background process.
type Spinner struct {
// frames denote the frames of the spinner that displays in continiuation
frames []string
// fps is the speed at which the spinner frames are displayed
fps time.Duration
// outStream is the Output stream to which the spinner frames are printed onto
outStream Output
// started denotes whether the spinner has started spinning and ticker
// is the time.Ticker for the continuous time update for the spinner.
started bool
ticker *time.Ticker
}
func NewDotSpinner(o Output) *Spinner {
return &Spinner{
frames: []string{"⣾ ", "⣽ ", "⣻ ", "⢿ ", "⡿ ", "⣟ ", "⣯ ", "⣷ "},
fps: time.Second / 10,
outStream: o,
}
}
func NewPulseSpinner(o Output) *Spinner {
return &Spinner{
frames: []string{"█", "▓", "▒", "░"},
fps: time.Second / 4,
outStream: o,
}
}
func NewGlobeSpinner(o Output) *Spinner {
return &Spinner{
frames: []string{"🌍", "🌎", "🌏"},
fps: time.Second / 4,
outStream: o,
}
}
func (s *Spinner) Spin(ctx context.Context) *Spinner {
t := time.NewTicker(s.fps)
s.ticker = t
s.started = true
i := 0
s.outStream.HideCursor()
go func() {
for range t.C {
select {
case <-ctx.Done():
t.Stop()
default:
if !s.started {
break
}
s.outStream.Print("\r")
s.outStream.Printf("%s", s.frames[i%len(s.frames)])
i++
}
}
}()
return s
}
func (s *Spinner) Stop() {
s.started = false
s.ticker.Stop()
s.outStream.ClearLine()
s.outStream.ShowCursor()
s.outStream.CursorBack(1)
}
================================================
FILE: pkg/gofr/cmd/terminal/spinner_test.go
================================================
package terminal
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestSpinner(t *testing.T) {
var (
waitTime = 1 * time.Second
ctx = t.Context()
)
// Testing Dot spinner
b := &bytes.Buffer{}
out := &Out{out: b}
spinner := NewDotSpinner(out)
// Start the spinner
spinner.Spin(ctx)
// Let it run for a bit
time.Sleep(waitTime)
// Stop the spinner
spinner.Stop()
// Check if output contains spinner frames
outputStr := b.String()
assert.NotEmpty(t, outputStr)
// Testing Globe Spinner
b = &bytes.Buffer{}
out = &Out{out: b}
spinner = NewGlobeSpinner(out)
// Start the spinner
spinner.Spin(ctx)
// Let it run for a bit
time.Sleep(waitTime)
// Stop the spinner
spinner.Stop()
// Check if output contains spinner frames
outputStr = b.String()
assert.NotEmpty(t, outputStr)
// Testing Pulse Spinner
b = &bytes.Buffer{}
out = &Out{out: b}
spinner = NewPulseSpinner(out)
// Start the spinner
spinner.Spin(ctx)
// Let it run for a bit
time.Sleep(waitTime)
// Stop the spinner
spinner.Stop()
// Check if output contains spinner frames
outputStr = b.String()
fmt.Println(outputStr)
assert.NotEmpty(t, outputStr)
}
func TestSpinner_contextDone(t *testing.T) {
b := &bytes.Buffer{}
out := &Out{out: b}
spinner := NewDotSpinner(out)
ctx, cancel := context.WithCancel(t.Context())
// start the spinner
spinner.Spin(ctx)
// let the spinner start spinning
time.Sleep(100 * time.Millisecond)
cancel()
select {
case <-spinner.ticker.C:
t.Error("ticker should have been stopped after cancel")
case <-time.After(1 * time.Second):
// successful case as ticker did not send a tick
return
}
}
================================================
FILE: pkg/gofr/cmd.go
================================================
package gofr
import (
"fmt"
"os"
"strings"
cmd2 "gofr.dev/pkg/gofr/cmd"
"gofr.dev/pkg/gofr/cmd/terminal"
"gofr.dev/pkg/gofr/container"
)
type cmd struct {
routes []route
out terminal.Output
}
type route struct {
pattern string
handler Handler
description string
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 {
Command string
}
func (e ErrCommandNotFound) Error() string {
return fmt.Sprintf("'%s' is not a valid command.", e.Command)
}
func (cmd *cmd) Run(c *container.Container) {
args := os.Args[1:] // First one is command itself
subCommand, showHelp, firstArg := parseArgs(args)
if showHelp && subCommand == "" {
cmd.printHelp()
return
}
r := cmd.handler(subCommand)
ctx := newCMDContext(&cmd2.Responder{}, cmd2.NewRequest(args), c, cmd.out)
commandForError := getCommandForError(subCommand, firstArg)
if cmd.noCommandResponse(r, ctx, commandForError) {
return
}
if showHelp {
cmd.out.Println(r.help)
return
}
ctx.responder.Respond(r.handler(ctx))
}
// parseArgs parses command line arguments and returns subCommand, showHelp flag, and firstArg.
func parseArgs(args []string) (subCommand string, showHelp bool, firstArg string) {
subCommand = ""
showHelp = false
for _, a := range args {
if a == "" {
continue // This takes care of cases where subCommand has multiple spaces in between.
}
if a == "-h" || a == "--help" {
showHelp = true
continue
}
if firstArg == "" {
firstArg = a
}
if a[0] != '-' {
subCommand = subCommand + " " + a
}
}
return subCommand, showHelp, firstArg
}
// getCommandForError returns the command string to use in error messages.
func getCommandForError(subCommand, firstArg string) string {
commandForError := strings.TrimSpace(subCommand)
if commandForError == "" && firstArg != "" {
commandForError = firstArg
}
return commandForError
}
// noCommandResponse responds with error when no route with the given subcommand is not found or handler is nil.
func (cmd *cmd) noCommandResponse(r *route, ctx *Context, subCommand string) bool {
if r == nil {
ctx.responder.Respond(nil, ErrCommandNotFound{Command: strings.TrimSpace(subCommand)})
fmt.Println()
cmd.printHelp()
return true
}
if r.handler == nil {
ctx.responder.Respond(nil, ErrCommandNotFound{Command: strings.TrimSpace(subCommand)})
return true
}
return false
}
func (cmd *cmd) handler(path string) *route {
// Trim leading dashes
path = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(path, "--"), "-"))
// Iterate over the routes to find a matching handler
for _, r := range cmd.routes {
if strings.HasPrefix(path, r.pattern) {
return &r
}
}
// Return nil if no handler matches
return nil
}
// AddDescription adds the description text for a specified subcommand.
func AddDescription(descString string) Options {
return func(r *route) {
r.description = descString
}
}
// AddHelp adds the helper text for the given subcommand
// this is displayed when -h or --help option/flag is provided.
func AddHelp(helperString string) Options {
return func(r *route) {
r.help = helperString
}
}
// addRoute adds a new route to cmd's list of routes.
func (cmd *cmd) addRoute(pattern string, handler Handler, options ...Options) {
// Define restricted characters
restrictedChars := "$^"
// Check if the pattern contains any restricted characters
for i := 0; i < len(pattern); i++ {
if strings.ContainsRune(restrictedChars, rune(pattern[i])) {
fmt.Println("found a restricted character in the command while registering with GoFr:", pattern[i])
return
}
}
tempRoute := route{
pattern: pattern,
handler: handler,
}
for _, opt := range options {
opt(&tempRoute)
}
cmd.routes = append(cmd.routes, tempRoute)
}
func (cmd *cmd) printHelp() {
fmt.Println("Available commands:")
var maxPatternLen, maxDescLen int
for _, r := range cmd.routes {
if len(r.pattern) > maxPatternLen {
maxPatternLen = len(r.pattern)
}
if len(r.description) > maxDescLen {
maxDescLen = len(r.description)
}
}
for _, r := range cmd.routes {
fmt.Printf(" %-*s %-*s", maxPatternLen, r.pattern, maxDescLen, r.description)
if r.help != "" {
fmt.Printf(" Help: %s", r.help)
}
fmt.Println()
}
}
================================================
FILE: pkg/gofr/cmd_test.go
================================================
package gofr
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/cmd/terminal"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func Test_Run_SuccessCallRegisteredArgument(t *testing.T) {
os.Args = []string{"", "log"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
AddHelp("Custom helper documentation for log command"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile(".env", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called")
}
func Test_Run_SuccessSkipEmptySpaceAndMatchCommandWithSpace(t *testing.T) {
os.Args = []string{"", "", " ", "log"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called")
}
func Test_Run_SuccessCommandWithMultipleParameters(t *testing.T) {
os.Args = []string{"", "log", "-param=value", "-b", "-c"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
assert.Equal(t, "value", c.Request.Param("param"))
assert.Equal(t, "true", c.Request.Param("b"))
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message with parameters"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called")
}
func Test_Run_SuccessRouteWithSpecialCharacters(t *testing.T) {
testCases := []struct {
desc string
args []string
}{
{"special character !", []string{"", "command-with-special-characters!"}},
{"special character @", []string{"", "command-with-special-characters@"}},
{"special character #", []string{"", "command-with-special-characters#"}},
{"special character %", []string{"", "command-with-special-characters%"}},
{"special character &", []string{"", "command-with-special-characters&"}},
{"special character *", []string{"", "command-with-special-characters*"}},
}
for i, tc := range testCases {
os.Args = tc.args
c := cmd{}
c.addRoute(tc.args[1],
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Handles special character commands"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called", "TEST[%d] Failed.\n %s", i, tc.desc)
assert.NotContains(t, logs, fmt.Sprintf("'%s' is not a valid command.", tc.args[1]), "TEST[%d] Failed.\n %s", i, tc.desc)
}
}
func Test_Run_ErrorRouteWithSpecialCharacters(t *testing.T) {
testCases := []struct {
desc string
args []string
}{
{"special character $", []string{"", "command-with-special-characters$"}},
{"special character ^", []string{"", "command-with-special-characters^"}},
}
for i, tc := range testCases {
os.Args = tc.args
c := cmd{}
c.addRoute(tc.args[1],
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Handles special character commands"),
)
logs := testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.NotContains(t, logs, "handler called", "TEST[%d] Failed.\n %s", i, tc.desc)
assert.Contains(t, logs, fmt.Sprintf("'%s' is not a valid command.", tc.args[1]), "TEST[%d] Failed.\n %s", i, tc.desc)
}
}
func Test_Run_ErrorParamNotReadWithoutHyphen(t *testing.T) {
os.Args = []string{"", "log", "hello=world"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
assert.Empty(t, c.Request.Param("hello"))
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called")
}
func Test_Run_ErrorNotARegisteredCommand(t *testing.T) {
os.Args = []string{"", "log"}
c := cmd{}
logs := testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "'log' is not a valid command.")
}
func Test_Run_ErrorWhenOnlyParamAreGiven(t *testing.T) {
os.Args = []string{"", "-route"}
c := cmd{}
c.addRoute("-route",
func(c *Context) (any, error) {
c.Logger.Info("handler called of route -route")
return nil, nil
},
AddDescription("Route with hyphen"),
)
logs := testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "'-route' is not a valid command.")
assert.NotContains(t, logs, "handler called of route -route")
}
func Test_Run_ErrorRouteRegisteredButNilHandler(t *testing.T) {
os.Args = []string{"", "route"}
c := cmd{}
c.addRoute("route",
nil,
AddDescription("Nil handler route"),
)
logs := testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "'route' is not a valid command.")
}
func Test_Run_ErrorNoArgumentGiven(t *testing.T) {
errlog := ""
os.Args = []string{""}
c := cmd{}
out := testutil.StdoutOutputForFunc(func() {
errlog = testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
})
assert.Contains(t, errlog, "'' is not a valid command.")
assert.Contains(t, out, "Available commands:")
}
func Test_Run_SuccessCallInvalidHyphens(t *testing.T) {
os.Args = []string{"", "log", "-param=value", "-b", "-"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "handler called")
}
func Test_Run_HelpCommand(t *testing.T) {
os.Args = []string{"", "-h"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
AddHelp("logging messages to the terminal"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "Available commands:")
assert.Contains(t, logs, " log")
assert.Contains(t, logs, "Logs a message")
assert.Contains(t, logs, "Help: logging messages to the terminal")
}
func Test_Run_HelpCommandLong(t *testing.T) {
os.Args = []string{"", "--help"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
AddHelp("logging messages to the terminal"),
)
logs := testutil.StdoutOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
assert.Contains(t, logs, "Available commands:")
assert.Contains(t, logs, " log")
assert.Contains(t, logs, "Logs a message")
assert.Contains(t, logs, "Help: logging messages to the terminal")
}
func Test_Run_UnknownCommandShowsHelp(t *testing.T) {
errLogs := ""
os.Args = []string{"", "unknown"}
c := cmd{}
c.addRoute("log",
func(c *Context) (any, error) {
c.Logger.Info("handler called")
return nil, nil
},
AddDescription("Logs a message"),
AddHelp("logging messages to the terminal"),
)
logs := testutil.StdoutOutputForFunc(func() {
errLogs = testutil.StderrOutputForFunc(func() {
c.Run(container.NewContainer(config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))))
})
})
assert.Contains(t, errLogs, "'unknown' is not a valid command.")
assert.Contains(t, logs, "Available commands:")
assert.Contains(t, logs, " log")
assert.Contains(t, logs, "Logs a message")
assert.Contains(t, logs, "Help: logging messages to the terminal")
}
func Test_Run_handler_help(t *testing.T) {
var old []string
args := []string{"", "hello", "--help"}
old, os.Args = os.Args, args
t.Cleanup(func() {
os.Args = old
})
out := testutil.StdoutOutputForFunc(func() {
c := cmd{
out: terminal.New(),
}
c.addRoute("hello", func(_ *Context) (any, error) {
return "Hello", nil
}, AddHelp("this a helper string for hello sub command"))
c.Run(container.NewContainer(config.NewMockConfig(map[string]string{})))
})
// check that only help for the hello subcommand is printed
assert.Equal(t, "this a helper string for hello sub command\n", out)
}
================================================
FILE: pkg/gofr/config/config.go
================================================
package config
type Config interface {
Get(string) string
GetOrDefault(string, string) string
}
================================================
FILE: pkg/gofr/config/godotenv.go
================================================
package config
import (
"errors"
"fmt"
"io/fs"
"os"
"strings"
"github.com/joho/godotenv"
)
const (
defaultFileName = "/.env"
defaultOverrideFileName = "/.local.env"
)
type EnvLoader struct {
logger logger
}
type logger interface {
Warnf(format string, a ...any)
Infof(format string, a ...any)
Debugf(format string, a ...any)
Fatalf(format string, a ...any)
}
func NewEnvFile(configFolder string, logger logger) Config {
conf := &EnvLoader{logger: logger}
conf.read(configFolder)
return conf
}
func (e *EnvLoader) read(folder string) {
var (
defaultFile = folder + defaultFileName
overrideFile = folder + defaultOverrideFileName
env = e.Get("APP_ENV")
)
// Only Capture initial system environment before any file loading
initialEnv := e.captureInitialEnv()
err := godotenv.Load(defaultFile)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
e.logger.Fatalf("Failed to load config from file: %v, Err: %v", defaultFile, err)
}
e.logger.Warnf("Failed to load config from file: %v, Err: %v", defaultFile, err)
} else {
e.logger.Infof("Loaded config from file: %v", defaultFile)
}
if env != "" {
// If 'APP_ENV' is set to x, then GoFr will read '.env' from configs directory, and then it will be overwritten
// by configs present in file '.x.env'
overrideFile = fmt.Sprintf("%s/.%s.env", folder, env)
}
err = godotenv.Overload(overrideFile)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
e.logger.Fatalf("Failed to load config from file: %v, Err: %v", overrideFile, err)
}
} else {
e.logger.Infof("Loaded config from file: %v", overrideFile)
}
// Reload system environment variables to ensure they take precedence over values loaded from files.
// This is required because Overload replaces the original system variables, which we need to restore.
for key, envVar := range initialEnv {
os.Setenv(key, envVar)
}
}
// captureInitialEnv captures the initial system environment keys.
func (*EnvLoader) captureInitialEnv() map[string]string {
initialEnv := make(map[string]string)
for _, envVar := range os.Environ() {
key, value, found := strings.Cut(envVar, "=")
if found {
initialEnv[key] = value
}
}
return initialEnv
}
func (*EnvLoader) Get(key string) string {
return os.Getenv(key)
}
func (*EnvLoader) GetOrDefault(key, defaultValue string) string {
if val := os.Getenv(key); val != "" {
return val
}
return defaultValue
}
================================================
FILE: pkg/gofr/config/godotenv_test.go
================================================
package config
import (
"fmt"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/logging"
)
func clearAllEnv() {
for _, envVar := range os.Environ() {
key, _, _ := strings.Cut(envVar, "=")
_ = os.Unsetenv(key)
}
}
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
os.Exit(m.Run())
}
func Test_EnvSuccess(t *testing.T) {
clearAllEnv()
envData := map[string]string{
"DB_URL": "localhost:5432",
"API_KEY": "your_api_key_here",
"small_case": "small_case_value",
}
logger := logging.NewMockLogger(logging.DEBUG)
dir := t.TempDir()
// Call the function to create the .env file
createEnvFile(t, dir, ".env", envData)
env := NewEnvFile(dir, logger)
assert.Equal(t, "localhost:5432", env.Get("DB_URL"))
assert.Equal(t, "your_api_key_here", env.GetOrDefault("API_KEY", "xyz"))
assert.Equal(t, "test", env.GetOrDefault("DATABASE", "test"))
assert.Equal(t, "small_case_value", env.Get("small_case"))
}
func Test_EnvSuccess_AppEnv_Override(t *testing.T) {
clearAllEnv()
t.Setenv("APP_ENV", "prod")
envData := map[string]string{
"DB_URL": "localhost:5432",
}
dir := t.TempDir()
// Call the function to create the .env file
createEnvFile(t, dir, ".env", envData)
// override database url in '.prod.env' file to test if value if being overridden
createEnvFile(t, dir, ".prod.env", map[string]string{"DB_URL": "localhost:2001"})
logger := logging.NewMockLogger(logging.DEBUG)
env := NewEnvFile(dir, logger)
assert.Equal(t, "localhost:2001", env.Get("DB_URL"), "TEST Failed.\n godotenv success")
}
func Test_EnvSuccess_Local_Override(t *testing.T) {
clearAllEnv()
envData := map[string]string{
"API_KEY": "your_api_key_here",
}
dir := t.TempDir()
// Call the function to create the .env file
createEnvFile(t, dir, ".env", envData)
// override database url in '.prod.env' file to test if value if being overridden
createEnvFile(t, dir, ".local.env", map[string]string{"API_KEY": "overloaded_api_key"})
logger := logging.NewMockLogger(logging.DEBUG)
env := NewEnvFile(dir, logger)
assert.Equal(t, "overloaded_api_key", env.Get("API_KEY"), "TEST Failed.\n godotenv success")
}
func Test_EnvSuccess_SystemEnv_Override(t *testing.T) {
clearAllEnv()
// Set initial environment variables
envData := map[string]string{
"TEST_ENV": "env",
}
dir := t.TempDir()
// Create the .env file
createEnvFile(t, dir, ".env", envData)
// Create the override file
createEnvFile(t, dir, ".local.env", map[string]string{"TEST_ENV": "local"})
t.Setenv("TEST_ENV", "system")
logger := logging.NewMockLogger(logging.DEBUG)
env := NewEnvFile(dir, logger)
assert.Equal(t, "system", env.Get("TEST_ENV"), "TEST Failed.\n system env override")
}
func Test_EnvFailureWithHyphen(t *testing.T) {
clearAllEnv()
envData := map[string]string{
"KEY-WITH-HYPHEN": "DASH-VALUE",
"UNABLE_TO_LOAD": "VALUE",
}
logger := logging.NewMockLogger(logging.DEBUG)
dir := t.TempDir()
configFiles := []string{".env", ".local.env"}
for _, file := range configFiles {
createEnvFile(t, dir, file, envData)
env := NewEnvFile(dir, logger)
assert.Equal(t, "test", env.GetOrDefault("KEY-WITH-HYPHEN", "test"), "TEST Failed.\n godotenv failure with hyphen")
assert.Empty(t, env.Get("UNABLE_TO_LOAD"), "TEST Failed.\n godotenv failure with hyphen")
}
}
func createEnvFile(t *testing.T, dir, fileName string, envData map[string]string) {
t.Helper()
// Create or open the env file for writing
envFile, err := os.Create(dir + "/" + fileName)
if err != nil {
t.Fatalf("error creating %s file: %v", fileName, err)
}
defer envFile.Close()
// Write data to the env file
for key, value := range envData {
_, err := fmt.Fprintf(envFile, "%s=%s\n", key, value)
if err != nil {
t.Fatalf("unable to write to file: %v", err)
}
}
}
================================================
FILE: pkg/gofr/config/mock_config.go
================================================
package config
type mockConfig struct {
conf map[string]string
}
func NewMockConfig(configMap map[string]string) Config {
if configMap == nil {
configMap = make(map[string]string)
}
// setting telemetry false for running tests
configMap["GOFR_TELEMETRY"] = "false"
return &mockConfig{
conf: configMap,
}
}
func (m *mockConfig) Get(s string) string {
return m.conf[s]
}
func (m *mockConfig) GetOrDefault(s, d string) string {
res, ok := m.conf[s]
if !ok {
res = d
}
return res
}
================================================
FILE: pkg/gofr/config/mock_config_test.go
================================================
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_NewMockConfig(t *testing.T) {
cfg := NewMockConfig(map[string]string{"config": "value"})
assert.Equal(t, "value", cfg.Get("config"))
assert.Equal(t, "value1", cfg.GetOrDefault("config1", "value1"))
}
================================================
FILE: pkg/gofr/constants.go
================================================
package gofr
import "time"
const (
defaultPublicStaticDir = "static"
shutDownTimeout = 30 * time.Second
gofrTraceExporter = "gofr"
gofrTracerURL = "https://tracer.gofr.dev"
checkPortTimeout = 2 * time.Second
gofrHost = "https://gofr.dev"
startServerPing = "/api/ping/up"
shutServerPing = "/api/ping/down"
pingTimeout = 5 * time.Second
defaultTelemetry = "true"
defaultReflection = "false"
)
================================================
FILE: pkg/gofr/container/container.go
================================================
/*
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, GCS, Azure File Storage)
*/
package container
import (
"context"
"errors"
"strconv"
"strings"
"time"
_ "github.com/go-sql-driver/mysql" // This is required to be blank import
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/datasource/pubsub/google"
"gofr.dev/pkg/gofr/datasource/pubsub/kafka"
"gofr.dev/pkg/gofr/datasource/pubsub/mqtt"
"gofr.dev/pkg/gofr/datasource/redis"
"gofr.dev/pkg/gofr/datasource/sql"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/logging/remotelogger"
"gofr.dev/pkg/gofr/metrics"
"gofr.dev/pkg/gofr/metrics/exporters"
"gofr.dev/pkg/gofr/service"
"gofr.dev/pkg/gofr/version"
"gofr.dev/pkg/gofr/websocket"
)
const (
redisPubSubModeStreams = "streams"
redisPubSubModePubSub = "pubsub"
)
// Container is a collection of all common application level concerns. Things like Logger, Connection Pool for Redis
// etc. which is shared across is placed here.
type Container struct {
logging.Logger
appName string
appVersion string
Services map[string]service.HTTP
metricsManager metrics.Manager
PubSub pubsub.Client
WSManager *websocket.Manager
Redis Redis
SQL DB
Cassandra CassandraWithContext
Clickhouse Clickhouse
Mongo Mongo
Solr Solr
DGraph Dgraph
OpenTSDB OpenTSDB
ScyllaDB ScyllaDB
SurrealDB SurrealDB
ArangoDB ArangoDB
Elasticsearch Elasticsearch
Oracle OracleDB
Couchbase Couchbase
InfluxDB InfluxDB
KVStore KVStore
File file.FileSystem
}
func NewContainer(conf config.Config) *Container {
if conf == nil {
return &Container{}
}
c := &Container{
appName: conf.GetOrDefault("APP_NAME", "gofr-app"),
appVersion: conf.GetOrDefault("APP_VERSION", "dev"),
}
c.Create(conf)
return c
}
func (c *Container) Create(conf config.Config) {
if c.appName == "" {
c.appName = conf.GetOrDefault("APP_NAME", "gofr-app")
}
if c.appVersion == "" {
c.appVersion = conf.GetOrDefault("APP_VERSION", "dev")
}
if c.Logger == nil {
levelFetchConfig, err := strconv.Atoi(conf.GetOrDefault("REMOTE_LOG_FETCH_INTERVAL", "15"))
if err != nil {
levelFetchConfig = 15
}
c.Logger = remotelogger.New(logging.GetLevelFromString(conf.Get("LOG_LEVEL")), conf.Get("REMOTE_LOG_URL"),
time.Duration(levelFetchConfig)*time.Second)
if err != nil {
c.Logger.Error("invalid value for REMOTE_LOG_FETCH_INTERVAL. setting default of 15 sec.")
}
}
c.Logger.Debug("Container is being created")
c.metricsManager = metrics.NewMetricsManager(exporters.Prometheus(c.GetAppName(), c.GetAppVersion()), c.Logger)
exporters.SendFrameworkStartupTelemetry(c.GetAppName(), c.GetAppVersion())
// Register framework metrics
c.registerFrameworkMetrics()
// Populating an instance of app_info with the app details, the value is set as 1 to depict the no. of instances
c.Metrics().SetGauge("app_info", 1,
"app_name", c.GetAppName(), "app_version", c.GetAppVersion(), "framework_version", version.Framework)
c.Redis = redis.NewClient(conf, c.Logger, c.metricsManager)
c.SQL = sql.NewSQL(conf, c.Logger, c.metricsManager)
c.createPubSub(conf)
c.File = file.NewLocalFileSystem(c.Logger)
c.WSManager = websocket.New()
}
func (c *Container) createPubSub(conf config.Config) {
switch strings.ToUpper(conf.Get("PUBSUB_BACKEND")) {
case "KAFKA":
c.createKafkaPubSub(conf)
case "GOOGLE":
c.createGooglePubSub(conf)
case "MQTT":
c.PubSub = c.createMqttPubSub(conf)
case "REDIS":
c.createRedisPubSub(conf)
}
}
func (c *Container) Close() error {
var err error
if !isNil(c.SQL) {
err = errors.Join(err, c.SQL.Close())
}
if !isNil(c.Redis) {
err = errors.Join(err, c.Redis.Close())
}
if !isNil(c.PubSub) {
err = errors.Join(err, c.PubSub.Close())
}
for _, conn := range c.WSManager.ListConnections() {
c.WSManager.CloseConnection(conn)
}
return err
}
func (c *Container) createMqttPubSub(conf config.Config) pubsub.Client {
var qos byte
port, err := strconv.Atoi(conf.GetOrDefault("MQTT_PORT", "0"))
if err != nil {
c.Logger.Error("Invalid value for MQTT_PORT, using default: 0")
}
order, _ := strconv.ParseBool(conf.GetOrDefault("MQTT_MESSAGE_ORDER", "false"))
retrieveRetained, _ := strconv.ParseBool(conf.GetOrDefault("MQTT_RETRIEVE_RETAINED", "false"))
keepAlive, err := time.ParseDuration(conf.Get("MQTT_KEEP_ALIVE"))
if err != nil {
keepAlive = 30 * time.Second
c.Logger.Debug("MQTT_KEEP_ALIVE is not set or invalid, setting it to 30 seconds")
}
switch conf.Get("MQTT_QOS") {
case "1":
qos = 1
case "2":
qos = 2
default:
qos = 0
}
configs := &mqtt.Config{
Protocol: conf.GetOrDefault("MQTT_PROTOCOL", "tcp"), // using tcp as default method to connect to broker
Hostname: conf.Get("MQTT_HOST"),
Port: port,
Username: conf.Get("MQTT_USER"),
Password: conf.Get("MQTT_PASSWORD"),
ClientID: conf.Get("MQTT_CLIENT_ID_SUFFIX"),
QoS: qos,
Order: order,
RetrieveRetained: retrieveRetained,
KeepAlive: keepAlive,
CloseTimeout: 0 * time.Millisecond,
}
return mqtt.New(configs, c.Logger, c.metricsManager)
}
// GetHTTPService returns registered HTTP services.
// HTTP services are registered from AddHTTPService method of GoFr object.
func (c *Container) GetHTTPService(serviceName string) service.HTTP {
return c.Services[serviceName]
}
func (c *Container) Metrics() metrics.Manager {
return c.metricsManager
}
func (c *Container) registerFrameworkMetrics() {
// system info metrics
c.Metrics().NewGauge("app_info", "Info for app_name, app_version and framework_version.")
c.Metrics().NewGauge("app_go_routines", "Number of Go routines running.")
c.Metrics().NewGauge("app_sys_memory_alloc", "Number of bytes allocated for heap objects.")
c.Metrics().NewGauge("app_sys_total_alloc", "Number of cumulative bytes allocated for heap objects.")
c.Metrics().NewGauge("app_go_numGC", "Number of completed Garbage Collector cycles.")
c.Metrics().NewGauge("app_go_sys", "Number of total bytes of memory.")
{ // HTTP metrics
httpBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30}
c.Metrics().NewHistogram("app_http_response", "Response time of HTTP requests in seconds.", httpBuckets...)
c.Metrics().NewHistogram("app_http_service_response", "Response time of HTTP service requests in seconds.", httpBuckets...)
c.Metrics().NewCounter("app_http_retry_count", "Total number of retry events")
c.Metrics().NewGauge("app_http_circuit_breaker_state", "Current state of the circuit breaker (0 for Closed, 1 for Open)")
}
{ // Redis metrics
redisBuckets := getDefaultDatasourceBuckets()
c.Metrics().NewHistogram("app_redis_stats", "Response time of Redis commands in milliseconds.", redisBuckets...)
}
{ // SQL metrics
sqlBuckets := getDefaultDatasourceBuckets()
c.Metrics().NewHistogram("app_sql_stats", "Response time of SQL queries in milliseconds.", sqlBuckets...)
c.Metrics().NewGauge("app_sql_open_connections", "Number of open SQL connections.")
c.Metrics().NewGauge("app_sql_inUse_connections", "Number of inUse SQL connections.")
}
// pubsub metrics
c.Metrics().NewCounter("app_pubsub_publish_total_count", "Number of total publish operations.")
c.Metrics().NewCounter("app_pubsub_publish_success_count", "Number of successful publish operations.")
c.Metrics().NewCounter("app_pubsub_subscribe_total_count", "Number of total subscribe operations.")
c.Metrics().NewCounter("app_pubsub_subscribe_success_count", "Number of successful subscribe operations.")
}
func (c *Container) GetAppName() string {
return c.appName
}
func (c *Container) GetAppVersion() string {
return c.appVersion
}
func (c *Container) GetPublisher() pubsub.Publisher {
return c.PubSub
}
func (c *Container) GetSubscriber() pubsub.Subscriber {
return c.PubSub
}
// GetConnectionFromContext retrieves a WebSocket connection from the context using the Manager.
func (c *Container) GetConnectionFromContext(ctx context.Context) *websocket.Connection {
if c.WSManager == nil {
return nil
}
// First check if connection is directly stored in context
if conn, ok := ctx.Value(websocket.WSConnectionKey).(*websocket.Connection); ok {
return conn
}
// Fallback to connection ID lookup
if connID, ok := ctx.Value(websocket.WSConnectionKey).(string); ok {
return c.WSManager.GetWebsocketConnection(connID)
}
return nil
}
// GetWSConnectionByServiceName retrieves a WebSocket connection by its service name.
func (c *Container) GetWSConnectionByServiceName(serviceName string) *websocket.Connection {
return c.WSManager.GetConnectionByServiceName(serviceName)
}
// AddConnection adds a WebSocket connection to the Manager.
func (c *Container) AddConnection(connID string, conn *websocket.Connection) {
c.WSManager.AddWebsocketConnection(connID, conn)
}
// RemoveConnection removes a WebSocket connection from the Manager.
func (c *Container) RemoveConnection(connID string) {
c.WSManager.CloseConnection(connID)
}
// getDefaultDatasourceBuckets returns the standard histogram buckets for all datasource operations in milliseconds.
// Covers 0-30s range to align with typical request timeout boundaries and provide consistent observability
// across SQL, Redis, MongoDB, Cassandra, and other datasources.
func getDefaultDatasourceBuckets() []float64 {
return []float64{
.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 5, 7.5, 10, // 0-10ms: fast operations
25, 50, 100, 250, 500, 1000, 5000, 10000, 30000, // 10ms-30s: slower operations
}
}
func (c *Container) createKafkaPubSub(conf config.Config) {
if conf.Get("PUBSUB_BROKER") == "" {
return
}
partition, err := strconv.Atoi(conf.GetOrDefault("PARTITION_SIZE", "0"))
if err != nil {
c.Logger.Error("Invalid value for PARTITION_SIZE, using default: 0")
}
// PUBSUB_OFFSET determines the starting position for message consumption in Kafka.
// This allows control over whether to read historical messages or only new ones:
// - Default value -1: Start from the latest offset (only consume new messages after consumer starts)
// - Value 0: Start from the earliest offset (read all historical messages from the beginning)
// - Positive value: Start from a specific offset position (useful for resuming from a known point)
// This is particularly important for scenarios like message replay, recovery from failures,
// or when you only want to process messages that arrive after the consumer is initialized.
offSet, err := strconv.Atoi(conf.GetOrDefault("PUBSUB_OFFSET", "-1"))
if err != nil {
c.Logger.Error("Invalid value for PUBSUB_OFFSET, using default: -1")
}
batchSize, err := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_SIZE", strconv.Itoa(kafka.DefaultBatchSize)))
if err != nil {
c.Logger.Errorf("Invalid value for KAFKA_BATCH_SIZE, using default: %d", kafka.DefaultBatchSize)
}
batchBytes, err := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_BYTES", strconv.Itoa(kafka.DefaultBatchBytes)))
if err != nil {
c.Logger.Errorf("Invalid value for KAFKA_BATCH_BYTES, using default: %d", kafka.DefaultBatchBytes)
}
batchTimeout, err := strconv.Atoi(conf.GetOrDefault("KAFKA_BATCH_TIMEOUT", strconv.Itoa(kafka.DefaultBatchTimeout)))
if err != nil {
c.Logger.Errorf("Invalid value for KAFKA_BATCH_TIMEOUT, using default: %d", kafka.DefaultBatchTimeout)
}
tlsConf := kafka.TLSConfig{
CertFile: conf.Get("KAFKA_TLS_CERT_FILE"),
KeyFile: conf.Get("KAFKA_TLS_KEY_FILE"),
CACertFile: conf.Get("KAFKA_TLS_CA_CERT_FILE"),
InsecureSkipVerify: conf.Get("KAFKA_TLS_INSECURE_SKIP_VERIFY") == "true",
}
pubsubBrokers := strings.Split(conf.Get("PUBSUB_BROKER"), ",")
c.PubSub = kafka.New(&kafka.Config{
Brokers: pubsubBrokers,
Partition: partition,
ConsumerGroupID: conf.Get("CONSUMER_ID"),
OffSet: offSet,
BatchSize: batchSize,
BatchBytes: batchBytes,
BatchTimeout: batchTimeout,
SecurityProtocol: conf.Get("KAFKA_SECURITY_PROTOCOL"),
SASLMechanism: conf.Get("KAFKA_SASL_MECHANISM"),
SASLUser: conf.Get("KAFKA_SASL_USERNAME"),
SASLPassword: conf.Get("KAFKA_SASL_PASSWORD"),
TLS: tlsConf,
}, c.Logger, c.metricsManager)
}
func (c *Container) createGooglePubSub(conf config.Config) {
c.PubSub = google.New(google.Config{
ProjectID: conf.Get("GOOGLE_PROJECT_ID"),
SubscriptionName: conf.Get("GOOGLE_SUBSCRIPTION_NAME"),
}, c.Logger, c.metricsManager)
}
func (c *Container) createRedisPubSub(conf config.Config) {
c.warnIfRedisPubSubSharesRedisDB(conf)
// Redis PubSub is initialized via NewPubSub constructor, aligning with other PubSub implementations.
c.PubSub = redis.NewPubSub(conf, c.Logger, c.metricsManager)
}
func (c *Container) warnIfRedisPubSubSharesRedisDB(conf config.Config) {
// Warn (do not fail): if Redis PubSub (streams mode) shares the same Redis logical DB as the primary Redis datasource,
// GoFr migrations can later fail due to `gofr_migrations` key-type collision (HASH vs STREAM).
if isNil(c.Redis) || effectiveRedisPubSubMode(conf) != redisPubSubModeStreams {
return
}
redisDBStr := conf.Get("REDIS_DB")
if redisDBStr == "" {
redisDBStr = "0"
}
redisDB, err := strconv.Atoi(redisDBStr)
if err != nil {
redisDB = 0
}
pubsubDBStr := conf.Get("REDIS_PUBSUB_DB")
if pubsubDBStr == "" {
// No warning needed - defaults to DB 15 which is safe and different from typical REDIS_DB (0)
return
}
// Only warn if user explicitly set it to the same as REDIS_DB
pubsubDB, err := strconv.Atoi(pubsubDBStr)
if err != nil {
// Invalid value - will use default DB 15, no warning needed
return
}
if pubsubDB == redisDB {
c.Logger.Warnf(
"REDIS_PUBSUB_DB (%d) is the same as REDIS_DB (%d); migrations may fail (gofr_migrations HASH/STREAM). "+
"Set REDIS_PUBSUB_DB to a different DB.",
pubsubDB, redisDB,
)
}
}
func effectiveRedisPubSubMode(conf config.Config) string {
mode := strings.ToLower(conf.Get("REDIS_PUBSUB_MODE"))
if mode == redisPubSubModePubSub {
return redisPubSubModePubSub
}
// Default and fallback is streams.
return redisPubSubModeStreams
}
================================================
FILE: pkg/gofr/container/container_test.go
================================================
package container
import (
"context"
"fmt"
"os"
"testing"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource/pubsub/mqtt"
gofrRedis "gofr.dev/pkg/gofr/datasource/redis"
gofrSql "gofr.dev/pkg/gofr/datasource/sql"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
ws "gofr.dev/pkg/gofr/websocket"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_newContainerSuccessWithLogger(t *testing.T) {
cfg := config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))
container := NewContainer(cfg)
assert.NotNil(t, container.Logger, "TEST, Failed.\nlogger initialization")
}
func Test_newContainerDBInitializationFail(t *testing.T) {
t.Setenv("REDIS_HOST", "invalid")
t.Setenv("DB_DIALECT", "mysql")
t.Setenv("DB_HOST", "invalid")
cfg := config.NewEnvFile("", logging.NewMockLogger(logging.DEBUG))
container := NewContainer(cfg)
db := container.SQL.(*gofrSql.DB)
redis := container.Redis.(*gofrRedis.Redis)
// container is a pointer, and we need to see if db are not initialized, comparing the container object
// will not suffice the purpose of this test
require.Error(t, db.DB.PingContext(t.Context()), "TEST, Failed.\ninvalid db connections")
assert.NotNil(t, redis.Client, "TEST, Failed.\ninvalid redis connections")
}
func Test_newContainerPubSubInitializationFail(t *testing.T) {
testCases := []struct {
desc string
configs map[string]string
}{
{
desc: "Google PubSub fail",
configs: map[string]string{
"PUBSUB_BACKEND": "GOOGLE",
},
},
}
for _, tc := range testCases {
c := NewContainer(config.NewMockConfig(tc.configs))
assert.Nil(t, c.PubSub)
}
}
func TestContainer_MQTTInitialization_Default(t *testing.T) {
configs := map[string]string{
"PUBSUB_BACKEND": "MQTT",
}
c := NewContainer(config.NewMockConfig(configs))
assert.NotNil(t, c.PubSub)
m, ok := c.PubSub.(*mqtt.MQTT)
assert.True(t, ok)
assert.NotNil(t, m.Client)
}
func TestContainer_GetHTTPService(t *testing.T) {
svc := service.NewHTTPService("", nil, nil)
c := &Container{Services: map[string]service.HTTP{
"test-service": svc,
}}
testCases := []struct {
desc string
servicName string
expected service.HTTP
}{
{
desc: "success get",
servicName: "test-service",
expected: svc,
},
{
desc: "failed get",
servicName: "invalid-service",
expected: nil,
},
}
for _, tc := range testCases {
out := c.GetHTTPService(tc.servicName)
assert.Equal(t, tc.expected, out)
}
}
func TestContainer_GetAppName(t *testing.T) {
c := &Container{appName: "test-app"}
out := c.GetAppName()
assert.Equal(t, "test-app", out)
}
func TestContainer_GetAppVersion(t *testing.T) {
c := &Container{appVersion: "v0.1.0"}
out := c.GetAppVersion()
assert.Equal(t, "v0.1.0", out)
}
func TestContainer_GetPublisher(t *testing.T) {
publisher := &MockPubSub{}
c := &Container{PubSub: publisher}
out := c.GetPublisher()
assert.Equal(t, publisher, out)
}
func TestContainer_GetSubscriber(t *testing.T) {
subscriber := &MockPubSub{}
c := &Container{PubSub: subscriber}
out := c.GetSubscriber()
assert.Equal(t, subscriber, out)
}
func TestContainer_newContainerWithNilConfig(t *testing.T) {
container := NewContainer(nil)
failureMsg := "TestContainer_newContainerWithNilConfig Failed!"
assert.Nil(t, container.Redis, "%s", failureMsg)
assert.Nil(t, container.SQL, "%s", failureMsg)
assert.Nil(t, container.Services, "%s", failureMsg)
assert.Nil(t, container.PubSub, "%s", failureMsg)
assert.Nil(t, container.Logger, "%s", failureMsg)
}
func TestContainer_Close(t *testing.T) {
controller := gomock.NewController(t)
defer controller.Finish()
mockDB, sqlMock, _ := gofrSql.NewSQLMocks(t)
mockRedis := NewMockRedis(controller)
mockPubSub := &MockPubSub{}
mockRedis.EXPECT().Close().Return(nil)
sqlMock.ExpectClose()
c := NewContainer(config.NewMockConfig(nil))
c.SQL = &sqlMockDB{mockDB, &expectedQuery{}, logging.NewLogger(logging.DEBUG)}
c.Redis = mockRedis
c.PubSub = mockPubSub
assert.NotNil(t, c.PubSub)
err := c.Close()
require.NoError(t, err)
}
func Test_GetConnectionFromContext(t *testing.T) {
tests := []struct {
name string
ctx context.Context
setup func(c *Container)
expected *ws.Connection
}{
{
name: "no connection in context",
ctx: t.Context(),
setup: func(*Container) {},
expected: nil,
},
{
name: "connection in context",
ctx: context.WithValue(t.Context(), ws.WSConnectionKey, "test-conn-id"),
setup: func(c *Container) {
c.WSManager = ws.New()
c.WSManager.AddWebsocketConnection("test-conn-id", &ws.Connection{Conn: &websocket.Conn{}})
},
expected: &ws.Connection{Conn: &websocket.Conn{}},
},
{
name: "wrong type in context",
ctx: context.WithValue(t.Context(), ws.WSConnectionKey, 12345),
setup: func(*Container) {},
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
container := &Container{}
tt.setup(container)
conn := container.GetConnectionFromContext(tt.ctx)
assert.Equal(t, tt.expected, conn)
})
}
}
func TestContainer_CreateSetsAppNameAndVersion(t *testing.T) {
// Test case: Explicit values are provided
t.Run("explicit config values", func(t *testing.T) {
cfg := config.NewMockConfig(map[string]string{
"APP_NAME": "test-app",
"APP_VERSION": "v1.0.0",
})
c := &Container{}
c.Create(cfg)
assert.Equal(t, "test-app", c.GetAppName())
assert.Equal(t, "v1.0.0", c.GetAppVersion())
})
// Test case: Empty config should use default values
t.Run("empty config uses defaults", func(t *testing.T) {
cfg := config.NewMockConfig(map[string]string{}) // No values provided
c := &Container{}
c.Create(cfg)
assert.Equal(t, "gofr-app", c.GetAppName())
assert.Equal(t, "dev", c.GetAppVersion())
})
}
func TestRedisPubSubEffectiveMode(t *testing.T) {
tests := []struct {
desc string
mode string
expected string
}{
{desc: "explicit pubsub", mode: "pubsub", expected: redisPubSubModePubSub},
{desc: "explicit streams", mode: "streams", expected: redisPubSubModeStreams},
{desc: "empty defaults to streams", mode: "", expected: redisPubSubModeStreams},
{desc: "invalid falls back to streams", mode: "invalid", expected: redisPubSubModeStreams},
}
for _, tc := range tests {
conf := config.NewMockConfig(map[string]string{"REDIS_PUBSUB_MODE": tc.mode})
require.Equal(t, tc.expected, effectiveRedisPubSubMode(conf), tc.desc)
}
}
func TestWarnRedisPubSubSharedDB_NoWarnWhenRedisNil(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{Logger: NewMockLogger(ctrl)}
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_DB": "0",
}))
}
func TestWarnRedisPubSubSharedDB_NoWarnWhenModeIsPubSub(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{
Logger: NewMockLogger(ctrl),
Redis: NewMockRedis(ctrl), // non-nil
}
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
"REDIS_DB": "0",
}))
}
func TestWarnRedisPubSubSharedDB_WarnsWhenPubSubDBUnset(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{
Logger: NewMockLogger(ctrl),
Redis: NewMockRedis(ctrl), // non-nil
}
// No warning expected when REDIS_PUBSUB_DB is unset (defaults to DB 15, which is safe)
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_DB": "0",
}))
}
func TestWarnRedisPubSubSharedDB_WarnsWhenPubSubDBInvalid(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{
Logger: NewMockLogger(ctrl),
Redis: NewMockRedis(ctrl), // non-nil
}
// No warning expected when REDIS_PUBSUB_DB is invalid (will use default DB 15, which is safe)
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_DB": "0",
"REDIS_PUBSUB_DB": "not-a-number",
}))
}
func TestWarnRedisPubSubSharedDB_WarnsWhenPubSubDBEqualsRedisDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var warns []string
logger := NewMockLogger(ctrl)
// Warnf is called with format string + 2 integer arguments (pubsubDB, redisDB)
logger.EXPECT().Warnf(gomock.Any(), gomock.Any(), gomock.Any()).Do(func(format string, args ...any) {
warns = append(warns, fmt.Sprintf(format, args...))
}).Times(1)
c := &Container{
Logger: logger,
Redis: NewMockRedis(ctrl), // non-nil
}
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_DB": "0",
"REDIS_PUBSUB_DB": "0",
}))
require.Len(t, warns, 1)
require.Contains(t, warns[0], "migrations may fail")
}
func TestWarnRedisPubSubSharedDB_NoWarnWhenPubSubDBDiffers(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{
Logger: NewMockLogger(ctrl),
Redis: NewMockRedis(ctrl), // non-nil
}
c.warnIfRedisPubSubSharesRedisDB(config.NewMockConfig(map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_DB": "0",
"REDIS_PUBSUB_DB": "1",
}))
}
func TestCreatePubSub_DispatchBranches(t *testing.T) {
t.Run("kafka branch with empty broker does nothing", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{Logger: NewMockLogger(ctrl)}
c.createPubSub(config.NewMockConfig(map[string]string{"PUBSUB_BACKEND": "KAFKA"}))
require.Nil(t, c.PubSub)
})
t.Run("google branch with missing configs returns nil client", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
var errs []string
logger := NewMockLogger(ctrl)
// google.New uses Debugf with varying arg counts; allow both shapes.
logger.EXPECT().Debugf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
logger.EXPECT().Debugf(gomock.Any()).AnyTimes()
logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
logger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Do(func(format string, args ...any) {
errs = append(errs, fmt.Sprintf(format, args...))
}).AnyTimes()
c := &Container{Logger: logger}
c.createPubSub(config.NewMockConfig(map[string]string{"PUBSUB_BACKEND": "GOOGLE"}))
require.Nil(t, c.PubSub)
require.NotEmpty(t, errs)
})
t.Run("redis branch with empty host returns nil client", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := &Container{Logger: NewMockLogger(ctrl)}
c.createPubSub(config.NewMockConfig(map[string]string{"PUBSUB_BACKEND": "REDIS"}))
require.Nil(t, c.PubSub)
})
}
func TestWebsocketManagerHelpers(t *testing.T) {
m := ws.New()
c := &Container{
WSManager: m,
}
connID := "svc-1"
conn := &ws.Connection{}
c.AddConnection(connID, conn)
got := c.GetWSConnectionByServiceName(connID)
require.Equal(t, conn, got)
c.RemoveConnection(connID)
require.Nil(t, c.GetWSConnectionByServiceName(connID))
}
func TestContainer_registerFrameworkMetrics_RegistersExpectedMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
c := &Container{
Logger: NewMockLogger(ctrl),
metricsManager: mockMetrics,
}
gauges := []string{
"app_info",
"app_go_routines",
"app_sys_memory_alloc",
"app_sys_total_alloc",
"app_go_numGC",
"app_go_sys",
"app_sql_open_connections",
"app_sql_inUse_connections",
}
for _, gauge := range gauges {
mockMetrics.EXPECT().NewGauge(gauge, gomock.Any()).Times(1)
}
mockMetrics.EXPECT().NewGauge("app_http_circuit_breaker_state", gomock.Any()).Times(1)
counters := []string{
"app_pubsub_publish_total_count",
"app_pubsub_publish_success_count",
"app_pubsub_subscribe_total_count",
"app_pubsub_subscribe_success_count",
"app_http_retry_count",
}
for _, counter := range counters {
mockMetrics.EXPECT().NewCounter(counter, gomock.Any()).Times(1)
}
httpBuckets := []float64{.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30}
dsBuckets := getDefaultDatasourceBuckets()
histograms := []struct {
name string
buckets []float64
}{
{name: "app_http_response", buckets: httpBuckets},
{name: "app_http_service_response", buckets: httpBuckets},
{name: "app_redis_stats", buckets: dsBuckets},
{name: "app_sql_stats", buckets: dsBuckets},
}
for _, tc := range histograms {
bucketMatchers := make([]any, 0, len(tc.buckets))
for range tc.buckets {
bucketMatchers = append(bucketMatchers, gomock.Any())
}
mockMetrics.EXPECT().
NewHistogram(tc.name, gomock.Any(), bucketMatchers...).
Do(func(_ string, _ string, buckets ...float64) {
require.Equal(t, tc.buckets, buckets)
}).
Times(1)
}
c.registerFrameworkMetrics()
}
func TestGetDefaultDatasourceBuckets(t *testing.T) {
buckets := getDefaultDatasourceBuckets()
require.NotEmpty(t, buckets)
assert.InDelta(t, 0.05, buckets[0], 1e-12)
assert.InDelta(t, 30000.0, buckets[len(buckets)-1], 1e-12)
for i := 1; i < len(buckets); i++ {
assert.Greater(t, buckets[i], buckets[i-1])
}
}
func TestContainer_Close_ClosesWebsocketConnections(t *testing.T) {
c := &Container{
WSManager: ws.New(),
}
connID := "conn-1"
c.AddConnection(connID, &ws.Connection{})
require.Len(t, c.WSManager.ListConnections(), 1)
err := c.Close()
require.NoError(t, err)
require.Empty(t, c.WSManager.ListConnections())
}
================================================
FILE: pkg/gofr/container/datasources.go
================================================
package container
import (
"bytes"
"context"
"database/sql"
"time"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
gofrSQL "gofr.dev/pkg/gofr/datasource/sql"
)
//go:generate go run go.uber.org/mock/mockgen -source=datasources.go -destination=mock_datasources.go -package=container
type DB interface {
Query(query string, args ...any) (*sql.Rows, error)
QueryRow(query string, args ...any) *sql.Row
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
Exec(query string, args ...any) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
Prepare(query string) (*sql.Stmt, error)
Begin() (*gofrSQL.Tx, error)
Select(ctx context.Context, data any, query string, args ...any)
HealthCheck() *datasource.Health
Dialect() string
Close() error
}
type Redis interface {
redis.Cmdable
redis.HashCmdable
HealthCheck() datasource.Health
Close() error
}
// Cassandra is an interface representing a cassandra database
// Deprecated: Cassandra interface is deprecated and will be removed in future releases, users must use CassandraWithContext.
type Cassandra interface {
// Deprecated: Query method is deprecated and will be removed in future releases, users must use QueryWithCtx.
// Query executes the query and binds the result into dest parameter.
// Returns error if any error occurs while binding the result.
// Can be used to single as well as multiple rows.
// Accepts pointer to struct or slice as dest parameter for single and multiple rows retrieval respectively.
//
// Example:
//
// // Get multiple rows with only one column
// ids := make([]int, 0)
// err := c.Query(&ids, "SELECT id FROM users")
//
// // Get a single object from database
// type user struct {
// ID int
// Name string
// }
// u := user{}
// err := c.Query(&u, "SELECT * FROM users WHERE id=?", 1)
//
// // Get array of objects from multiple rows
// type user struct {
// ID int
// Name string `db:"name"`
// }
// users := []user{}
// err := c.Query(&users, "SELECT * FROM users")
Query(dest any, stmt string, values ...any) error
// Deprecated: Exec method is deprecated and will be removed in future releases, users must use ExecWithCtx.
// Exec executes the query without returning any rows.
// Return error if any error occurs while executing the query.
// Can be used to execute UPDATE or INSERT.
//
// Example:
//
// // Without values
// err := c.Exec("INSERT INTO users VALUES(1, 'John Doe')")
//
// // With Values
// id := 1
// name := "John Doe"
// err := c.Exec("INSERT INTO users VALUES(?, ?)", id, name)
Exec(stmt string, values ...any) error
// Deprecated: ExecCAS method is deprecated and will be removed in future releases, users must use ExecCASWithCtx.
// ExecCAS executes a lightweight transaction (i.e. an UPDATE or INSERT statement containing an IF clause).
// If the transaction fails because the existing values did not match, the previous values will be stored in dest.
// Returns true if the query is applied otherwise false.
// Returns false and error if any error occur while executing the query.
// Accepts only pointer to struct and built-in types as the dest parameter.
//
// Example:
//
// type user struct {
// ID int
// Name string
// }
// u := user{}
// applied, err := c.ExecCAS(&user, "INSERT INTO users VALUES(1, 'John Doe') IF NOT EXISTS")
ExecCAS(dest any, stmt string, values ...any) (bool, error)
// Deprecated: NewBatch method is deprecated and will be removed in future releases, users must use NewBatchWithCtx.
// NewBatch creates a new Cassandra batch with the specified name and batch type.
// This method initializes a new Cassandra batch operation. It sets up the batch
// with the given name and type, allowing you to execute multiple queries in
// a single batch operation. The `batchType` determines the type of batch operation
// and can be one of `LoggedBatch`, `UnloggedBatch`, or `CounterBatch`.
// These constants have been defined in gofr.dev/pkg/gofr/datasource/cassandra
//
// Example:
// err := client.NewBatch("myBatch", cassandra.LoggedBatch)
NewBatch(name string, batchType int) error
CassandraBatch
HealthChecker
}
type CassandraBatch interface {
// Deprecated: BatchQuery method is deprecated and will be removed in future releases, users must use BatchQueryWithCtx.
// BatchQuery adds the query to the batch operation
//
// Example:
//
// // Without values
// c.BatchQuery("INSERT INTO users VALUES(1, 'John Doe')")
// c.BatchQuery("INSERT INTO users VALUES(2, 'Jane Smith')")
//
// // With Values
// id1 := 1
// name1 := "John Doe"
// id2 := 2
// name2 := "Jane Smith"
// c.BatchQuery("INSERT INTO users VALUES(?, ?)", id1, name1)
// c.BatchQuery("INSERT INTO users VALUES(?, ?)", id2, name2)
BatchQuery(name, stmt string, values ...any) error
// Deprecated: ExecuteBatch method is deprecated and will be removed in future releases, users must use ExecuteBatchWithCtx.
// ExecuteBatch executes a batch operation and returns nil if successful otherwise an error is returned describing the failure.
//
// Example:
//
// err := c.ExecuteBatch("myBatch")
ExecuteBatch(name string) error
// Deprecated: ExecuteBatchCAS method is deprecated and will be removed in future releases, users must use ExecuteBatchCASWithCtx.
// ExecuteBatchCAS executes a batch operation and returns true if successful.
// Returns true if the query is applied otherwise false.
// Returns false and error if any error occur while executing the query.
// Accepts only pointer to struct and built-in types as the dest parameter.
//
// Example:
//
// applied, err := c.ExecuteBatchCAS("myBatch");
ExecuteBatchCAS(name string, dest ...any) (bool, error)
}
type CassandraWithContext interface {
// QueryWithCtx executes the query with a context and binds the result into dest parameter.
// Accepts pointer to struct or slice as dest parameter for single and multiple rows retrieval respectively.
QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error
// ExecWithCtx executes the query with a context, without returning any rows.
ExecWithCtx(ctx context.Context, stmt string, values ...any) error
// ExecCASWithCtx executes a lightweight transaction with a context.
ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error)
// NewBatchWithCtx creates a new Cassandra batch with context.
NewBatchWithCtx(ctx context.Context, name string, batchType int) error
Cassandra
CassandraBatchWithContext
}
type CassandraBatchWithContext interface {
// BatchQueryWithCtx adds the query to the batch operation with a context.
BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error
// ExecuteBatchWithCtx executes a batch operation with a context.
ExecuteBatchWithCtx(ctx context.Context, name string) error
// ExecuteBatchCASWithCtx executes a batch operation with context and returns the result.
ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error)
}
type CassandraProvider interface {
CassandraWithContext
provider
}
type Clickhouse interface {
Exec(ctx context.Context, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error
HealthChecker
}
type ClickhouseProvider interface {
Clickhouse
provider
}
type OracleDB interface {
Exec(ctx context.Context, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
Begin() (OracleTx, error)
HealthChecker
}
type OracleTx interface {
ExecContext(ctx context.Context, query string, args ...any) error
SelectContext(ctx context.Context, dest any, query string, args ...any) error
Commit() error
Rollback() error
}
type OracleProvider interface {
OracleDB
provider
}
// Mongo is an interface representing a MongoDB database client with common CRUD operations.
type Mongo interface {
// Find executes a query to find documents in a collection based on a filter and stores the results
// into the provided results interface.
Find(ctx context.Context, collection string, filter any, results any) error
// FindOne executes a query to find a single document in a collection based on a filter and stores the result
// into the provided result interface.
FindOne(ctx context.Context, collection string, filter any, result any) error
// InsertOne inserts a single document into a collection.
// It returns the identifier of the inserted document and an error, if any.
InsertOne(ctx context.Context, collection string, document any) (any, error)
// InsertMany inserts multiple documents into a collection.
// It returns the identifiers of the inserted documents and an error, if any.
InsertMany(ctx context.Context, collection string, documents []any) ([]any, error)
// DeleteOne deletes a single document from a collection based on a filter.
// It returns the number of documents deleted and an error, if any.
DeleteOne(ctx context.Context, collection string, filter any) (int64, error)
// DeleteMany deletes multiple documents from a collection based on a filter.
// It returns the number of documents deleted and an error, if any.
DeleteMany(ctx context.Context, collection string, filter any) (int64, error)
// UpdateByID updates a document in a collection by its ID.
// It returns the number of documents updated and an error if any.
UpdateByID(ctx context.Context, collection string, id any, update any) (int64, error)
// UpdateOne updates a single document in a collection based on a filter.
// It returns an error if any.
UpdateOne(ctx context.Context, collection string, filter any, update any) error
// UpdateMany updates multiple documents in a collection based on a filter.
// It returns the number of documents updated and an error if any.
UpdateMany(ctx context.Context, collection string, filter any, update any) (int64, error)
// CountDocuments counts the number of documents in a collection based on a filter.
// It returns the count and an error if any.
CountDocuments(ctx context.Context, collection string, filter any) (int64, error)
// Drop an entire collection from the database.
// It returns an error if any.
Drop(ctx context.Context, collection string) error
// CreateCollection creates a new collection with specified name and default options.
CreateCollection(ctx context.Context, name string) error
// StartSession starts a session and provide methods to run commands in a transaction.
StartSession() (any, error)
HealthChecker
}
type Transaction interface {
StartTransaction() error
AbortTransaction(context.Context) error
CommitTransaction(context.Context) error
EndSession(context.Context)
}
// MongoProvider is an interface that extends Mongo with additional methods for logging, metrics, and connection management.
// Which is used for initializing datasource.
type MongoProvider interface {
Mongo
provider
}
// SurrealDB defines an interface representing a SurrealDB client with common database operations.
type SurrealDB interface {
// CreateNamespace creates a new namespace in the SurrealDB instance.
CreateNamespace(ctx context.Context, namespace string) error
// CreateDatabase creates a new database in the SurrealDB instance.
CreateDatabase(ctx context.Context, database string) error
// DropNamespace deletes a namespace from the SurrealDB instance.
DropNamespace(ctx context.Context, namespace string) error
// DropDatabase deletes a database from the SurrealDB instance.
DropDatabase(ctx context.Context, database string) error
// Query executes a Surreal query with the provided variables and returns the query results as a slice of interfaces{}.
// It returns an error if the query execution fails.
Query(ctx context.Context, query string, vars map[string]any) ([]any, error)
// Create inserts a new record into the specified table and returns the created record as a map.
// It returns an error if the operation fails.
Create(ctx context.Context, table string, data any) (map[string]any, error)
// Update modifies an existing record in the specified table by its ID with the provided data.
// It returns the updated record as an interface and an error if the operation fails.
Update(ctx context.Context, table string, id string, data any) (any, error)
// Delete removes a record from the specified table by its ID.
// It returns the result of the delete operation as an interface and an error if the operation fails.
Delete(ctx context.Context, table string, id string) (any, error)
// Select retrieves all records from the specified table.
// It returns a slice of maps representing the records and an error if the operation fails.
Select(ctx context.Context, table string) ([]map[string]any, error)
HealthChecker
}
// SurrealBDProvider is an interface that extends SurrealDB with additional methods for logging, metrics, or connection management.
// It is typically used for initializing and managing SurrealDB-based data sources.
type SurrealBDProvider interface {
SurrealDB
provider
}
type provider interface {
// UseLogger sets the logger for the Cassandra client.
UseLogger(logger any)
// UseMetrics sets the metrics for the Cassandra client.
UseMetrics(metrics any)
// UseTracer sets the tracer for the Cassandra client.
UseTracer(tracer any)
// Connect establishes a connection to Cassandra and registers metrics using the provided configuration when the client was Created.
Connect()
}
type HealthChecker interface {
// HealthCheck returns an interface rather than a struct as externalDB's are part of different module.
// It is done to avoid adding packages which are not being used.
HealthCheck(context.Context) (any, error)
}
type KVStore interface {
Get(ctx context.Context, key string) (string, error)
Set(ctx context.Context, key, value string) error
Delete(ctx context.Context, key string) error
HealthChecker
}
type KVStoreProvider interface {
KVStore
provider
}
type PubSubProvider interface {
pubsub.Client
provider
}
type Solr interface {
Search(ctx context.Context, collection string, params map[string]any) (any, error)
Create(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Update(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Delete(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error)
Retrieve(ctx context.Context, collection string, params map[string]any) (any, error)
ListFields(ctx context.Context, collection string, params map[string]any) (any, error)
AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error)
HealthChecker
}
type SolrProvider interface {
Solr
provider
}
// Dgraph defines the methods for interacting with a Dgraph database.
type Dgraph interface {
// ApplySchema applies or updates the complete database schema.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - schema: Schema definition in Dgraph Schema Definition Language (SDL) format
// Returns:
// - error: An error if the schema application fails
ApplySchema(ctx context.Context, schema string) error
// AddOrUpdateField atomically creates or updates a single field definition.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - fieldName: Name of the field/predicate to create or update
// - fieldType: Dgraph data type (e.g., string, int, datetime)
// - directives: Space-separated Dgraph directives (e.g., "@index(hash) @upsert")
// Returns:
// - error: An error if the field operation fails
AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error
// DropField permanently removes a field/predicate and all its associated data.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - fieldName: Name of the field/predicate to remove
// Returns:
// - error: An error if the field removal fails
DropField(ctx context.Context, fieldName string) error
// Query executes a read-only query in the Dgraph database and returns the result.
// Parameters:
// - ctx: The context for the query, used for controlling timeouts, cancellation, etc.
// - query: The Dgraph query string in GraphQL+- format.
// Returns:
// - any: The result of the query, usually of type *api.Response.
// - error: An error if the query execution fails.
Query(ctx context.Context, query string) (any, error)
// QueryWithVars executes a read-only query with variables in the Dgraph database.
// Parameters:
// - ctx: The context for the query.
// - query: The Dgraph query string in GraphQL+- format.
// - vars: A map of variables to be used within the query.
// Returns:
// - any: The result of the query with variables, usually of type *api.Response.
// - error: An error if the query execution fails.
QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error)
// Mutate executes a write operation (mutation) in the Dgraph database and returns the result.
// Parameters:
// - ctx: The context for the mutation.
// - mu: The mutation operation, usually of type *api.Mutation.
// Returns:
// - any: The result of the mutation, usually of type *api.Assigned.
// - error: An error if the mutation execution fails.
Mutate(ctx context.Context, mu any) (any, error)
// Alter applies schema or other changes to the Dgraph database.
// Parameters:
// - ctx: The context for the alter operation.
// - op: The alter operation, usually of type *api.Operation.
// Returns:
// - error: An error if the operation fails.
Alter(ctx context.Context, op any) error
// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database.
// Returns:
// - any: A new transaction, usually of type *api.Txn.
NewTxn() any
// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database.
// Returns:
// - any: A new read-only transaction, usually of type *api.Txn.
NewReadOnlyTxn() any
// HealthChecker checks the health of the Dgraph instance, ensuring it is up and running.
// Returns:
// - error: An error if the health check fails.
HealthChecker
}
// DgraphProvider extends Dgraph with connection management capabilities.
type DgraphProvider interface {
Dgraph
provider
}
type OpenTSDBProvider interface {
OpenTSDB
provider
}
// OpenTSDB provides methods for GoFr applications to communicate with OpenTSDB
// through its REST APIs. Each method corresponds to an API endpoint defined in the
// OpenTSDB documentation (http://opentsdb.net/docs/build/html/api_http/index.html#api-endpoints).
type OpenTSDB interface {
// HealthChecker verifies if the OpenTSDB server is reachable.
// Returns an error if the server is unreachable, otherwise nil.
HealthChecker
// PutDataPoints sends data to the 'POST /api/put' endpoint to store metrics in OpenTSDB.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - data: A slice of DataPoint objects; must contain at least one entry.
// - queryParam: Specifies the response format:
// - client.PutRespWithSummary: Requests a summary response.
// - client.PutRespWithDetails: Requests detailed response information.
// - Empty string (""): No additional response details.
// - res: A pointer to PutResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
PutDataPoints(ctx context.Context, data any, queryParam string, res any) error
// QueryDataPoints retrieves data using the 'GET /api/query' endpoint based on the specified parameters.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - param: An instance of QueryParam with query parameters for filtering data.
// - res: A pointer to QueryResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
QueryDataPoints(ctx context.Context, param any, res any) error
// QueryLatestDataPoints fetches the latest data point(s) using the 'GET /api/query/last' endpoint,
// supported in OpenTSDB v2.2 and later.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - param: An instance of QueryLastParam with query parameters for the latest data point.
// - res: A pointer to QueryLastResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
QueryLatestDataPoints(ctx context.Context, param any, res any) error
// GetAggregators retrieves available aggregation functions using the 'GET /api/aggregators' endpoint.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - res: A pointer to AggregatorsResponse, where the server's response will be stored.
//
// Returns:
// - Error if response parsing fails or if connectivity issues occur.
GetAggregators(ctx context.Context, res any) error
// QueryAnnotation retrieves a single annotation from OpenTSDB using the 'GET /api/annotation' endpoint.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - queryAnnoParam: A map of parameters for the annotation query, such as client.AnQueryStartTime, client.AnQueryTSUid.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error
// PostAnnotation creates or updates an annotation in OpenTSDB using the 'POST /api/annotation' endpoint.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be created or updated.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
PostAnnotation(ctx context.Context, annotation any, res any) error
// PutAnnotation creates or replaces an annotation in OpenTSDB using the 'PUT /api/annotation' endpoint.
// Fields not included in the request will be reset to default values.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be created or replaced.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
PutAnnotation(ctx context.Context, annotation any, res any) error
// DeleteAnnotation removes an annotation from OpenTSDB using the 'DELETE /api/annotation' endpoint.
//
// Parameters:
// - ctx: Context for managing request lifetime.
// - annotation: The annotation to be deleted.
// - res: A pointer to AnnotationResponse, where the server's response will be stored.
//
// Returns:
// - Error if parameters are invalid, response parsing fails, or if connectivity issues occur.
DeleteAnnotation(ctx context.Context, annotation any, res any) error
}
type ScyllaDB interface {
// Query executes a CQL (Cassandra Query Language) query on the ScyllaDB cluster
// and stores the result in the provided destination variable `dest`.
// Accepts pointer to struct or slice as dest parameter for single and multiple
Query(dest any, stmt string, values ...any) error
// QueryWithCtx executes the query with a context and binds the result into dest parameter.
// Accepts pointer to struct or slice as dest parameter for single and multiple rows retrieval respectively.
QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error
// Exec executes a CQL statement (e.g., INSERT, UPDATE, DELETE) on the ScyllaDB cluster without returning any result.
Exec(stmt string, values ...any) error
// ExecWithCtx executes a CQL statement with the provided context and without returning any result.
ExecWithCtx(ctx context.Context, stmt string, values ...any) error
// ExecCAS executes a lightweight transaction (i.e. an UPDATE or INSERT statement containing an IF clause).
// If the transaction fails because the existing values did not match, the previous values will be stored in dest.
// Returns true if the query is applied otherwise false.
// Returns false and error if any error occur while executing the query.
// Accepts only pointer to struct and built-in types as the dest parameter.
ExecCAS(dest any, stmt string, values ...any) (bool, error)
// NewBatch initializes a new batch operation with the specified name and batch type.
NewBatch(name string, batchType int) error
// NewBatchWithCtx takes context,name and batchtype and return error.
NewBatchWithCtx(_ context.Context, name string, batchType int) error
// BatchQuery executes a batch query in the ScyllaDB cluster with the specified name, statement, and values.
BatchQuery(name, stmt string, values ...any) error
// BatchQueryWithCtx executes a batch query with the provided context.
BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error
// ExecuteBatchWithCtx executes a batch with context and name returns error.
ExecuteBatchWithCtx(ctx context.Context, name string) error
// HealthChecker defines the HealthChecker interface.
HealthChecker
}
type ScyllaDBProvider interface {
ScyllaDB
provider
}
type ArangoDB interface {
// CreateDB creates a new database in ArangoDB.
CreateDB(ctx context.Context, database string) error
// DropDB deletes an existing database in ArangoDB.
DropDB(ctx context.Context, database string) error
// CreateCollection creates a new collection in a database with specified type.
CreateCollection(ctx context.Context, database, collection string, isEdge bool) error
// DropCollection deletes an existing collection from a database.
DropCollection(ctx context.Context, database, collection string) error
// CreateGraph creates a new graph in a database.
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - database: Name of the database where the graph will be created.
// - graph: Name of the graph to be created.
// - edgeDefinitions: Pointer to EdgeDefinition struct containing edge definitions.
//
// Returns an error if the edgeDefinitions parameter is not of type *EdgeDefinition or is nil.
CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error
// DropGraph deletes an existing graph from a database.
DropGraph(ctx context.Context, database, graph string) error
// CreateDocument creates a new document in the specified collection.
CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error)
// GetDocument retrieves a document by its ID from the specified collection.
GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error
// UpdateDocument updates an existing document in the specified collection.
UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error
// DeleteDocument deletes a document by its ID from the specified collection.
DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error
// GetEdges fetches all edges connected to a given vertex in the specified edge collection.
//
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - dbName: Database name.
// - graphName: Graph name.
// - edgeCollection: Edge collection name.
// - vertexID: Full vertex ID (e.g., "persons/16563").
// - resp: Pointer to `*EdgeDetails` to store results.
//
// Returns an error if input is invalid, `resp` is of the wrong type, or the query fails.
GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string, resp any) error
// Query executes an AQL query and binds the results.
//
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - dbName: Name of the database where the query will be executed.
// - query: AQL query string to be executed.
// - bindVars: Map of bind variables to be used in the query.
// - result: Pointer to a slice of maps where the query results will be stored.
// - options : A flexible map[string]any to customize query behavior. Keys should be in camelCase
// and correspond to fields in ArangoDB’s QueryOptions and QuerySubOptions structs.
//
// Returns an error if the database connection fails, the query execution fails, or
// the result parameter is not a pointer to a slice of maps.
Query(ctx context.Context, dbName string, query string, bindVars map[string]any, result any, options ...map[string]any) error
HealthChecker
}
// ArangoDBProvider is an interface that extends ArangoDB with additional methods for logging, metrics, and connection management.
type ArangoDBProvider interface {
ArangoDB
provider
}
// Elasticsearch defines all the operations GoFr users need.
type Elasticsearch interface {
// CreateIndex creates a new index with optional mapping/settings.
CreateIndex(ctx context.Context, index string, settings map[string]any) error
// DeleteIndex deletes an existing index.
DeleteIndex(ctx context.Context, index string) error
// IndexDocument indexes (creates or replaces) a single document.
IndexDocument(ctx context.Context, index, id string, document any) error
// GetDocument retrieves a single document by ID.
// Returns the raw JSON as a map.
GetDocument(ctx context.Context, index, id string) (map[string]any, error)
// UpdateDocument applies a partial update to an existing document.
UpdateDocument(ctx context.Context, index, id string, update map[string]any) error
// DeleteDocument removes a document by ID.
DeleteDocument(ctx context.Context, index, id string) error
// Bulk executes multiple indexing/updating/deleting operations in one request.
// Each entry in `operations` should be a JSON‑serializable object
// following the Elasticsearch bulk API format.
Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error)
// Search executes a query against one or more indices.
// Returns the entire response JSON as a map.
Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error)
HealthChecker
}
// ElasticsearchProvider an interface that extends Elasticsearch with additional methods for logging, metrics, and connection management.
type ElasticsearchProvider interface {
Elasticsearch
provider
}
// Couchbase defines the methods for interacting with a Couchbase database.
type Couchbase interface {
// Get retrieves a document by its key from the specified bucket.
// The result parameter should be a pointer to the struct where the document will be unmarshaled.
Get(ctx context.Context, key string, result any) error
// InsertOne inserts a new document in the collection.
Insert(ctx context.Context, key string, document, result any) error
// Upsert inserts a new document or replaces an existing one in the specified bucket.
// The document parameter can be any Go type that can be marshaled into JSON.
Upsert(ctx context.Context, key string, document any, result any) error
// Remove deletes a document by its key from the specified bucket.
Remove(ctx context.Context, key string) error
// Query executes a N1QL query against the Couchbase cluster.
// The statement is the N1QL query string, and params are any query parameters.
// The result parameter should be a pointer to a slice of structs or maps where the query results will be unmarshaled.
Query(ctx context.Context, statement string, params map[string]any, result any) error
// AnalyticsQuery executes an Analytics query against the Couchbase Analytics service.
// The statement is the Analytics query string, and params are any query parameters.
// The result parameter should be a pointer to a slice of structs or maps where the query results will be unmarshaled.
AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error
RunTransaction(ctx context.Context, logic func(attempt any) error) (any, error)
Close(opts any) error
HealthChecker
}
// CouchbaseProvider is an interface that extends Couchbase with additional methods
// for logging, metrics, tracing, and connection management, aligning with other
// data source providers in your package.
type CouchbaseProvider interface {
Couchbase
provider
}
// DBResolverProvider defines an interface for SQL read/write splitting providers.
type DBResolverProvider interface {
GetResolver() DB
provider
}
// InfluxDB defines the operations required to interact with an InfluxDB instance.
type InfluxDB interface {
// CreateOrganization create new bucket in the influxdb
CreateOrganization(ctx context.Context, org string) (string, error)
// DeleteOrganization deletes a organization under the specified organization.
DeleteOrganization(ctx context.Context, orgID string) error
// ListOrganization list all the available organization
ListOrganization(ctx context.Context) (orgs map[string]string, err error)
// WritePoint writes one time-series points to a bucket.
// 'points' should follow the line protocol format or structured map format.
WritePoint(ctx context.Context, org, bucket string,
measurement string,
tags map[string]string,
fields map[string]any,
timestamp time.Time) error
// Query runs a Flux query and returns the result as a slice of maps,
// where each map is a row with column name-value pairs.
Query(ctx context.Context, org, fluxQuery string) ([]map[string]any, error)
// CreateBucket creates a new bucket under the specified organization.
CreateBucket(ctx context.Context, org, bucket string) (string, error)
// DeleteBucket deletes a bucketId with bucketID
DeleteBucket(ctx context.Context, bucketID string) error
// ListBuckets lists all buckets under the specified organization.
ListBuckets(ctx context.Context, org string) (map[string]string, error)
// Ping checks if the InfluxDB instance is reachable and healthy.
Ping(ctx context.Context) (bool, error)
HealthChecker
}
// InfluxDBProvider an interface that extends InfluxDB with additional methods for logging, metrics, and connection management.
type InfluxDBProvider interface {
InfluxDB
provider
}
================================================
FILE: pkg/gofr/container/health.go
================================================
package container
import (
"context"
"reflect"
)
func (c *Container) Health(ctx context.Context) any {
var (
healthMap = make(map[string]any)
downCount int
)
const statusDown = "DOWN"
if !isNil(c.SQL) {
health := c.SQL.HealthCheck()
if health.Status == statusDown {
downCount++
}
healthMap["sql"] = health
}
if !isNil(c.Redis) {
health := c.Redis.HealthCheck()
if health.Status == statusDown {
downCount++
}
healthMap["redis"] = health
}
if c.PubSub != nil {
health := c.PubSub.Health()
if health.Status == statusDown {
downCount++
}
healthMap["pubsub"] = health
}
downCount += checkExternalDBHealth(ctx, c, healthMap)
for name, svc := range c.Services {
health := svc.HealthCheck(ctx)
if health.Status == statusDown {
downCount++
}
healthMap[name] = health
}
c.appHealth(healthMap, downCount)
return healthMap
}
func checkExternalDBHealth(ctx context.Context, c *Container, healthMap map[string]any) (downCount int) {
services := map[string]interface {
HealthCheck(context.Context) (any, error)
}{
"mongo": c.Mongo,
"cassandra": c.Cassandra,
"clickHouse": c.Clickhouse,
"kv-store": c.KVStore,
"dgraph": c.DGraph,
"opentsdb": c.OpenTSDB,
"elasticsearch": c.Elasticsearch,
"oracle": c.Oracle,
"couchbase": c.Couchbase,
"influx": c.InfluxDB,
}
for name, service := range services {
if !isNil(service) {
health, err := service.HealthCheck(ctx)
if err != nil {
downCount++
}
healthMap[name] = health
}
}
return downCount
}
func (c *Container) appHealth(healthMap map[string]any, downCount int) {
healthMap["name"] = c.GetAppName()
healthMap["version"] = c.GetAppVersion()
if downCount == 0 {
healthMap["status"] = "UP"
} else {
healthMap["status"] = "DEGRADED"
}
}
func isNil(i any) bool {
// Get the value of the interface
val := reflect.ValueOf(i)
// If the interface is not assigned or is nil, return true
return !val.IsValid() || val.IsNil()
}
================================================
FILE: pkg/gofr/container/health_test.go
================================================
package container
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/sql"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
)
func TestContainer_Health(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
logger := logging.NewMockLogger(logging.ERROR)
tests := []struct {
desc string
datasourceHealth string
appHealth string
}{
{"datasources UP", "UP", "UP"},
{"datasources DOWN", "DOWN", "DEGRADED"},
}
for i, tc := range tests {
expected := getExpectedData(tc.datasourceHealth, tc.appHealth, srv.URL)
expectedJSONdata, _ := json.Marshal(expected)
c, mocks := NewMockContainer(t)
registerMocks(mocks, tc.datasourceHealth)
c.appName = "test-app"
c.appVersion = "test"
c.Services = make(map[string]service.HTTP)
c.Services["test-service"] = service.NewHTTPService(srv.URL, logger, nil)
healthData := c.Health(t.Context())
jsonData, _ := json.Marshal(healthData)
assert.JSONEq(t, string(expectedJSONdata), string(jsonData), "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func registerMocks(mocks *Mocks, health string) {
mocks.SQL.ExpectHealthCheck().WillReturnHealthCheck(&datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:3306/test",
"stats": sql.DBStats{
MaxOpenConnections: 0, OpenConnections: 1, InUse: 0, Idle: 1, WaitCount: 0,
WaitDuration: 0, MaxIdleClosed: 0, MaxIdleTimeClosed: 0, MaxLifetimeClosed: 0,
},
},
})
mocks.Redis.EXPECT().HealthCheck().Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:6379",
"error": "redis not connected",
},
})
mocks.Mongo.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:6379",
"error": "mongo not connected",
},
}, nil)
mocks.Cassandra.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:6379",
"error": "cassandra not connected",
},
}, nil)
mocks.Clickhouse.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:6379",
"error": "clickhouse not connected",
},
}, nil)
mocks.Oracle.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:1521",
"error": "oracle not connected",
},
}, nil)
mocks.KVStore.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:1234",
"error": "kv-store not connected",
},
}, nil)
mocks.DGraph.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:8000",
"error": "dgraph not connected",
},
}, nil)
mocks.OpenTSDB.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:8000",
"error": "opentsdb not connected",
},
}, nil)
mocks.PubSub.EXPECT().Health().Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:pubsub",
"error": nil,
},
}).Times(1)
mocks.Elasticsearch.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:9200",
"error": "elasticsearch not connected",
},
}, nil)
mocks.Couchbase.EXPECT().HealthCheck(gomock.Any()).Return(datasource.Health{
Status: health,
Details: map[string]any{
"host": "localhost:9000",
"error": "couchbase not connected",
},
}, nil)
}
func getExpectedData(datasourceHealth, appHealth, srvURL string) map[string]any {
return map[string]any{
"kv-store": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:1234",
"error": "kv-store not connected",
},
},
"redis": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:6379",
"error": "redis not connected",
},
},
"mongo": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:6379",
"error": "mongo not connected",
},
},
"clickHouse": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:6379",
"error": "clickhouse not connected",
},
},
"oracle": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:1521",
"error": "oracle not connected",
},
},
"cassandra": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:6379",
"error": "cassandra not connected",
},
},
"sql": &datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:3306/test",
"stats": sql.DBStats{
MaxOpenConnections: 0, OpenConnections: 1, InUse: 0, Idle: 1, WaitCount: 0,
WaitDuration: 0, MaxIdleClosed: 0, MaxIdleTimeClosed: 0, MaxLifetimeClosed: 0,
},
},
},
"dgraph": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:8000",
"error": "dgraph not connected",
},
},
"opentsdb": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:8000",
"error": "opentsdb not connected",
},
},
"elasticsearch": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:9200",
"error": "elasticsearch not connected",
},
},
"couchbase": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:9000",
"error": "couchbase not connected",
},
},
"pubsub": datasource.Health{
Status: datasourceHealth, Details: map[string]any{
"host": "localhost:pubsub",
"error": nil,
},
},
"test-service": &service.Health{
Status: "UP", Details: map[string]any{
"host": strings.TrimPrefix(srvURL, "http://"),
},
},
"name": "test-app",
"status": appHealth,
"version": "test",
}
}
================================================
FILE: pkg/gofr/container/metrics.go
================================================
package container
import "context"
type Metrics interface {
NewCounter(name, desc string)
NewUpDownCounter(name, desc string)
NewHistogram(name, desc string, buckets ...float64)
NewGauge(name, desc string)
IncrementCounter(ctx context.Context, name string, labels ...string)
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/container/mock_container.go
================================================
package container
import (
"context"
"testing"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/datasource/sql"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
)
type Mocks struct {
Redis *MockRedis
SQL *mockSQL
Clickhouse *MockClickhouse
Cassandra *MockCassandraWithContext
Mongo *MockMongo
KVStore *MockKVStore
DGraph *MockDgraph
ArangoDB *MockArangoDBProvider
OpenTSDB *MockOpenTSDB
SurrealDB *MockSurrealDB
Elasticsearch *MockElasticsearch
PubSub *MockPubSubProvider
Couchbase *MockCouchbase
File *file.MockFileSystemProvider
// Deprecated: Use HTTPServices map instead. This field is kept for backward compatibility only and will be removed in a future version.
HTTPService *service.MockHTTP
// Map of service names to their mock instances. Use this to set different expectations for different services.
HTTPServices map[string]*service.MockHTTP
Metrics *MockMetrics
Oracle *MockOracleDB
ScyllaDB *MockScyllaDB
}
type options func(c *Container, ctrl *gomock.Controller) any
func WithMockHTTPService(httpServiceNames ...string) options { //nolint:revive // WithMockHTTPService returns an
// exported type intentionally; options are internal and subject to change.
return func(c *Container, ctrl *gomock.Controller) any {
// Create a separate mock instance for each service name
// This allows different services to have different expectations
serviceMocks := make(map[string]*service.MockHTTP)
for _, s := range httpServiceNames {
mockservice := service.NewMockHTTP(ctrl)
c.Services[s] = mockservice
serviceMocks[s] = mockservice
}
// Return the map of service mocks
return serviceMocks
}
}
// Helper function to initialize all container DB/service mocks.
func setContainerMocks(c *Container, ctrl *gomock.Controller) {
c.Redis = NewMockRedis(ctrl)
c.Cassandra = NewMockCassandraWithContext(ctrl)
c.Clickhouse = NewMockClickhouse(ctrl)
c.Oracle = NewMockOracleDB(ctrl)
c.Mongo = NewMockMongo(ctrl)
c.KVStore = NewMockKVStore(ctrl)
c.File = file.NewMockFileSystemProvider(ctrl)
c.DGraph = NewMockDgraph(ctrl)
c.OpenTSDB = NewMockOpenTSDB(ctrl)
c.ArangoDB = NewMockArangoDBProvider(ctrl)
c.SurrealDB = NewMockSurrealDB(ctrl)
c.Elasticsearch = NewMockElasticsearch(ctrl)
c.ScyllaDB = NewMockScyllaDB(ctrl)
c.PubSub = NewMockPubSubProvider(ctrl)
c.Couchbase = NewMockCouchbase(ctrl)
}
func NewMockContainer(t *testing.T, options ...options) (*Container, *Mocks) {
t.Helper()
container := &Container{}
container.Logger = logging.NewLogger(logging.DEBUG)
ctrl := gomock.NewController(t)
mockDB, sqlMock, _ := sql.NewSQLMocks(t)
// initialization of expectations.
expectation := expectedQuery{}
sqlMockWrapper := &mockSQL{sqlMock, &expectation}
sqlDB := &sqlMockDB{mockDB, &expectation, logging.NewLogger(logging.DEBUG)}
sqlDB.finish(t)
container.SQL = sqlDB
// Initialize all other mocks via helpers.
setContainerMocks(container, ctrl)
var httpMock *service.MockHTTP
httpServiceMocks := make(map[string]*service.MockHTTP)
// Initialize Services map BEFORE processing options so WithMockHTTPService can populate it
container.Services = make(map[string]service.HTTP)
for _, option := range options {
optionsAdded := option(container, ctrl)
// Check if the option returned a map of HTTP service mocks
switch val := optionsAdded.(type) {
case map[string]*service.MockHTTP:
// Merge the service mocks into our map
for name, mock := range val {
httpServiceMocks[name] = mock
}
// Set httpMock to the first service mock for backward compatibility
if httpMock == nil && len(val) > 0 {
for _, mock := range val {
httpMock = mock
break
}
}
case *service.MockHTTP:
// Legacy support: if a single mock is returned, use it
httpMock = val
}
}
// Setup expectations/mockmetrics
container.Redis.(*MockRedis).EXPECT().Close().AnyTimes()
mockMetrics := NewMockMetrics(ctrl)
container.metricsManager = mockMetrics
mocks := Mocks{
Redis: container.Redis.(*MockRedis),
SQL: sqlMockWrapper,
Clickhouse: container.Clickhouse.(*MockClickhouse),
Cassandra: container.Cassandra.(*MockCassandraWithContext),
Mongo: container.Mongo.(*MockMongo),
KVStore: container.KVStore.(*MockKVStore),
File: container.File.(*file.MockFileSystemProvider),
HTTPService: httpMock, // Backward compatibility: first service mock or nil
HTTPServices: httpServiceMocks, // Map of all service mocks
DGraph: container.DGraph.(*MockDgraph),
OpenTSDB: container.OpenTSDB.(*MockOpenTSDB),
ArangoDB: container.ArangoDB.(*MockArangoDBProvider),
SurrealDB: container.SurrealDB.(*MockSurrealDB),
Elasticsearch: container.Elasticsearch.(*MockElasticsearch),
PubSub: container.PubSub.(*MockPubSubProvider),
Metrics: mockMetrics,
Oracle: container.Oracle.(*MockOracleDB),
ScyllaDB: container.ScyllaDB.(*MockScyllaDB),
Couchbase: container.Couchbase.(*MockCouchbase),
}
container.metricsManager = mocks.Metrics
// TODO: Remove this expectation from mock container (previous generalization) to the actual tests where their expectations are being set.
mocks.Metrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return container, &mocks
}
type MockPubSub struct{}
func (*MockPubSub) Query(_ context.Context, _ string, _ ...any) ([]byte, error) {
return nil, nil
}
func (*MockPubSub) CreateTopic(_ context.Context, _ string) error {
return nil
}
func (*MockPubSub) DeleteTopic(_ context.Context, _ string) error {
return nil
}
func (*MockPubSub) Health() datasource.Health {
return datasource.Health{}
}
func (*MockPubSub) Publish(_ context.Context, _ string, _ []byte) error {
return nil
}
func (*MockPubSub) Subscribe(_ context.Context, _ string) (*pubsub.Message, error) {
return nil, nil
}
func (*MockPubSub) Close() error { return nil }
================================================
FILE: pkg/gofr/container/mock_datasources.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: datasources.go
//
// Generated by this command:
//
// mockgen -source=datasources.go -destination=mock_datasources.go -package=container
//
// Package container is a generated GoMock package.
package container
import (
bytes "bytes"
context "context"
sql "database/sql"
reflect "reflect"
time "time"
redis "github.com/redis/go-redis/v9"
gomock "go.uber.org/mock/gomock"
datasource "gofr.dev/pkg/gofr/datasource"
pubsub "gofr.dev/pkg/gofr/datasource/pubsub"
sql0 "gofr.dev/pkg/gofr/datasource/sql"
)
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
isgomock struct{}
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// Begin mocks base method.
func (m *MockDB) Begin() (*sql0.Tx, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Begin")
ret0, _ := ret[0].(*sql0.Tx)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Begin indicates an expected call of Begin.
func (mr *MockDBMockRecorder) Begin() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockDB)(nil).Begin))
}
// Close mocks base method.
func (m *MockDB) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockDBMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDB)(nil).Close))
}
// Dialect mocks base method.
func (m *MockDB) Dialect() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dialect")
ret0, _ := ret[0].(string)
return ret0
}
// Dialect indicates an expected call of Dialect.
func (mr *MockDBMockRecorder) Dialect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dialect", reflect.TypeOf((*MockDB)(nil).Dialect))
}
// Exec mocks base method.
func (m *MockDB) Exec(query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec.
func (mr *MockDBMockRecorder) Exec(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDB)(nil).Exec), varargs...)
}
// ExecContext mocks base method.
func (m *MockDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecContext", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecContext indicates an expected call of ExecContext.
func (mr *MockDBMockRecorder) ExecContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockDB)(nil).ExecContext), varargs...)
}
// HealthCheck mocks base method.
func (m *MockDB) HealthCheck() *datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck")
ret0, _ := ret[0].(*datasource.Health)
return ret0
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDBMockRecorder) HealthCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDB)(nil).HealthCheck))
}
// Prepare mocks base method.
func (m *MockDB) Prepare(query string) (*sql.Stmt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Prepare", query)
ret0, _ := ret[0].(*sql.Stmt)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Prepare indicates an expected call of Prepare.
func (mr *MockDBMockRecorder) Prepare(query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockDB)(nil).Prepare), query)
}
// Query mocks base method.
func (m *MockDB) Query(query string, args ...any) (*sql.Rows, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(*sql.Rows)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockDBMockRecorder) Query(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDB)(nil).Query), varargs...)
}
// QueryContext mocks base method.
func (m *MockDB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryContext", varargs...)
ret0, _ := ret[0].(*sql.Rows)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryContext indicates an expected call of QueryContext.
func (mr *MockDBMockRecorder) QueryContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryContext", reflect.TypeOf((*MockDB)(nil).QueryContext), varargs...)
}
// QueryRow mocks base method.
func (m *MockDB) QueryRow(query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRow", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRow indicates an expected call of QueryRow.
func (mr *MockDBMockRecorder) QueryRow(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockDB)(nil).QueryRow), varargs...)
}
// QueryRowContext mocks base method.
func (m *MockDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRowContext", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRowContext indicates an expected call of QueryRowContext.
func (mr *MockDBMockRecorder) QueryRowContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRowContext", reflect.TypeOf((*MockDB)(nil).QueryRowContext), varargs...)
}
// Select mocks base method.
func (m *MockDB) Select(ctx context.Context, data any, query string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{ctx, data, query}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Select", varargs...)
}
// Select indicates an expected call of Select.
func (mr *MockDBMockRecorder) Select(ctx, data, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, data, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockDB)(nil).Select), varargs...)
}
// MockRedis is a mock of Redis interface.
type MockRedis struct {
ctrl *gomock.Controller
recorder *MockRedisMockRecorder
isgomock struct{}
}
// MockRedisMockRecorder is the mock recorder for MockRedis.
type MockRedisMockRecorder struct {
mock *MockRedis
}
// NewMockRedis creates a new mock instance.
func NewMockRedis(ctrl *gomock.Controller) *MockRedis {
mock := &MockRedis{ctrl: ctrl}
mock.recorder = &MockRedisMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRedis) EXPECT() *MockRedisMockRecorder {
return m.recorder
}
// ACLCat mocks base method.
func (m *MockRedis) ACLCat(ctx context.Context) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLCat", ctx)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ACLCat indicates an expected call of ACLCat.
func (mr *MockRedisMockRecorder) ACLCat(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLCat", reflect.TypeOf((*MockRedis)(nil).ACLCat), ctx)
}
// ACLCatArgs mocks base method.
func (m *MockRedis) ACLCatArgs(ctx context.Context, options *redis.ACLCatArgs) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLCatArgs", ctx, options)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ACLCatArgs indicates an expected call of ACLCatArgs.
func (mr *MockRedisMockRecorder) ACLCatArgs(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLCatArgs", reflect.TypeOf((*MockRedis)(nil).ACLCatArgs), ctx, options)
}
// ACLDelUser mocks base method.
func (m *MockRedis) ACLDelUser(ctx context.Context, username string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLDelUser", ctx, username)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ACLDelUser indicates an expected call of ACLDelUser.
func (mr *MockRedisMockRecorder) ACLDelUser(ctx, username any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLDelUser", reflect.TypeOf((*MockRedis)(nil).ACLDelUser), ctx, username)
}
// ACLDryRun mocks base method.
func (m *MockRedis) ACLDryRun(ctx context.Context, username string, command ...any) *redis.StringCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, username}
for _, a := range command {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ACLDryRun", varargs...)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ACLDryRun indicates an expected call of ACLDryRun.
func (mr *MockRedisMockRecorder) ACLDryRun(ctx, username any, command ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, username}, command...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLDryRun", reflect.TypeOf((*MockRedis)(nil).ACLDryRun), varargs...)
}
// ACLGenPass mocks base method.
func (m *MockRedis) ACLGenPass(ctx context.Context, bit int) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLGenPass", ctx, bit)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ACLGenPass indicates an expected call of ACLGenPass.
func (mr *MockRedisMockRecorder) ACLGenPass(ctx, bit any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLGenPass", reflect.TypeOf((*MockRedis)(nil).ACLGenPass), ctx, bit)
}
// ACLList mocks base method.
func (m *MockRedis) ACLList(ctx context.Context) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLList", ctx)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ACLList indicates an expected call of ACLList.
func (mr *MockRedisMockRecorder) ACLList(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLList", reflect.TypeOf((*MockRedis)(nil).ACLList), ctx)
}
// ACLLog mocks base method.
func (m *MockRedis) ACLLog(ctx context.Context, count int64) *redis.ACLLogCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLLog", ctx, count)
ret0, _ := ret[0].(*redis.ACLLogCmd)
return ret0
}
// ACLLog indicates an expected call of ACLLog.
func (mr *MockRedisMockRecorder) ACLLog(ctx, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLLog", reflect.TypeOf((*MockRedis)(nil).ACLLog), ctx, count)
}
// ACLLogReset mocks base method.
func (m *MockRedis) ACLLogReset(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLLogReset", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ACLLogReset indicates an expected call of ACLLogReset.
func (mr *MockRedisMockRecorder) ACLLogReset(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLLogReset", reflect.TypeOf((*MockRedis)(nil).ACLLogReset), ctx)
}
// ACLSetUser mocks base method.
func (m *MockRedis) ACLSetUser(ctx context.Context, username string, rules ...string) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, username}
for _, a := range rules {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ACLSetUser", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ACLSetUser indicates an expected call of ACLSetUser.
func (mr *MockRedisMockRecorder) ACLSetUser(ctx, username any, rules ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, username}, rules...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLSetUser", reflect.TypeOf((*MockRedis)(nil).ACLSetUser), varargs...)
}
// ACLUsers mocks base method.
func (m *MockRedis) ACLUsers(ctx context.Context) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLUsers", ctx)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ACLUsers indicates an expected call of ACLUsers.
func (mr *MockRedisMockRecorder) ACLUsers(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLUsers", reflect.TypeOf((*MockRedis)(nil).ACLUsers), ctx)
}
// ACLWhoAmI mocks base method.
func (m *MockRedis) ACLWhoAmI(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ACLWhoAmI", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ACLWhoAmI indicates an expected call of ACLWhoAmI.
func (mr *MockRedisMockRecorder) ACLWhoAmI(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ACLWhoAmI", reflect.TypeOf((*MockRedis)(nil).ACLWhoAmI), ctx)
}
// Append mocks base method.
func (m *MockRedis) Append(ctx context.Context, key, value string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Append", ctx, key, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Append indicates an expected call of Append.
func (mr *MockRedisMockRecorder) Append(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Append", reflect.TypeOf((*MockRedis)(nil).Append), ctx, key, value)
}
// BFAdd mocks base method.
func (m *MockRedis) BFAdd(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFAdd", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// BFAdd indicates an expected call of BFAdd.
func (mr *MockRedisMockRecorder) BFAdd(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFAdd", reflect.TypeOf((*MockRedis)(nil).BFAdd), ctx, key, element)
}
// BFCard mocks base method.
func (m *MockRedis) BFCard(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFCard", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BFCard indicates an expected call of BFCard.
func (mr *MockRedisMockRecorder) BFCard(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFCard", reflect.TypeOf((*MockRedis)(nil).BFCard), ctx, key)
}
// BFExists mocks base method.
func (m *MockRedis) BFExists(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFExists", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// BFExists indicates an expected call of BFExists.
func (mr *MockRedisMockRecorder) BFExists(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFExists", reflect.TypeOf((*MockRedis)(nil).BFExists), ctx, key, element)
}
// BFInfo mocks base method.
func (m *MockRedis) BFInfo(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfo", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfo indicates an expected call of BFInfo.
func (mr *MockRedisMockRecorder) BFInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfo", reflect.TypeOf((*MockRedis)(nil).BFInfo), ctx, key)
}
// BFInfoArg mocks base method.
func (m *MockRedis) BFInfoArg(ctx context.Context, key, option string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoArg", ctx, key, option)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoArg indicates an expected call of BFInfoArg.
func (mr *MockRedisMockRecorder) BFInfoArg(ctx, key, option any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoArg", reflect.TypeOf((*MockRedis)(nil).BFInfoArg), ctx, key, option)
}
// BFInfoCapacity mocks base method.
func (m *MockRedis) BFInfoCapacity(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoCapacity", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoCapacity indicates an expected call of BFInfoCapacity.
func (mr *MockRedisMockRecorder) BFInfoCapacity(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoCapacity", reflect.TypeOf((*MockRedis)(nil).BFInfoCapacity), ctx, key)
}
// BFInfoExpansion mocks base method.
func (m *MockRedis) BFInfoExpansion(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoExpansion", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoExpansion indicates an expected call of BFInfoExpansion.
func (mr *MockRedisMockRecorder) BFInfoExpansion(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoExpansion", reflect.TypeOf((*MockRedis)(nil).BFInfoExpansion), ctx, key)
}
// BFInfoFilters mocks base method.
func (m *MockRedis) BFInfoFilters(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoFilters", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoFilters indicates an expected call of BFInfoFilters.
func (mr *MockRedisMockRecorder) BFInfoFilters(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoFilters", reflect.TypeOf((*MockRedis)(nil).BFInfoFilters), ctx, key)
}
// BFInfoItems mocks base method.
func (m *MockRedis) BFInfoItems(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoItems", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoItems indicates an expected call of BFInfoItems.
func (mr *MockRedisMockRecorder) BFInfoItems(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoItems", reflect.TypeOf((*MockRedis)(nil).BFInfoItems), ctx, key)
}
// BFInfoSize mocks base method.
func (m *MockRedis) BFInfoSize(ctx context.Context, key string) *redis.BFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFInfoSize", ctx, key)
ret0, _ := ret[0].(*redis.BFInfoCmd)
return ret0
}
// BFInfoSize indicates an expected call of BFInfoSize.
func (mr *MockRedisMockRecorder) BFInfoSize(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInfoSize", reflect.TypeOf((*MockRedis)(nil).BFInfoSize), ctx, key)
}
// BFInsert mocks base method.
func (m *MockRedis) BFInsert(ctx context.Context, key string, options *redis.BFInsertOptions, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BFInsert", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// BFInsert indicates an expected call of BFInsert.
func (mr *MockRedisMockRecorder) BFInsert(ctx, key, options any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFInsert", reflect.TypeOf((*MockRedis)(nil).BFInsert), varargs...)
}
// BFLoadChunk mocks base method.
func (m *MockRedis) BFLoadChunk(ctx context.Context, key string, iterator int64, data any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFLoadChunk", ctx, key, iterator, data)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BFLoadChunk indicates an expected call of BFLoadChunk.
func (mr *MockRedisMockRecorder) BFLoadChunk(ctx, key, iterator, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFLoadChunk", reflect.TypeOf((*MockRedis)(nil).BFLoadChunk), ctx, key, iterator, data)
}
// BFMAdd mocks base method.
func (m *MockRedis) BFMAdd(ctx context.Context, key string, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BFMAdd", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// BFMAdd indicates an expected call of BFMAdd.
func (mr *MockRedisMockRecorder) BFMAdd(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFMAdd", reflect.TypeOf((*MockRedis)(nil).BFMAdd), varargs...)
}
// BFMExists mocks base method.
func (m *MockRedis) BFMExists(ctx context.Context, key string, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BFMExists", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// BFMExists indicates an expected call of BFMExists.
func (mr *MockRedisMockRecorder) BFMExists(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFMExists", reflect.TypeOf((*MockRedis)(nil).BFMExists), varargs...)
}
// BFReserve mocks base method.
func (m *MockRedis) BFReserve(ctx context.Context, key string, errorRate float64, capacity int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFReserve", ctx, key, errorRate, capacity)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BFReserve indicates an expected call of BFReserve.
func (mr *MockRedisMockRecorder) BFReserve(ctx, key, errorRate, capacity any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFReserve", reflect.TypeOf((*MockRedis)(nil).BFReserve), ctx, key, errorRate, capacity)
}
// BFReserveExpansion mocks base method.
func (m *MockRedis) BFReserveExpansion(ctx context.Context, key string, errorRate float64, capacity, expansion int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFReserveExpansion", ctx, key, errorRate, capacity, expansion)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BFReserveExpansion indicates an expected call of BFReserveExpansion.
func (mr *MockRedisMockRecorder) BFReserveExpansion(ctx, key, errorRate, capacity, expansion any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFReserveExpansion", reflect.TypeOf((*MockRedis)(nil).BFReserveExpansion), ctx, key, errorRate, capacity, expansion)
}
// BFReserveNonScaling mocks base method.
func (m *MockRedis) BFReserveNonScaling(ctx context.Context, key string, errorRate float64, capacity int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFReserveNonScaling", ctx, key, errorRate, capacity)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BFReserveNonScaling indicates an expected call of BFReserveNonScaling.
func (mr *MockRedisMockRecorder) BFReserveNonScaling(ctx, key, errorRate, capacity any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFReserveNonScaling", reflect.TypeOf((*MockRedis)(nil).BFReserveNonScaling), ctx, key, errorRate, capacity)
}
// BFReserveWithArgs mocks base method.
func (m *MockRedis) BFReserveWithArgs(ctx context.Context, key string, options *redis.BFReserveOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFReserveWithArgs", ctx, key, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BFReserveWithArgs indicates an expected call of BFReserveWithArgs.
func (mr *MockRedisMockRecorder) BFReserveWithArgs(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFReserveWithArgs", reflect.TypeOf((*MockRedis)(nil).BFReserveWithArgs), ctx, key, options)
}
// BFScanDump mocks base method.
func (m *MockRedis) BFScanDump(ctx context.Context, key string, iterator int64) *redis.ScanDumpCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BFScanDump", ctx, key, iterator)
ret0, _ := ret[0].(*redis.ScanDumpCmd)
return ret0
}
// BFScanDump indicates an expected call of BFScanDump.
func (mr *MockRedisMockRecorder) BFScanDump(ctx, key, iterator any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BFScanDump", reflect.TypeOf((*MockRedis)(nil).BFScanDump), ctx, key, iterator)
}
// BLMPop mocks base method.
func (m *MockRedis) BLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *redis.KeyValuesCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout, direction, count}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BLMPop", varargs...)
ret0, _ := ret[0].(*redis.KeyValuesCmd)
return ret0
}
// BLMPop indicates an expected call of BLMPop.
func (mr *MockRedisMockRecorder) BLMPop(ctx, timeout, direction, count any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout, direction, count}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLMPop", reflect.TypeOf((*MockRedis)(nil).BLMPop), varargs...)
}
// BLMove mocks base method.
func (m *MockRedis) BLMove(ctx context.Context, source, destination, srcpos, destpos string, timeout time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BLMove", ctx, source, destination, srcpos, destpos, timeout)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// BLMove indicates an expected call of BLMove.
func (mr *MockRedisMockRecorder) BLMove(ctx, source, destination, srcpos, destpos, timeout any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLMove", reflect.TypeOf((*MockRedis)(nil).BLMove), ctx, source, destination, srcpos, destpos, timeout)
}
// BLPop mocks base method.
func (m *MockRedis) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BLPop", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// BLPop indicates an expected call of BLPop.
func (mr *MockRedisMockRecorder) BLPop(ctx, timeout any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BLPop", reflect.TypeOf((*MockRedis)(nil).BLPop), varargs...)
}
// BRPop mocks base method.
func (m *MockRedis) BRPop(ctx context.Context, timeout time.Duration, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BRPop", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// BRPop indicates an expected call of BRPop.
func (mr *MockRedisMockRecorder) BRPop(ctx, timeout any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BRPop", reflect.TypeOf((*MockRedis)(nil).BRPop), varargs...)
}
// BRPopLPush mocks base method.
func (m *MockRedis) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BRPopLPush", ctx, source, destination, timeout)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// BRPopLPush indicates an expected call of BRPopLPush.
func (mr *MockRedisMockRecorder) BRPopLPush(ctx, source, destination, timeout any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BRPopLPush", reflect.TypeOf((*MockRedis)(nil).BRPopLPush), ctx, source, destination, timeout)
}
// BZMPop mocks base method.
func (m *MockRedis) BZMPop(ctx context.Context, timeout time.Duration, order string, count int64, keys ...string) *redis.ZSliceWithKeyCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout, order, count}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BZMPop", varargs...)
ret0, _ := ret[0].(*redis.ZSliceWithKeyCmd)
return ret0
}
// BZMPop indicates an expected call of BZMPop.
func (mr *MockRedisMockRecorder) BZMPop(ctx, timeout, order, count any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout, order, count}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZMPop", reflect.TypeOf((*MockRedis)(nil).BZMPop), varargs...)
}
// BZPopMax mocks base method.
func (m *MockRedis) BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *redis.ZWithKeyCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BZPopMax", varargs...)
ret0, _ := ret[0].(*redis.ZWithKeyCmd)
return ret0
}
// BZPopMax indicates an expected call of BZPopMax.
func (mr *MockRedisMockRecorder) BZPopMax(ctx, timeout any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZPopMax", reflect.TypeOf((*MockRedis)(nil).BZPopMax), varargs...)
}
// BZPopMin mocks base method.
func (m *MockRedis) BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *redis.ZWithKeyCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, timeout}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BZPopMin", varargs...)
ret0, _ := ret[0].(*redis.ZWithKeyCmd)
return ret0
}
// BZPopMin indicates an expected call of BZPopMin.
func (mr *MockRedisMockRecorder) BZPopMin(ctx, timeout any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, timeout}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BZPopMin", reflect.TypeOf((*MockRedis)(nil).BZPopMin), varargs...)
}
// BgRewriteAOF mocks base method.
func (m *MockRedis) BgRewriteAOF(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BgRewriteAOF", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BgRewriteAOF indicates an expected call of BgRewriteAOF.
func (mr *MockRedisMockRecorder) BgRewriteAOF(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BgRewriteAOF", reflect.TypeOf((*MockRedis)(nil).BgRewriteAOF), ctx)
}
// BgSave mocks base method.
func (m *MockRedis) BgSave(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BgSave", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// BgSave indicates an expected call of BgSave.
func (mr *MockRedisMockRecorder) BgSave(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BgSave", reflect.TypeOf((*MockRedis)(nil).BgSave), ctx)
}
// BitCount mocks base method.
func (m *MockRedis) BitCount(ctx context.Context, key string, bitCount *redis.BitCount) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BitCount", ctx, key, bitCount)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitCount indicates an expected call of BitCount.
func (mr *MockRedisMockRecorder) BitCount(ctx, key, bitCount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitCount", reflect.TypeOf((*MockRedis)(nil).BitCount), ctx, key, bitCount)
}
// BitField mocks base method.
func (m *MockRedis) BitField(ctx context.Context, key string, values ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitField", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// BitField indicates an expected call of BitField.
func (mr *MockRedisMockRecorder) BitField(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitField", reflect.TypeOf((*MockRedis)(nil).BitField), varargs...)
}
// BitFieldRO mocks base method.
func (m *MockRedis) BitFieldRO(ctx context.Context, key string, values ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitFieldRO", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// BitFieldRO indicates an expected call of BitFieldRO.
func (mr *MockRedisMockRecorder) BitFieldRO(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitFieldRO", reflect.TypeOf((*MockRedis)(nil).BitFieldRO), varargs...)
}
// BitOpAnd mocks base method.
func (m *MockRedis) BitOpAnd(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpAnd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpAnd indicates an expected call of BitOpAnd.
func (mr *MockRedisMockRecorder) BitOpAnd(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpAnd", reflect.TypeOf((*MockRedis)(nil).BitOpAnd), varargs...)
}
// BitOpAndOr mocks base method.
func (m *MockRedis) BitOpAndOr(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpAndOr", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpAndOr indicates an expected call of BitOpAndOr.
func (mr *MockRedisMockRecorder) BitOpAndOr(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpAndOr", reflect.TypeOf((*MockRedis)(nil).BitOpAndOr), varargs...)
}
// BitOpDiff mocks base method.
func (m *MockRedis) BitOpDiff(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpDiff", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpDiff indicates an expected call of BitOpDiff.
func (mr *MockRedisMockRecorder) BitOpDiff(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpDiff", reflect.TypeOf((*MockRedis)(nil).BitOpDiff), varargs...)
}
// BitOpDiff1 mocks base method.
func (m *MockRedis) BitOpDiff1(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpDiff1", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpDiff1 indicates an expected call of BitOpDiff1.
func (mr *MockRedisMockRecorder) BitOpDiff1(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpDiff1", reflect.TypeOf((*MockRedis)(nil).BitOpDiff1), varargs...)
}
// BitOpNot mocks base method.
func (m *MockRedis) BitOpNot(ctx context.Context, destKey, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BitOpNot", ctx, destKey, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpNot indicates an expected call of BitOpNot.
func (mr *MockRedisMockRecorder) BitOpNot(ctx, destKey, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpNot", reflect.TypeOf((*MockRedis)(nil).BitOpNot), ctx, destKey, key)
}
// BitOpOne mocks base method.
func (m *MockRedis) BitOpOne(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpOne", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpOne indicates an expected call of BitOpOne.
func (mr *MockRedisMockRecorder) BitOpOne(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpOne", reflect.TypeOf((*MockRedis)(nil).BitOpOne), varargs...)
}
// BitOpOr mocks base method.
func (m *MockRedis) BitOpOr(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpOr", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpOr indicates an expected call of BitOpOr.
func (mr *MockRedisMockRecorder) BitOpOr(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpOr", reflect.TypeOf((*MockRedis)(nil).BitOpOr), varargs...)
}
// BitOpXor mocks base method.
func (m *MockRedis) BitOpXor(ctx context.Context, destKey string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitOpXor", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitOpXor indicates an expected call of BitOpXor.
func (mr *MockRedisMockRecorder) BitOpXor(ctx, destKey any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitOpXor", reflect.TypeOf((*MockRedis)(nil).BitOpXor), varargs...)
}
// BitPos mocks base method.
func (m *MockRedis) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, bit}
for _, a := range pos {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BitPos", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitPos indicates an expected call of BitPos.
func (mr *MockRedisMockRecorder) BitPos(ctx, key, bit any, pos ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, bit}, pos...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitPos", reflect.TypeOf((*MockRedis)(nil).BitPos), varargs...)
}
// BitPosSpan mocks base method.
func (m *MockRedis) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BitPosSpan", ctx, key, bit, start, end, span)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// BitPosSpan indicates an expected call of BitPosSpan.
func (mr *MockRedisMockRecorder) BitPosSpan(ctx, key, bit, start, end, span any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BitPosSpan", reflect.TypeOf((*MockRedis)(nil).BitPosSpan), ctx, key, bit, start, end, span)
}
// CFAdd mocks base method.
func (m *MockRedis) CFAdd(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFAdd", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// CFAdd indicates an expected call of CFAdd.
func (mr *MockRedisMockRecorder) CFAdd(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFAdd", reflect.TypeOf((*MockRedis)(nil).CFAdd), ctx, key, element)
}
// CFAddNX mocks base method.
func (m *MockRedis) CFAddNX(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFAddNX", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// CFAddNX indicates an expected call of CFAddNX.
func (mr *MockRedisMockRecorder) CFAddNX(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFAddNX", reflect.TypeOf((*MockRedis)(nil).CFAddNX), ctx, key, element)
}
// CFCount mocks base method.
func (m *MockRedis) CFCount(ctx context.Context, key string, element any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFCount", ctx, key, element)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// CFCount indicates an expected call of CFCount.
func (mr *MockRedisMockRecorder) CFCount(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFCount", reflect.TypeOf((*MockRedis)(nil).CFCount), ctx, key, element)
}
// CFDel mocks base method.
func (m *MockRedis) CFDel(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFDel", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// CFDel indicates an expected call of CFDel.
func (mr *MockRedisMockRecorder) CFDel(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFDel", reflect.TypeOf((*MockRedis)(nil).CFDel), ctx, key, element)
}
// CFExists mocks base method.
func (m *MockRedis) CFExists(ctx context.Context, key string, element any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFExists", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// CFExists indicates an expected call of CFExists.
func (mr *MockRedisMockRecorder) CFExists(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFExists", reflect.TypeOf((*MockRedis)(nil).CFExists), ctx, key, element)
}
// CFInfo mocks base method.
func (m *MockRedis) CFInfo(ctx context.Context, key string) *redis.CFInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFInfo", ctx, key)
ret0, _ := ret[0].(*redis.CFInfoCmd)
return ret0
}
// CFInfo indicates an expected call of CFInfo.
func (mr *MockRedisMockRecorder) CFInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFInfo", reflect.TypeOf((*MockRedis)(nil).CFInfo), ctx, key)
}
// CFInsert mocks base method.
func (m *MockRedis) CFInsert(ctx context.Context, key string, options *redis.CFInsertOptions, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CFInsert", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// CFInsert indicates an expected call of CFInsert.
func (mr *MockRedisMockRecorder) CFInsert(ctx, key, options any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFInsert", reflect.TypeOf((*MockRedis)(nil).CFInsert), varargs...)
}
// CFInsertNX mocks base method.
func (m *MockRedis) CFInsertNX(ctx context.Context, key string, options *redis.CFInsertOptions, elements ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CFInsertNX", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// CFInsertNX indicates an expected call of CFInsertNX.
func (mr *MockRedisMockRecorder) CFInsertNX(ctx, key, options any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFInsertNX", reflect.TypeOf((*MockRedis)(nil).CFInsertNX), varargs...)
}
// CFLoadChunk mocks base method.
func (m *MockRedis) CFLoadChunk(ctx context.Context, key string, iterator int64, data any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFLoadChunk", ctx, key, iterator, data)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFLoadChunk indicates an expected call of CFLoadChunk.
func (mr *MockRedisMockRecorder) CFLoadChunk(ctx, key, iterator, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFLoadChunk", reflect.TypeOf((*MockRedis)(nil).CFLoadChunk), ctx, key, iterator, data)
}
// CFMExists mocks base method.
func (m *MockRedis) CFMExists(ctx context.Context, key string, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CFMExists", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// CFMExists indicates an expected call of CFMExists.
func (mr *MockRedisMockRecorder) CFMExists(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFMExists", reflect.TypeOf((*MockRedis)(nil).CFMExists), varargs...)
}
// CFReserve mocks base method.
func (m *MockRedis) CFReserve(ctx context.Context, key string, capacity int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFReserve", ctx, key, capacity)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFReserve indicates an expected call of CFReserve.
func (mr *MockRedisMockRecorder) CFReserve(ctx, key, capacity any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFReserve", reflect.TypeOf((*MockRedis)(nil).CFReserve), ctx, key, capacity)
}
// CFReserveBucketSize mocks base method.
func (m *MockRedis) CFReserveBucketSize(ctx context.Context, key string, capacity, bucketsize int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFReserveBucketSize", ctx, key, capacity, bucketsize)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFReserveBucketSize indicates an expected call of CFReserveBucketSize.
func (mr *MockRedisMockRecorder) CFReserveBucketSize(ctx, key, capacity, bucketsize any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFReserveBucketSize", reflect.TypeOf((*MockRedis)(nil).CFReserveBucketSize), ctx, key, capacity, bucketsize)
}
// CFReserveExpansion mocks base method.
func (m *MockRedis) CFReserveExpansion(ctx context.Context, key string, capacity, expansion int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFReserveExpansion", ctx, key, capacity, expansion)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFReserveExpansion indicates an expected call of CFReserveExpansion.
func (mr *MockRedisMockRecorder) CFReserveExpansion(ctx, key, capacity, expansion any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFReserveExpansion", reflect.TypeOf((*MockRedis)(nil).CFReserveExpansion), ctx, key, capacity, expansion)
}
// CFReserveMaxIterations mocks base method.
func (m *MockRedis) CFReserveMaxIterations(ctx context.Context, key string, capacity, maxiterations int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFReserveMaxIterations", ctx, key, capacity, maxiterations)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFReserveMaxIterations indicates an expected call of CFReserveMaxIterations.
func (mr *MockRedisMockRecorder) CFReserveMaxIterations(ctx, key, capacity, maxiterations any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFReserveMaxIterations", reflect.TypeOf((*MockRedis)(nil).CFReserveMaxIterations), ctx, key, capacity, maxiterations)
}
// CFReserveWithArgs mocks base method.
func (m *MockRedis) CFReserveWithArgs(ctx context.Context, key string, options *redis.CFReserveOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFReserveWithArgs", ctx, key, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CFReserveWithArgs indicates an expected call of CFReserveWithArgs.
func (mr *MockRedisMockRecorder) CFReserveWithArgs(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFReserveWithArgs", reflect.TypeOf((*MockRedis)(nil).CFReserveWithArgs), ctx, key, options)
}
// CFScanDump mocks base method.
func (m *MockRedis) CFScanDump(ctx context.Context, key string, iterator int64) *redis.ScanDumpCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CFScanDump", ctx, key, iterator)
ret0, _ := ret[0].(*redis.ScanDumpCmd)
return ret0
}
// CFScanDump indicates an expected call of CFScanDump.
func (mr *MockRedisMockRecorder) CFScanDump(ctx, key, iterator any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CFScanDump", reflect.TypeOf((*MockRedis)(nil).CFScanDump), ctx, key, iterator)
}
// CMSIncrBy mocks base method.
func (m *MockRedis) CMSIncrBy(ctx context.Context, key string, elements ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CMSIncrBy", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// CMSIncrBy indicates an expected call of CMSIncrBy.
func (mr *MockRedisMockRecorder) CMSIncrBy(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSIncrBy", reflect.TypeOf((*MockRedis)(nil).CMSIncrBy), varargs...)
}
// CMSInfo mocks base method.
func (m *MockRedis) CMSInfo(ctx context.Context, key string) *redis.CMSInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CMSInfo", ctx, key)
ret0, _ := ret[0].(*redis.CMSInfoCmd)
return ret0
}
// CMSInfo indicates an expected call of CMSInfo.
func (mr *MockRedisMockRecorder) CMSInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSInfo", reflect.TypeOf((*MockRedis)(nil).CMSInfo), ctx, key)
}
// CMSInitByDim mocks base method.
func (m *MockRedis) CMSInitByDim(ctx context.Context, key string, width, height int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CMSInitByDim", ctx, key, width, height)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CMSInitByDim indicates an expected call of CMSInitByDim.
func (mr *MockRedisMockRecorder) CMSInitByDim(ctx, key, width, height any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSInitByDim", reflect.TypeOf((*MockRedis)(nil).CMSInitByDim), ctx, key, width, height)
}
// CMSInitByProb mocks base method.
func (m *MockRedis) CMSInitByProb(ctx context.Context, key string, errorRate, probability float64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CMSInitByProb", ctx, key, errorRate, probability)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CMSInitByProb indicates an expected call of CMSInitByProb.
func (mr *MockRedisMockRecorder) CMSInitByProb(ctx, key, errorRate, probability any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSInitByProb", reflect.TypeOf((*MockRedis)(nil).CMSInitByProb), ctx, key, errorRate, probability)
}
// CMSMerge mocks base method.
func (m *MockRedis) CMSMerge(ctx context.Context, destKey string, sourceKeys ...string) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey}
for _, a := range sourceKeys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CMSMerge", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CMSMerge indicates an expected call of CMSMerge.
func (mr *MockRedisMockRecorder) CMSMerge(ctx, destKey any, sourceKeys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey}, sourceKeys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSMerge", reflect.TypeOf((*MockRedis)(nil).CMSMerge), varargs...)
}
// CMSMergeWithWeight mocks base method.
func (m *MockRedis) CMSMergeWithWeight(ctx context.Context, destKey string, sourceKeys map[string]int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CMSMergeWithWeight", ctx, destKey, sourceKeys)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// CMSMergeWithWeight indicates an expected call of CMSMergeWithWeight.
func (mr *MockRedisMockRecorder) CMSMergeWithWeight(ctx, destKey, sourceKeys any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSMergeWithWeight", reflect.TypeOf((*MockRedis)(nil).CMSMergeWithWeight), ctx, destKey, sourceKeys)
}
// CMSQuery mocks base method.
func (m *MockRedis) CMSQuery(ctx context.Context, key string, elements ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CMSQuery", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// CMSQuery indicates an expected call of CMSQuery.
func (mr *MockRedisMockRecorder) CMSQuery(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CMSQuery", reflect.TypeOf((*MockRedis)(nil).CMSQuery), varargs...)
}
// ClientGetName mocks base method.
func (m *MockRedis) ClientGetName(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientGetName", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClientGetName indicates an expected call of ClientGetName.
func (mr *MockRedisMockRecorder) ClientGetName(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientGetName", reflect.TypeOf((*MockRedis)(nil).ClientGetName), ctx)
}
// ClientID mocks base method.
func (m *MockRedis) ClientID(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientID", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClientID indicates an expected call of ClientID.
func (mr *MockRedisMockRecorder) ClientID(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientID", reflect.TypeOf((*MockRedis)(nil).ClientID), ctx)
}
// ClientInfo mocks base method.
func (m *MockRedis) ClientInfo(ctx context.Context) *redis.ClientInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientInfo", ctx)
ret0, _ := ret[0].(*redis.ClientInfoCmd)
return ret0
}
// ClientInfo indicates an expected call of ClientInfo.
func (mr *MockRedisMockRecorder) ClientInfo(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientInfo", reflect.TypeOf((*MockRedis)(nil).ClientInfo), ctx)
}
// ClientKill mocks base method.
func (m *MockRedis) ClientKill(ctx context.Context, ipPort string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientKill", ctx, ipPort)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClientKill indicates an expected call of ClientKill.
func (mr *MockRedisMockRecorder) ClientKill(ctx, ipPort any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientKill", reflect.TypeOf((*MockRedis)(nil).ClientKill), ctx, ipPort)
}
// ClientKillByFilter mocks base method.
func (m *MockRedis) ClientKillByFilter(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ClientKillByFilter", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClientKillByFilter indicates an expected call of ClientKillByFilter.
func (mr *MockRedisMockRecorder) ClientKillByFilter(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientKillByFilter", reflect.TypeOf((*MockRedis)(nil).ClientKillByFilter), varargs...)
}
// ClientList mocks base method.
func (m *MockRedis) ClientList(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientList", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClientList indicates an expected call of ClientList.
func (mr *MockRedisMockRecorder) ClientList(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientList", reflect.TypeOf((*MockRedis)(nil).ClientList), ctx)
}
// ClientMaintNotifications mocks base method.
func (m *MockRedis) ClientMaintNotifications(ctx context.Context, enabled bool, endpointType string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientMaintNotifications", ctx, enabled, endpointType)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClientMaintNotifications indicates an expected call of ClientMaintNotifications.
func (mr *MockRedisMockRecorder) ClientMaintNotifications(ctx, enabled, endpointType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientMaintNotifications", reflect.TypeOf((*MockRedis)(nil).ClientMaintNotifications), ctx, enabled, endpointType)
}
// ClientPause mocks base method.
func (m *MockRedis) ClientPause(ctx context.Context, dur time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientPause", ctx, dur)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ClientPause indicates an expected call of ClientPause.
func (mr *MockRedisMockRecorder) ClientPause(ctx, dur any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientPause", reflect.TypeOf((*MockRedis)(nil).ClientPause), ctx, dur)
}
// ClientUnblock mocks base method.
func (m *MockRedis) ClientUnblock(ctx context.Context, id int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientUnblock", ctx, id)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClientUnblock indicates an expected call of ClientUnblock.
func (mr *MockRedisMockRecorder) ClientUnblock(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnblock", reflect.TypeOf((*MockRedis)(nil).ClientUnblock), ctx, id)
}
// ClientUnblockWithError mocks base method.
func (m *MockRedis) ClientUnblockWithError(ctx context.Context, id int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientUnblockWithError", ctx, id)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClientUnblockWithError indicates an expected call of ClientUnblockWithError.
func (mr *MockRedisMockRecorder) ClientUnblockWithError(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnblockWithError", reflect.TypeOf((*MockRedis)(nil).ClientUnblockWithError), ctx, id)
}
// ClientUnpause mocks base method.
func (m *MockRedis) ClientUnpause(ctx context.Context) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClientUnpause", ctx)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ClientUnpause indicates an expected call of ClientUnpause.
func (mr *MockRedisMockRecorder) ClientUnpause(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClientUnpause", reflect.TypeOf((*MockRedis)(nil).ClientUnpause), ctx)
}
// Close mocks base method.
func (m *MockRedis) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockRedisMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRedis)(nil).Close))
}
// ClusterAddSlots mocks base method.
func (m *MockRedis) ClusterAddSlots(ctx context.Context, slots ...int) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range slots {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ClusterAddSlots", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterAddSlots indicates an expected call of ClusterAddSlots.
func (mr *MockRedisMockRecorder) ClusterAddSlots(ctx any, slots ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, slots...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterAddSlots", reflect.TypeOf((*MockRedis)(nil).ClusterAddSlots), varargs...)
}
// ClusterAddSlotsRange mocks base method.
func (m *MockRedis) ClusterAddSlotsRange(ctx context.Context, min, max int) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterAddSlotsRange", ctx, min, max)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterAddSlotsRange indicates an expected call of ClusterAddSlotsRange.
func (mr *MockRedisMockRecorder) ClusterAddSlotsRange(ctx, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterAddSlotsRange", reflect.TypeOf((*MockRedis)(nil).ClusterAddSlotsRange), ctx, min, max)
}
// ClusterCountFailureReports mocks base method.
func (m *MockRedis) ClusterCountFailureReports(ctx context.Context, nodeID string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterCountFailureReports", ctx, nodeID)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClusterCountFailureReports indicates an expected call of ClusterCountFailureReports.
func (mr *MockRedisMockRecorder) ClusterCountFailureReports(ctx, nodeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterCountFailureReports", reflect.TypeOf((*MockRedis)(nil).ClusterCountFailureReports), ctx, nodeID)
}
// ClusterCountKeysInSlot mocks base method.
func (m *MockRedis) ClusterCountKeysInSlot(ctx context.Context, slot int) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterCountKeysInSlot", ctx, slot)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClusterCountKeysInSlot indicates an expected call of ClusterCountKeysInSlot.
func (mr *MockRedisMockRecorder) ClusterCountKeysInSlot(ctx, slot any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterCountKeysInSlot", reflect.TypeOf((*MockRedis)(nil).ClusterCountKeysInSlot), ctx, slot)
}
// ClusterDelSlots mocks base method.
func (m *MockRedis) ClusterDelSlots(ctx context.Context, slots ...int) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range slots {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ClusterDelSlots", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterDelSlots indicates an expected call of ClusterDelSlots.
func (mr *MockRedisMockRecorder) ClusterDelSlots(ctx any, slots ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, slots...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterDelSlots", reflect.TypeOf((*MockRedis)(nil).ClusterDelSlots), varargs...)
}
// ClusterDelSlotsRange mocks base method.
func (m *MockRedis) ClusterDelSlotsRange(ctx context.Context, min, max int) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterDelSlotsRange", ctx, min, max)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterDelSlotsRange indicates an expected call of ClusterDelSlotsRange.
func (mr *MockRedisMockRecorder) ClusterDelSlotsRange(ctx, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterDelSlotsRange", reflect.TypeOf((*MockRedis)(nil).ClusterDelSlotsRange), ctx, min, max)
}
// ClusterFailover mocks base method.
func (m *MockRedis) ClusterFailover(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterFailover", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterFailover indicates an expected call of ClusterFailover.
func (mr *MockRedisMockRecorder) ClusterFailover(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterFailover", reflect.TypeOf((*MockRedis)(nil).ClusterFailover), ctx)
}
// ClusterForget mocks base method.
func (m *MockRedis) ClusterForget(ctx context.Context, nodeID string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterForget", ctx, nodeID)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterForget indicates an expected call of ClusterForget.
func (mr *MockRedisMockRecorder) ClusterForget(ctx, nodeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterForget", reflect.TypeOf((*MockRedis)(nil).ClusterForget), ctx, nodeID)
}
// ClusterGetKeysInSlot mocks base method.
func (m *MockRedis) ClusterGetKeysInSlot(ctx context.Context, slot, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterGetKeysInSlot", ctx, slot, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ClusterGetKeysInSlot indicates an expected call of ClusterGetKeysInSlot.
func (mr *MockRedisMockRecorder) ClusterGetKeysInSlot(ctx, slot, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterGetKeysInSlot", reflect.TypeOf((*MockRedis)(nil).ClusterGetKeysInSlot), ctx, slot, count)
}
// ClusterInfo mocks base method.
func (m *MockRedis) ClusterInfo(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterInfo", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClusterInfo indicates an expected call of ClusterInfo.
func (mr *MockRedisMockRecorder) ClusterInfo(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterInfo", reflect.TypeOf((*MockRedis)(nil).ClusterInfo), ctx)
}
// ClusterKeySlot mocks base method.
func (m *MockRedis) ClusterKeySlot(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterKeySlot", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ClusterKeySlot indicates an expected call of ClusterKeySlot.
func (mr *MockRedisMockRecorder) ClusterKeySlot(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterKeySlot", reflect.TypeOf((*MockRedis)(nil).ClusterKeySlot), ctx, key)
}
// ClusterLinks mocks base method.
func (m *MockRedis) ClusterLinks(ctx context.Context) *redis.ClusterLinksCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterLinks", ctx)
ret0, _ := ret[0].(*redis.ClusterLinksCmd)
return ret0
}
// ClusterLinks indicates an expected call of ClusterLinks.
func (mr *MockRedisMockRecorder) ClusterLinks(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterLinks", reflect.TypeOf((*MockRedis)(nil).ClusterLinks), ctx)
}
// ClusterMeet mocks base method.
func (m *MockRedis) ClusterMeet(ctx context.Context, host, port string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterMeet", ctx, host, port)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterMeet indicates an expected call of ClusterMeet.
func (mr *MockRedisMockRecorder) ClusterMeet(ctx, host, port any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterMeet", reflect.TypeOf((*MockRedis)(nil).ClusterMeet), ctx, host, port)
}
// ClusterMyID mocks base method.
func (m *MockRedis) ClusterMyID(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterMyID", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClusterMyID indicates an expected call of ClusterMyID.
func (mr *MockRedisMockRecorder) ClusterMyID(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterMyID", reflect.TypeOf((*MockRedis)(nil).ClusterMyID), ctx)
}
// ClusterMyShardID mocks base method.
func (m *MockRedis) ClusterMyShardID(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterMyShardID", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClusterMyShardID indicates an expected call of ClusterMyShardID.
func (mr *MockRedisMockRecorder) ClusterMyShardID(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterMyShardID", reflect.TypeOf((*MockRedis)(nil).ClusterMyShardID), ctx)
}
// ClusterNodes mocks base method.
func (m *MockRedis) ClusterNodes(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterNodes", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ClusterNodes indicates an expected call of ClusterNodes.
func (mr *MockRedisMockRecorder) ClusterNodes(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterNodes", reflect.TypeOf((*MockRedis)(nil).ClusterNodes), ctx)
}
// ClusterReplicate mocks base method.
func (m *MockRedis) ClusterReplicate(ctx context.Context, nodeID string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterReplicate", ctx, nodeID)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterReplicate indicates an expected call of ClusterReplicate.
func (mr *MockRedisMockRecorder) ClusterReplicate(ctx, nodeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterReplicate", reflect.TypeOf((*MockRedis)(nil).ClusterReplicate), ctx, nodeID)
}
// ClusterResetHard mocks base method.
func (m *MockRedis) ClusterResetHard(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterResetHard", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterResetHard indicates an expected call of ClusterResetHard.
func (mr *MockRedisMockRecorder) ClusterResetHard(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterResetHard", reflect.TypeOf((*MockRedis)(nil).ClusterResetHard), ctx)
}
// ClusterResetSoft mocks base method.
func (m *MockRedis) ClusterResetSoft(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterResetSoft", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterResetSoft indicates an expected call of ClusterResetSoft.
func (mr *MockRedisMockRecorder) ClusterResetSoft(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterResetSoft", reflect.TypeOf((*MockRedis)(nil).ClusterResetSoft), ctx)
}
// ClusterSaveConfig mocks base method.
func (m *MockRedis) ClusterSaveConfig(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterSaveConfig", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ClusterSaveConfig indicates an expected call of ClusterSaveConfig.
func (mr *MockRedisMockRecorder) ClusterSaveConfig(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSaveConfig", reflect.TypeOf((*MockRedis)(nil).ClusterSaveConfig), ctx)
}
// ClusterShards mocks base method.
func (m *MockRedis) ClusterShards(ctx context.Context) *redis.ClusterShardsCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterShards", ctx)
ret0, _ := ret[0].(*redis.ClusterShardsCmd)
return ret0
}
// ClusterShards indicates an expected call of ClusterShards.
func (mr *MockRedisMockRecorder) ClusterShards(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterShards", reflect.TypeOf((*MockRedis)(nil).ClusterShards), ctx)
}
// ClusterSlaves mocks base method.
func (m *MockRedis) ClusterSlaves(ctx context.Context, nodeID string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterSlaves", ctx, nodeID)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ClusterSlaves indicates an expected call of ClusterSlaves.
func (mr *MockRedisMockRecorder) ClusterSlaves(ctx, nodeID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSlaves", reflect.TypeOf((*MockRedis)(nil).ClusterSlaves), ctx, nodeID)
}
// ClusterSlots mocks base method.
func (m *MockRedis) ClusterSlots(ctx context.Context) *redis.ClusterSlotsCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterSlots", ctx)
ret0, _ := ret[0].(*redis.ClusterSlotsCmd)
return ret0
}
// ClusterSlots indicates an expected call of ClusterSlots.
func (mr *MockRedisMockRecorder) ClusterSlots(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterSlots", reflect.TypeOf((*MockRedis)(nil).ClusterSlots), ctx)
}
// Command mocks base method.
func (m *MockRedis) Command(ctx context.Context) *redis.CommandsInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Command", ctx)
ret0, _ := ret[0].(*redis.CommandsInfoCmd)
return ret0
}
// Command indicates an expected call of Command.
func (mr *MockRedisMockRecorder) Command(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Command", reflect.TypeOf((*MockRedis)(nil).Command), ctx)
}
// CommandGetKeys mocks base method.
func (m *MockRedis) CommandGetKeys(ctx context.Context, commands ...any) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range commands {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CommandGetKeys", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// CommandGetKeys indicates an expected call of CommandGetKeys.
func (mr *MockRedisMockRecorder) CommandGetKeys(ctx any, commands ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, commands...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandGetKeys", reflect.TypeOf((*MockRedis)(nil).CommandGetKeys), varargs...)
}
// CommandGetKeysAndFlags mocks base method.
func (m *MockRedis) CommandGetKeysAndFlags(ctx context.Context, commands ...any) *redis.KeyFlagsCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range commands {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CommandGetKeysAndFlags", varargs...)
ret0, _ := ret[0].(*redis.KeyFlagsCmd)
return ret0
}
// CommandGetKeysAndFlags indicates an expected call of CommandGetKeysAndFlags.
func (mr *MockRedisMockRecorder) CommandGetKeysAndFlags(ctx any, commands ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, commands...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandGetKeysAndFlags", reflect.TypeOf((*MockRedis)(nil).CommandGetKeysAndFlags), varargs...)
}
// CommandList mocks base method.
func (m *MockRedis) CommandList(ctx context.Context, filter *redis.FilterBy) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CommandList", ctx, filter)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// CommandList indicates an expected call of CommandList.
func (mr *MockRedisMockRecorder) CommandList(ctx, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommandList", reflect.TypeOf((*MockRedis)(nil).CommandList), ctx, filter)
}
// ConfigGet mocks base method.
func (m *MockRedis) ConfigGet(ctx context.Context, parameter string) *redis.MapStringStringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigGet", ctx, parameter)
ret0, _ := ret[0].(*redis.MapStringStringCmd)
return ret0
}
// ConfigGet indicates an expected call of ConfigGet.
func (mr *MockRedisMockRecorder) ConfigGet(ctx, parameter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigGet", reflect.TypeOf((*MockRedis)(nil).ConfigGet), ctx, parameter)
}
// ConfigResetStat mocks base method.
func (m *MockRedis) ConfigResetStat(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigResetStat", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ConfigResetStat indicates an expected call of ConfigResetStat.
func (mr *MockRedisMockRecorder) ConfigResetStat(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigResetStat", reflect.TypeOf((*MockRedis)(nil).ConfigResetStat), ctx)
}
// ConfigRewrite mocks base method.
func (m *MockRedis) ConfigRewrite(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigRewrite", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ConfigRewrite indicates an expected call of ConfigRewrite.
func (mr *MockRedisMockRecorder) ConfigRewrite(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigRewrite", reflect.TypeOf((*MockRedis)(nil).ConfigRewrite), ctx)
}
// ConfigSet mocks base method.
func (m *MockRedis) ConfigSet(ctx context.Context, parameter, value string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConfigSet", ctx, parameter, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ConfigSet indicates an expected call of ConfigSet.
func (mr *MockRedisMockRecorder) ConfigSet(ctx, parameter, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConfigSet", reflect.TypeOf((*MockRedis)(nil).ConfigSet), ctx, parameter, value)
}
// Copy mocks base method.
func (m *MockRedis) Copy(ctx context.Context, sourceKey, destKey string, db int, replace bool) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Copy", ctx, sourceKey, destKey, db, replace)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Copy indicates an expected call of Copy.
func (mr *MockRedisMockRecorder) Copy(ctx, sourceKey, destKey, db, replace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Copy", reflect.TypeOf((*MockRedis)(nil).Copy), ctx, sourceKey, destKey, db, replace)
}
// DBSize mocks base method.
func (m *MockRedis) DBSize(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DBSize", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// DBSize indicates an expected call of DBSize.
func (mr *MockRedisMockRecorder) DBSize(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DBSize", reflect.TypeOf((*MockRedis)(nil).DBSize), ctx)
}
// DebugObject mocks base method.
func (m *MockRedis) DebugObject(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DebugObject", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// DebugObject indicates an expected call of DebugObject.
func (mr *MockRedisMockRecorder) DebugObject(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DebugObject", reflect.TypeOf((*MockRedis)(nil).DebugObject), ctx, key)
}
// Decr mocks base method.
func (m *MockRedis) Decr(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Decr", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Decr indicates an expected call of Decr.
func (mr *MockRedisMockRecorder) Decr(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decr", reflect.TypeOf((*MockRedis)(nil).Decr), ctx, key)
}
// DecrBy mocks base method.
func (m *MockRedis) DecrBy(ctx context.Context, key string, decrement int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DecrBy", ctx, key, decrement)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// DecrBy indicates an expected call of DecrBy.
func (mr *MockRedisMockRecorder) DecrBy(ctx, key, decrement any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DecrBy", reflect.TypeOf((*MockRedis)(nil).DecrBy), ctx, key, decrement)
}
// Del mocks base method.
func (m *MockRedis) Del(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Del", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Del indicates an expected call of Del.
func (mr *MockRedisMockRecorder) Del(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockRedis)(nil).Del), varargs...)
}
// DelExArgs mocks base method.
func (m *MockRedis) DelExArgs(ctx context.Context, key string, a redis.DelExArgs) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DelExArgs", ctx, key, a)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// DelExArgs indicates an expected call of DelExArgs.
func (mr *MockRedisMockRecorder) DelExArgs(ctx, key, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DelExArgs", reflect.TypeOf((*MockRedis)(nil).DelExArgs), ctx, key, a)
}
// Digest mocks base method.
func (m *MockRedis) Digest(ctx context.Context, key string) *redis.DigestCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Digest", ctx, key)
ret0, _ := ret[0].(*redis.DigestCmd)
return ret0
}
// Digest indicates an expected call of Digest.
func (mr *MockRedisMockRecorder) Digest(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Digest", reflect.TypeOf((*MockRedis)(nil).Digest), ctx, key)
}
// Dump mocks base method.
func (m *MockRedis) Dump(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dump", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Dump indicates an expected call of Dump.
func (mr *MockRedisMockRecorder) Dump(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dump", reflect.TypeOf((*MockRedis)(nil).Dump), ctx, key)
}
// Echo mocks base method.
func (m *MockRedis) Echo(ctx context.Context, message any) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Echo", ctx, message)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Echo indicates an expected call of Echo.
func (mr *MockRedisMockRecorder) Echo(ctx, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Echo", reflect.TypeOf((*MockRedis)(nil).Echo), ctx, message)
}
// Eval mocks base method.
func (m *MockRedis) Eval(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, script, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Eval", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// Eval indicates an expected call of Eval.
func (mr *MockRedisMockRecorder) Eval(ctx, script, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, script, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Eval", reflect.TypeOf((*MockRedis)(nil).Eval), varargs...)
}
// EvalRO mocks base method.
func (m *MockRedis) EvalRO(ctx context.Context, script string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, script, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "EvalRO", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// EvalRO indicates an expected call of EvalRO.
func (mr *MockRedisMockRecorder) EvalRO(ctx, script, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, script, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalRO", reflect.TypeOf((*MockRedis)(nil).EvalRO), varargs...)
}
// EvalSha mocks base method.
func (m *MockRedis) EvalSha(ctx context.Context, sha1 string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, sha1, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "EvalSha", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// EvalSha indicates an expected call of EvalSha.
func (mr *MockRedisMockRecorder) EvalSha(ctx, sha1, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, sha1, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalSha", reflect.TypeOf((*MockRedis)(nil).EvalSha), varargs...)
}
// EvalShaRO mocks base method.
func (m *MockRedis) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, sha1, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "EvalShaRO", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// EvalShaRO indicates an expected call of EvalShaRO.
func (mr *MockRedisMockRecorder) EvalShaRO(ctx, sha1, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, sha1, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EvalShaRO", reflect.TypeOf((*MockRedis)(nil).EvalShaRO), varargs...)
}
// Exists mocks base method.
func (m *MockRedis) Exists(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exists", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Exists indicates an expected call of Exists.
func (mr *MockRedisMockRecorder) Exists(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockRedis)(nil).Exists), varargs...)
}
// Expire mocks base method.
func (m *MockRedis) Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Expire", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// Expire indicates an expected call of Expire.
func (mr *MockRedisMockRecorder) Expire(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Expire", reflect.TypeOf((*MockRedis)(nil).Expire), ctx, key, expiration)
}
// ExpireAt mocks base method.
func (m *MockRedis) ExpireAt(ctx context.Context, key string, tm time.Time) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireAt", ctx, key, tm)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ExpireAt indicates an expected call of ExpireAt.
func (mr *MockRedisMockRecorder) ExpireAt(ctx, key, tm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireAt", reflect.TypeOf((*MockRedis)(nil).ExpireAt), ctx, key, tm)
}
// ExpireGT mocks base method.
func (m *MockRedis) ExpireGT(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireGT", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ExpireGT indicates an expected call of ExpireGT.
func (mr *MockRedisMockRecorder) ExpireGT(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireGT", reflect.TypeOf((*MockRedis)(nil).ExpireGT), ctx, key, expiration)
}
// ExpireLT mocks base method.
func (m *MockRedis) ExpireLT(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireLT", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ExpireLT indicates an expected call of ExpireLT.
func (mr *MockRedisMockRecorder) ExpireLT(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireLT", reflect.TypeOf((*MockRedis)(nil).ExpireLT), ctx, key, expiration)
}
// ExpireNX mocks base method.
func (m *MockRedis) ExpireNX(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireNX", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ExpireNX indicates an expected call of ExpireNX.
func (mr *MockRedisMockRecorder) ExpireNX(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireNX", reflect.TypeOf((*MockRedis)(nil).ExpireNX), ctx, key, expiration)
}
// ExpireTime mocks base method.
func (m *MockRedis) ExpireTime(ctx context.Context, key string) *redis.DurationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireTime", ctx, key)
ret0, _ := ret[0].(*redis.DurationCmd)
return ret0
}
// ExpireTime indicates an expected call of ExpireTime.
func (mr *MockRedisMockRecorder) ExpireTime(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireTime", reflect.TypeOf((*MockRedis)(nil).ExpireTime), ctx, key)
}
// ExpireXX mocks base method.
func (m *MockRedis) ExpireXX(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExpireXX", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// ExpireXX indicates an expected call of ExpireXX.
func (mr *MockRedisMockRecorder) ExpireXX(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExpireXX", reflect.TypeOf((*MockRedis)(nil).ExpireXX), ctx, key, expiration)
}
// FCall mocks base method.
func (m *MockRedis) FCall(ctx context.Context, function string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, function, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FCall", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// FCall indicates an expected call of FCall.
func (mr *MockRedisMockRecorder) FCall(ctx, function, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, function, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCall", reflect.TypeOf((*MockRedis)(nil).FCall), varargs...)
}
// FCallRO mocks base method.
func (m *MockRedis) FCallRO(ctx context.Context, function string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, function, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FCallRO", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// FCallRO indicates an expected call of FCallRO.
func (mr *MockRedisMockRecorder) FCallRO(ctx, function, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, function, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCallRO", reflect.TypeOf((*MockRedis)(nil).FCallRO), varargs...)
}
// FCallRo mocks base method.
func (m *MockRedis) FCallRo(ctx context.Context, function string, keys []string, args ...any) *redis.Cmd {
m.ctrl.T.Helper()
varargs := []any{ctx, function, keys}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FCallRo", varargs...)
ret0, _ := ret[0].(*redis.Cmd)
return ret0
}
// FCallRo indicates an expected call of FCallRo.
func (mr *MockRedisMockRecorder) FCallRo(ctx, function, keys any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, function, keys}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FCallRo", reflect.TypeOf((*MockRedis)(nil).FCallRo), varargs...)
}
// FTAggregate mocks base method.
func (m *MockRedis) FTAggregate(ctx context.Context, index, query string) *redis.MapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAggregate", ctx, index, query)
ret0, _ := ret[0].(*redis.MapStringInterfaceCmd)
return ret0
}
// FTAggregate indicates an expected call of FTAggregate.
func (mr *MockRedisMockRecorder) FTAggregate(ctx, index, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAggregate", reflect.TypeOf((*MockRedis)(nil).FTAggregate), ctx, index, query)
}
// FTAggregateWithArgs mocks base method.
func (m *MockRedis) FTAggregateWithArgs(ctx context.Context, index, query string, options *redis.FTAggregateOptions) *redis.AggregateCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAggregateWithArgs", ctx, index, query, options)
ret0, _ := ret[0].(*redis.AggregateCmd)
return ret0
}
// FTAggregateWithArgs indicates an expected call of FTAggregateWithArgs.
func (mr *MockRedisMockRecorder) FTAggregateWithArgs(ctx, index, query, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAggregateWithArgs", reflect.TypeOf((*MockRedis)(nil).FTAggregateWithArgs), ctx, index, query, options)
}
// FTAliasAdd mocks base method.
func (m *MockRedis) FTAliasAdd(ctx context.Context, index, alias string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAliasAdd", ctx, index, alias)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTAliasAdd indicates an expected call of FTAliasAdd.
func (mr *MockRedisMockRecorder) FTAliasAdd(ctx, index, alias any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAliasAdd", reflect.TypeOf((*MockRedis)(nil).FTAliasAdd), ctx, index, alias)
}
// FTAliasDel mocks base method.
func (m *MockRedis) FTAliasDel(ctx context.Context, alias string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAliasDel", ctx, alias)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTAliasDel indicates an expected call of FTAliasDel.
func (mr *MockRedisMockRecorder) FTAliasDel(ctx, alias any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAliasDel", reflect.TypeOf((*MockRedis)(nil).FTAliasDel), ctx, alias)
}
// FTAliasUpdate mocks base method.
func (m *MockRedis) FTAliasUpdate(ctx context.Context, index, alias string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAliasUpdate", ctx, index, alias)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTAliasUpdate indicates an expected call of FTAliasUpdate.
func (mr *MockRedisMockRecorder) FTAliasUpdate(ctx, index, alias any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAliasUpdate", reflect.TypeOf((*MockRedis)(nil).FTAliasUpdate), ctx, index, alias)
}
// FTAlter mocks base method.
func (m *MockRedis) FTAlter(ctx context.Context, index string, skipInitialScan bool, definition []any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTAlter", ctx, index, skipInitialScan, definition)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTAlter indicates an expected call of FTAlter.
func (mr *MockRedisMockRecorder) FTAlter(ctx, index, skipInitialScan, definition any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTAlter", reflect.TypeOf((*MockRedis)(nil).FTAlter), ctx, index, skipInitialScan, definition)
}
// FTConfigGet mocks base method.
func (m *MockRedis) FTConfigGet(ctx context.Context, option string) *redis.MapMapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTConfigGet", ctx, option)
ret0, _ := ret[0].(*redis.MapMapStringInterfaceCmd)
return ret0
}
// FTConfigGet indicates an expected call of FTConfigGet.
func (mr *MockRedisMockRecorder) FTConfigGet(ctx, option any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTConfigGet", reflect.TypeOf((*MockRedis)(nil).FTConfigGet), ctx, option)
}
// FTConfigSet mocks base method.
func (m *MockRedis) FTConfigSet(ctx context.Context, option string, value any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTConfigSet", ctx, option, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTConfigSet indicates an expected call of FTConfigSet.
func (mr *MockRedisMockRecorder) FTConfigSet(ctx, option, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTConfigSet", reflect.TypeOf((*MockRedis)(nil).FTConfigSet), ctx, option, value)
}
// FTCreate mocks base method.
func (m *MockRedis) FTCreate(ctx context.Context, index string, options *redis.FTCreateOptions, schema ...*redis.FieldSchema) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, index, options}
for _, a := range schema {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FTCreate", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTCreate indicates an expected call of FTCreate.
func (mr *MockRedisMockRecorder) FTCreate(ctx, index, options any, schema ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, index, options}, schema...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTCreate", reflect.TypeOf((*MockRedis)(nil).FTCreate), varargs...)
}
// FTCursorDel mocks base method.
func (m *MockRedis) FTCursorDel(ctx context.Context, index string, cursorId int) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTCursorDel", ctx, index, cursorId)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTCursorDel indicates an expected call of FTCursorDel.
func (mr *MockRedisMockRecorder) FTCursorDel(ctx, index, cursorId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTCursorDel", reflect.TypeOf((*MockRedis)(nil).FTCursorDel), ctx, index, cursorId)
}
// FTCursorRead mocks base method.
func (m *MockRedis) FTCursorRead(ctx context.Context, index string, cursorId, count int) *redis.MapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTCursorRead", ctx, index, cursorId, count)
ret0, _ := ret[0].(*redis.MapStringInterfaceCmd)
return ret0
}
// FTCursorRead indicates an expected call of FTCursorRead.
func (mr *MockRedisMockRecorder) FTCursorRead(ctx, index, cursorId, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTCursorRead", reflect.TypeOf((*MockRedis)(nil).FTCursorRead), ctx, index, cursorId, count)
}
// FTDictAdd mocks base method.
func (m *MockRedis) FTDictAdd(ctx context.Context, dict string, term ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, dict}
for _, a := range term {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FTDictAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// FTDictAdd indicates an expected call of FTDictAdd.
func (mr *MockRedisMockRecorder) FTDictAdd(ctx, dict any, term ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dict}, term...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTDictAdd", reflect.TypeOf((*MockRedis)(nil).FTDictAdd), varargs...)
}
// FTDictDel mocks base method.
func (m *MockRedis) FTDictDel(ctx context.Context, dict string, term ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, dict}
for _, a := range term {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FTDictDel", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// FTDictDel indicates an expected call of FTDictDel.
func (mr *MockRedisMockRecorder) FTDictDel(ctx, dict any, term ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dict}, term...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTDictDel", reflect.TypeOf((*MockRedis)(nil).FTDictDel), varargs...)
}
// FTDictDump mocks base method.
func (m *MockRedis) FTDictDump(ctx context.Context, dict string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTDictDump", ctx, dict)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// FTDictDump indicates an expected call of FTDictDump.
func (mr *MockRedisMockRecorder) FTDictDump(ctx, dict any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTDictDump", reflect.TypeOf((*MockRedis)(nil).FTDictDump), ctx, dict)
}
// FTDropIndex mocks base method.
func (m *MockRedis) FTDropIndex(ctx context.Context, index string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTDropIndex", ctx, index)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTDropIndex indicates an expected call of FTDropIndex.
func (mr *MockRedisMockRecorder) FTDropIndex(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTDropIndex", reflect.TypeOf((*MockRedis)(nil).FTDropIndex), ctx, index)
}
// FTDropIndexWithArgs mocks base method.
func (m *MockRedis) FTDropIndexWithArgs(ctx context.Context, index string, options *redis.FTDropIndexOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTDropIndexWithArgs", ctx, index, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTDropIndexWithArgs indicates an expected call of FTDropIndexWithArgs.
func (mr *MockRedisMockRecorder) FTDropIndexWithArgs(ctx, index, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTDropIndexWithArgs", reflect.TypeOf((*MockRedis)(nil).FTDropIndexWithArgs), ctx, index, options)
}
// FTExplain mocks base method.
func (m *MockRedis) FTExplain(ctx context.Context, index, query string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTExplain", ctx, index, query)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FTExplain indicates an expected call of FTExplain.
func (mr *MockRedisMockRecorder) FTExplain(ctx, index, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTExplain", reflect.TypeOf((*MockRedis)(nil).FTExplain), ctx, index, query)
}
// FTExplainWithArgs mocks base method.
func (m *MockRedis) FTExplainWithArgs(ctx context.Context, index, query string, options *redis.FTExplainOptions) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTExplainWithArgs", ctx, index, query, options)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FTExplainWithArgs indicates an expected call of FTExplainWithArgs.
func (mr *MockRedisMockRecorder) FTExplainWithArgs(ctx, index, query, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTExplainWithArgs", reflect.TypeOf((*MockRedis)(nil).FTExplainWithArgs), ctx, index, query, options)
}
// FTHybrid mocks base method.
func (m *MockRedis) FTHybrid(ctx context.Context, index, searchExpr, vectorField string, vectorData redis.Vector) *redis.FTHybridCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTHybrid", ctx, index, searchExpr, vectorField, vectorData)
ret0, _ := ret[0].(*redis.FTHybridCmd)
return ret0
}
// FTHybrid indicates an expected call of FTHybrid.
func (mr *MockRedisMockRecorder) FTHybrid(ctx, index, searchExpr, vectorField, vectorData any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTHybrid", reflect.TypeOf((*MockRedis)(nil).FTHybrid), ctx, index, searchExpr, vectorField, vectorData)
}
// FTHybridWithArgs mocks base method.
func (m *MockRedis) FTHybridWithArgs(ctx context.Context, index string, options *redis.FTHybridOptions) *redis.FTHybridCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTHybridWithArgs", ctx, index, options)
ret0, _ := ret[0].(*redis.FTHybridCmd)
return ret0
}
// FTHybridWithArgs indicates an expected call of FTHybridWithArgs.
func (mr *MockRedisMockRecorder) FTHybridWithArgs(ctx, index, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTHybridWithArgs", reflect.TypeOf((*MockRedis)(nil).FTHybridWithArgs), ctx, index, options)
}
// FTInfo mocks base method.
func (m *MockRedis) FTInfo(ctx context.Context, index string) *redis.FTInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTInfo", ctx, index)
ret0, _ := ret[0].(*redis.FTInfoCmd)
return ret0
}
// FTInfo indicates an expected call of FTInfo.
func (mr *MockRedisMockRecorder) FTInfo(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTInfo", reflect.TypeOf((*MockRedis)(nil).FTInfo), ctx, index)
}
// FTSearch mocks base method.
func (m *MockRedis) FTSearch(ctx context.Context, index, query string) *redis.FTSearchCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSearch", ctx, index, query)
ret0, _ := ret[0].(*redis.FTSearchCmd)
return ret0
}
// FTSearch indicates an expected call of FTSearch.
func (mr *MockRedisMockRecorder) FTSearch(ctx, index, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSearch", reflect.TypeOf((*MockRedis)(nil).FTSearch), ctx, index, query)
}
// FTSearchWithArgs mocks base method.
func (m *MockRedis) FTSearchWithArgs(ctx context.Context, index, query string, options *redis.FTSearchOptions) *redis.FTSearchCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSearchWithArgs", ctx, index, query, options)
ret0, _ := ret[0].(*redis.FTSearchCmd)
return ret0
}
// FTSearchWithArgs indicates an expected call of FTSearchWithArgs.
func (mr *MockRedisMockRecorder) FTSearchWithArgs(ctx, index, query, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSearchWithArgs", reflect.TypeOf((*MockRedis)(nil).FTSearchWithArgs), ctx, index, query, options)
}
// FTSpellCheck mocks base method.
func (m *MockRedis) FTSpellCheck(ctx context.Context, index, query string) *redis.FTSpellCheckCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSpellCheck", ctx, index, query)
ret0, _ := ret[0].(*redis.FTSpellCheckCmd)
return ret0
}
// FTSpellCheck indicates an expected call of FTSpellCheck.
func (mr *MockRedisMockRecorder) FTSpellCheck(ctx, index, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSpellCheck", reflect.TypeOf((*MockRedis)(nil).FTSpellCheck), ctx, index, query)
}
// FTSpellCheckWithArgs mocks base method.
func (m *MockRedis) FTSpellCheckWithArgs(ctx context.Context, index, query string, options *redis.FTSpellCheckOptions) *redis.FTSpellCheckCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSpellCheckWithArgs", ctx, index, query, options)
ret0, _ := ret[0].(*redis.FTSpellCheckCmd)
return ret0
}
// FTSpellCheckWithArgs indicates an expected call of FTSpellCheckWithArgs.
func (mr *MockRedisMockRecorder) FTSpellCheckWithArgs(ctx, index, query, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSpellCheckWithArgs", reflect.TypeOf((*MockRedis)(nil).FTSpellCheckWithArgs), ctx, index, query, options)
}
// FTSynDump mocks base method.
func (m *MockRedis) FTSynDump(ctx context.Context, index string) *redis.FTSynDumpCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSynDump", ctx, index)
ret0, _ := ret[0].(*redis.FTSynDumpCmd)
return ret0
}
// FTSynDump indicates an expected call of FTSynDump.
func (mr *MockRedisMockRecorder) FTSynDump(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSynDump", reflect.TypeOf((*MockRedis)(nil).FTSynDump), ctx, index)
}
// FTSynUpdate mocks base method.
func (m *MockRedis) FTSynUpdate(ctx context.Context, index string, synGroupId any, terms []any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSynUpdate", ctx, index, synGroupId, terms)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTSynUpdate indicates an expected call of FTSynUpdate.
func (mr *MockRedisMockRecorder) FTSynUpdate(ctx, index, synGroupId, terms any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSynUpdate", reflect.TypeOf((*MockRedis)(nil).FTSynUpdate), ctx, index, synGroupId, terms)
}
// FTSynUpdateWithArgs mocks base method.
func (m *MockRedis) FTSynUpdateWithArgs(ctx context.Context, index string, synGroupId any, options *redis.FTSynUpdateOptions, terms []any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTSynUpdateWithArgs", ctx, index, synGroupId, options, terms)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FTSynUpdateWithArgs indicates an expected call of FTSynUpdateWithArgs.
func (mr *MockRedisMockRecorder) FTSynUpdateWithArgs(ctx, index, synGroupId, options, terms any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTSynUpdateWithArgs", reflect.TypeOf((*MockRedis)(nil).FTSynUpdateWithArgs), ctx, index, synGroupId, options, terms)
}
// FTTagVals mocks base method.
func (m *MockRedis) FTTagVals(ctx context.Context, index, field string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FTTagVals", ctx, index, field)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// FTTagVals indicates an expected call of FTTagVals.
func (mr *MockRedisMockRecorder) FTTagVals(ctx, index, field any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FTTagVals", reflect.TypeOf((*MockRedis)(nil).FTTagVals), ctx, index, field)
}
// FT_List mocks base method.
func (m *MockRedis) FT_List(ctx context.Context) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FT_List", ctx)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// FT_List indicates an expected call of FT_List.
func (mr *MockRedisMockRecorder) FT_List(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FT_List", reflect.TypeOf((*MockRedis)(nil).FT_List), ctx)
}
// FlushAll mocks base method.
func (m *MockRedis) FlushAll(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FlushAll", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FlushAll indicates an expected call of FlushAll.
func (mr *MockRedisMockRecorder) FlushAll(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushAll", reflect.TypeOf((*MockRedis)(nil).FlushAll), ctx)
}
// FlushAllAsync mocks base method.
func (m *MockRedis) FlushAllAsync(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FlushAllAsync", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FlushAllAsync indicates an expected call of FlushAllAsync.
func (mr *MockRedisMockRecorder) FlushAllAsync(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushAllAsync", reflect.TypeOf((*MockRedis)(nil).FlushAllAsync), ctx)
}
// FlushDB mocks base method.
func (m *MockRedis) FlushDB(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FlushDB", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FlushDB indicates an expected call of FlushDB.
func (mr *MockRedisMockRecorder) FlushDB(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDB", reflect.TypeOf((*MockRedis)(nil).FlushDB), ctx)
}
// FlushDBAsync mocks base method.
func (m *MockRedis) FlushDBAsync(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FlushDBAsync", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// FlushDBAsync indicates an expected call of FlushDBAsync.
func (mr *MockRedisMockRecorder) FlushDBAsync(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FlushDBAsync", reflect.TypeOf((*MockRedis)(nil).FlushDBAsync), ctx)
}
// FunctionDelete mocks base method.
func (m *MockRedis) FunctionDelete(ctx context.Context, libName string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionDelete", ctx, libName)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionDelete indicates an expected call of FunctionDelete.
func (mr *MockRedisMockRecorder) FunctionDelete(ctx, libName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionDelete", reflect.TypeOf((*MockRedis)(nil).FunctionDelete), ctx, libName)
}
// FunctionDump mocks base method.
func (m *MockRedis) FunctionDump(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionDump", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionDump indicates an expected call of FunctionDump.
func (mr *MockRedisMockRecorder) FunctionDump(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionDump", reflect.TypeOf((*MockRedis)(nil).FunctionDump), ctx)
}
// FunctionFlush mocks base method.
func (m *MockRedis) FunctionFlush(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionFlush", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionFlush indicates an expected call of FunctionFlush.
func (mr *MockRedisMockRecorder) FunctionFlush(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionFlush", reflect.TypeOf((*MockRedis)(nil).FunctionFlush), ctx)
}
// FunctionFlushAsync mocks base method.
func (m *MockRedis) FunctionFlushAsync(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionFlushAsync", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionFlushAsync indicates an expected call of FunctionFlushAsync.
func (mr *MockRedisMockRecorder) FunctionFlushAsync(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionFlushAsync", reflect.TypeOf((*MockRedis)(nil).FunctionFlushAsync), ctx)
}
// FunctionKill mocks base method.
func (m *MockRedis) FunctionKill(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionKill", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionKill indicates an expected call of FunctionKill.
func (mr *MockRedisMockRecorder) FunctionKill(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionKill", reflect.TypeOf((*MockRedis)(nil).FunctionKill), ctx)
}
// FunctionList mocks base method.
func (m *MockRedis) FunctionList(ctx context.Context, q redis.FunctionListQuery) *redis.FunctionListCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionList", ctx, q)
ret0, _ := ret[0].(*redis.FunctionListCmd)
return ret0
}
// FunctionList indicates an expected call of FunctionList.
func (mr *MockRedisMockRecorder) FunctionList(ctx, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionList", reflect.TypeOf((*MockRedis)(nil).FunctionList), ctx, q)
}
// FunctionLoad mocks base method.
func (m *MockRedis) FunctionLoad(ctx context.Context, code string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionLoad", ctx, code)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionLoad indicates an expected call of FunctionLoad.
func (mr *MockRedisMockRecorder) FunctionLoad(ctx, code any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionLoad", reflect.TypeOf((*MockRedis)(nil).FunctionLoad), ctx, code)
}
// FunctionLoadReplace mocks base method.
func (m *MockRedis) FunctionLoadReplace(ctx context.Context, code string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionLoadReplace", ctx, code)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionLoadReplace indicates an expected call of FunctionLoadReplace.
func (mr *MockRedisMockRecorder) FunctionLoadReplace(ctx, code any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionLoadReplace", reflect.TypeOf((*MockRedis)(nil).FunctionLoadReplace), ctx, code)
}
// FunctionRestore mocks base method.
func (m *MockRedis) FunctionRestore(ctx context.Context, libDump string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionRestore", ctx, libDump)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// FunctionRestore indicates an expected call of FunctionRestore.
func (mr *MockRedisMockRecorder) FunctionRestore(ctx, libDump any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionRestore", reflect.TypeOf((*MockRedis)(nil).FunctionRestore), ctx, libDump)
}
// FunctionStats mocks base method.
func (m *MockRedis) FunctionStats(ctx context.Context) *redis.FunctionStatsCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FunctionStats", ctx)
ret0, _ := ret[0].(*redis.FunctionStatsCmd)
return ret0
}
// FunctionStats indicates an expected call of FunctionStats.
func (mr *MockRedisMockRecorder) FunctionStats(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FunctionStats", reflect.TypeOf((*MockRedis)(nil).FunctionStats), ctx)
}
// GeoAdd mocks base method.
func (m *MockRedis) GeoAdd(ctx context.Context, key string, geoLocation ...*redis.GeoLocation) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range geoLocation {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GeoAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// GeoAdd indicates an expected call of GeoAdd.
func (mr *MockRedisMockRecorder) GeoAdd(ctx, key any, geoLocation ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, geoLocation...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoAdd", reflect.TypeOf((*MockRedis)(nil).GeoAdd), varargs...)
}
// GeoDist mocks base method.
func (m *MockRedis) GeoDist(ctx context.Context, key, member1, member2, unit string) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoDist", ctx, key, member1, member2, unit)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// GeoDist indicates an expected call of GeoDist.
func (mr *MockRedisMockRecorder) GeoDist(ctx, key, member1, member2, unit any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoDist", reflect.TypeOf((*MockRedis)(nil).GeoDist), ctx, key, member1, member2, unit)
}
// GeoHash mocks base method.
func (m *MockRedis) GeoHash(ctx context.Context, key string, members ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GeoHash", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// GeoHash indicates an expected call of GeoHash.
func (mr *MockRedisMockRecorder) GeoHash(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoHash", reflect.TypeOf((*MockRedis)(nil).GeoHash), varargs...)
}
// GeoPos mocks base method.
func (m *MockRedis) GeoPos(ctx context.Context, key string, members ...string) *redis.GeoPosCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GeoPos", varargs...)
ret0, _ := ret[0].(*redis.GeoPosCmd)
return ret0
}
// GeoPos indicates an expected call of GeoPos.
func (mr *MockRedisMockRecorder) GeoPos(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoPos", reflect.TypeOf((*MockRedis)(nil).GeoPos), varargs...)
}
// GeoRadius mocks base method.
func (m *MockRedis) GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoRadius", ctx, key, longitude, latitude, query)
ret0, _ := ret[0].(*redis.GeoLocationCmd)
return ret0
}
// GeoRadius indicates an expected call of GeoRadius.
func (mr *MockRedisMockRecorder) GeoRadius(ctx, key, longitude, latitude, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadius", reflect.TypeOf((*MockRedis)(nil).GeoRadius), ctx, key, longitude, latitude, query)
}
// GeoRadiusByMember mocks base method.
func (m *MockRedis) GeoRadiusByMember(ctx context.Context, key, member string, query *redis.GeoRadiusQuery) *redis.GeoLocationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoRadiusByMember", ctx, key, member, query)
ret0, _ := ret[0].(*redis.GeoLocationCmd)
return ret0
}
// GeoRadiusByMember indicates an expected call of GeoRadiusByMember.
func (mr *MockRedisMockRecorder) GeoRadiusByMember(ctx, key, member, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusByMember", reflect.TypeOf((*MockRedis)(nil).GeoRadiusByMember), ctx, key, member, query)
}
// GeoRadiusByMemberStore mocks base method.
func (m *MockRedis) GeoRadiusByMemberStore(ctx context.Context, key, member string, query *redis.GeoRadiusQuery) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoRadiusByMemberStore", ctx, key, member, query)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// GeoRadiusByMemberStore indicates an expected call of GeoRadiusByMemberStore.
func (mr *MockRedisMockRecorder) GeoRadiusByMemberStore(ctx, key, member, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusByMemberStore", reflect.TypeOf((*MockRedis)(nil).GeoRadiusByMemberStore), ctx, key, member, query)
}
// GeoRadiusStore mocks base method.
func (m *MockRedis) GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *redis.GeoRadiusQuery) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoRadiusStore", ctx, key, longitude, latitude, query)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// GeoRadiusStore indicates an expected call of GeoRadiusStore.
func (mr *MockRedisMockRecorder) GeoRadiusStore(ctx, key, longitude, latitude, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoRadiusStore", reflect.TypeOf((*MockRedis)(nil).GeoRadiusStore), ctx, key, longitude, latitude, query)
}
// GeoSearch mocks base method.
func (m *MockRedis) GeoSearch(ctx context.Context, key string, q *redis.GeoSearchQuery) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoSearch", ctx, key, q)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// GeoSearch indicates an expected call of GeoSearch.
func (mr *MockRedisMockRecorder) GeoSearch(ctx, key, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearch", reflect.TypeOf((*MockRedis)(nil).GeoSearch), ctx, key, q)
}
// GeoSearchLocation mocks base method.
func (m *MockRedis) GeoSearchLocation(ctx context.Context, key string, q *redis.GeoSearchLocationQuery) *redis.GeoSearchLocationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoSearchLocation", ctx, key, q)
ret0, _ := ret[0].(*redis.GeoSearchLocationCmd)
return ret0
}
// GeoSearchLocation indicates an expected call of GeoSearchLocation.
func (mr *MockRedisMockRecorder) GeoSearchLocation(ctx, key, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearchLocation", reflect.TypeOf((*MockRedis)(nil).GeoSearchLocation), ctx, key, q)
}
// GeoSearchStore mocks base method.
func (m *MockRedis) GeoSearchStore(ctx context.Context, key, store string, q *redis.GeoSearchStoreQuery) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GeoSearchStore", ctx, key, store, q)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// GeoSearchStore indicates an expected call of GeoSearchStore.
func (mr *MockRedisMockRecorder) GeoSearchStore(ctx, key, store, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GeoSearchStore", reflect.TypeOf((*MockRedis)(nil).GeoSearchStore), ctx, key, store, q)
}
// Get mocks base method.
func (m *MockRedis) Get(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockRedisMockRecorder) Get(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRedis)(nil).Get), ctx, key)
}
// GetBit mocks base method.
func (m *MockRedis) GetBit(ctx context.Context, key string, offset int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBit", ctx, key, offset)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// GetBit indicates an expected call of GetBit.
func (mr *MockRedisMockRecorder) GetBit(ctx, key, offset any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBit", reflect.TypeOf((*MockRedis)(nil).GetBit), ctx, key, offset)
}
// GetDel mocks base method.
func (m *MockRedis) GetDel(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDel", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// GetDel indicates an expected call of GetDel.
func (mr *MockRedisMockRecorder) GetDel(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDel", reflect.TypeOf((*MockRedis)(nil).GetDel), ctx, key)
}
// GetEx mocks base method.
func (m *MockRedis) GetEx(ctx context.Context, key string, expiration time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEx", ctx, key, expiration)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// GetEx indicates an expected call of GetEx.
func (mr *MockRedisMockRecorder) GetEx(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEx", reflect.TypeOf((*MockRedis)(nil).GetEx), ctx, key, expiration)
}
// GetRange mocks base method.
func (m *MockRedis) GetRange(ctx context.Context, key string, start, end int64) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRange", ctx, key, start, end)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// GetRange indicates an expected call of GetRange.
func (mr *MockRedisMockRecorder) GetRange(ctx, key, start, end any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRange", reflect.TypeOf((*MockRedis)(nil).GetRange), ctx, key, start, end)
}
// GetSet mocks base method.
func (m *MockRedis) GetSet(ctx context.Context, key string, value any) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSet", ctx, key, value)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// GetSet indicates an expected call of GetSet.
func (mr *MockRedisMockRecorder) GetSet(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSet", reflect.TypeOf((*MockRedis)(nil).GetSet), ctx, key, value)
}
// HDel mocks base method.
func (m *MockRedis) HDel(ctx context.Context, key string, fields ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HDel", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HDel indicates an expected call of HDel.
func (mr *MockRedisMockRecorder) HDel(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HDel", reflect.TypeOf((*MockRedis)(nil).HDel), varargs...)
}
// HExists mocks base method.
func (m *MockRedis) HExists(ctx context.Context, key, field string) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HExists", ctx, key, field)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// HExists indicates an expected call of HExists.
func (mr *MockRedisMockRecorder) HExists(ctx, key, field any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExists", reflect.TypeOf((*MockRedis)(nil).HExists), ctx, key, field)
}
// HExpire mocks base method.
func (m *MockRedis) HExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, expiration}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HExpire", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HExpire indicates an expected call of HExpire.
func (mr *MockRedisMockRecorder) HExpire(ctx, key, expiration any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, expiration}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExpire", reflect.TypeOf((*MockRedis)(nil).HExpire), varargs...)
}
// HExpireAt mocks base method.
func (m *MockRedis) HExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, tm}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HExpireAt", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HExpireAt indicates an expected call of HExpireAt.
func (mr *MockRedisMockRecorder) HExpireAt(ctx, key, tm any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, tm}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExpireAt", reflect.TypeOf((*MockRedis)(nil).HExpireAt), varargs...)
}
// HExpireAtWithArgs mocks base method.
func (m *MockRedis) HExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs redis.HExpireArgs, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, tm, expirationArgs}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HExpireAtWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HExpireAtWithArgs indicates an expected call of HExpireAtWithArgs.
func (mr *MockRedisMockRecorder) HExpireAtWithArgs(ctx, key, tm, expirationArgs any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, tm, expirationArgs}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExpireAtWithArgs", reflect.TypeOf((*MockRedis)(nil).HExpireAtWithArgs), varargs...)
}
// HExpireTime mocks base method.
func (m *MockRedis) HExpireTime(ctx context.Context, key string, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HExpireTime", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HExpireTime indicates an expected call of HExpireTime.
func (mr *MockRedisMockRecorder) HExpireTime(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExpireTime", reflect.TypeOf((*MockRedis)(nil).HExpireTime), varargs...)
}
// HExpireWithArgs mocks base method.
func (m *MockRedis) HExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs redis.HExpireArgs, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, expiration, expirationArgs}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HExpireWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HExpireWithArgs indicates an expected call of HExpireWithArgs.
func (mr *MockRedisMockRecorder) HExpireWithArgs(ctx, key, expiration, expirationArgs any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, expiration, expirationArgs}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HExpireWithArgs", reflect.TypeOf((*MockRedis)(nil).HExpireWithArgs), varargs...)
}
// HGet mocks base method.
func (m *MockRedis) HGet(ctx context.Context, key, field string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HGet", ctx, key, field)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// HGet indicates an expected call of HGet.
func (mr *MockRedisMockRecorder) HGet(ctx, key, field any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGet", reflect.TypeOf((*MockRedis)(nil).HGet), ctx, key, field)
}
// HGetAll mocks base method.
func (m *MockRedis) HGetAll(ctx context.Context, key string) *redis.MapStringStringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HGetAll", ctx, key)
ret0, _ := ret[0].(*redis.MapStringStringCmd)
return ret0
}
// HGetAll indicates an expected call of HGetAll.
func (mr *MockRedisMockRecorder) HGetAll(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGetAll", reflect.TypeOf((*MockRedis)(nil).HGetAll), ctx, key)
}
// HGetDel mocks base method.
func (m *MockRedis) HGetDel(ctx context.Context, key string, fields ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HGetDel", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HGetDel indicates an expected call of HGetDel.
func (mr *MockRedisMockRecorder) HGetDel(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGetDel", reflect.TypeOf((*MockRedis)(nil).HGetDel), varargs...)
}
// HGetEX mocks base method.
func (m *MockRedis) HGetEX(ctx context.Context, key string, fields ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HGetEX", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HGetEX indicates an expected call of HGetEX.
func (mr *MockRedisMockRecorder) HGetEX(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGetEX", reflect.TypeOf((*MockRedis)(nil).HGetEX), varargs...)
}
// HGetEXWithArgs mocks base method.
func (m *MockRedis) HGetEXWithArgs(ctx context.Context, key string, options *redis.HGetEXOptions, fields ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HGetEXWithArgs", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HGetEXWithArgs indicates an expected call of HGetEXWithArgs.
func (mr *MockRedisMockRecorder) HGetEXWithArgs(ctx, key, options any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HGetEXWithArgs", reflect.TypeOf((*MockRedis)(nil).HGetEXWithArgs), varargs...)
}
// HIncrBy mocks base method.
func (m *MockRedis) HIncrBy(ctx context.Context, key, field string, incr int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HIncrBy", ctx, key, field, incr)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HIncrBy indicates an expected call of HIncrBy.
func (mr *MockRedisMockRecorder) HIncrBy(ctx, key, field, incr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HIncrBy", reflect.TypeOf((*MockRedis)(nil).HIncrBy), ctx, key, field, incr)
}
// HIncrByFloat mocks base method.
func (m *MockRedis) HIncrByFloat(ctx context.Context, key, field string, incr float64) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HIncrByFloat", ctx, key, field, incr)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// HIncrByFloat indicates an expected call of HIncrByFloat.
func (mr *MockRedisMockRecorder) HIncrByFloat(ctx, key, field, incr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HIncrByFloat", reflect.TypeOf((*MockRedis)(nil).HIncrByFloat), ctx, key, field, incr)
}
// HKeys mocks base method.
func (m *MockRedis) HKeys(ctx context.Context, key string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HKeys", ctx, key)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HKeys indicates an expected call of HKeys.
func (mr *MockRedisMockRecorder) HKeys(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HKeys", reflect.TypeOf((*MockRedis)(nil).HKeys), ctx, key)
}
// HLen mocks base method.
func (m *MockRedis) HLen(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HLen", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HLen indicates an expected call of HLen.
func (mr *MockRedisMockRecorder) HLen(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HLen", reflect.TypeOf((*MockRedis)(nil).HLen), ctx, key)
}
// HMGet mocks base method.
func (m *MockRedis) HMGet(ctx context.Context, key string, fields ...string) *redis.SliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HMGet", varargs...)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// HMGet indicates an expected call of HMGet.
func (mr *MockRedisMockRecorder) HMGet(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HMGet", reflect.TypeOf((*MockRedis)(nil).HMGet), varargs...)
}
// HMSet mocks base method.
func (m *MockRedis) HMSet(ctx context.Context, key string, values ...any) *redis.BoolCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HMSet", varargs...)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// HMSet indicates an expected call of HMSet.
func (mr *MockRedisMockRecorder) HMSet(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HMSet", reflect.TypeOf((*MockRedis)(nil).HMSet), varargs...)
}
// HPExpire mocks base method.
func (m *MockRedis) HPExpire(ctx context.Context, key string, expiration time.Duration, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, expiration}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPExpire", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPExpire indicates an expected call of HPExpire.
func (mr *MockRedisMockRecorder) HPExpire(ctx, key, expiration any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, expiration}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPExpire", reflect.TypeOf((*MockRedis)(nil).HPExpire), varargs...)
}
// HPExpireAt mocks base method.
func (m *MockRedis) HPExpireAt(ctx context.Context, key string, tm time.Time, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, tm}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPExpireAt", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPExpireAt indicates an expected call of HPExpireAt.
func (mr *MockRedisMockRecorder) HPExpireAt(ctx, key, tm any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, tm}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPExpireAt", reflect.TypeOf((*MockRedis)(nil).HPExpireAt), varargs...)
}
// HPExpireAtWithArgs mocks base method.
func (m *MockRedis) HPExpireAtWithArgs(ctx context.Context, key string, tm time.Time, expirationArgs redis.HExpireArgs, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, tm, expirationArgs}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPExpireAtWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPExpireAtWithArgs indicates an expected call of HPExpireAtWithArgs.
func (mr *MockRedisMockRecorder) HPExpireAtWithArgs(ctx, key, tm, expirationArgs any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, tm, expirationArgs}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPExpireAtWithArgs", reflect.TypeOf((*MockRedis)(nil).HPExpireAtWithArgs), varargs...)
}
// HPExpireTime mocks base method.
func (m *MockRedis) HPExpireTime(ctx context.Context, key string, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPExpireTime", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPExpireTime indicates an expected call of HPExpireTime.
func (mr *MockRedisMockRecorder) HPExpireTime(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPExpireTime", reflect.TypeOf((*MockRedis)(nil).HPExpireTime), varargs...)
}
// HPExpireWithArgs mocks base method.
func (m *MockRedis) HPExpireWithArgs(ctx context.Context, key string, expiration time.Duration, expirationArgs redis.HExpireArgs, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, expiration, expirationArgs}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPExpireWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPExpireWithArgs indicates an expected call of HPExpireWithArgs.
func (mr *MockRedisMockRecorder) HPExpireWithArgs(ctx, key, expiration, expirationArgs any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, expiration, expirationArgs}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPExpireWithArgs", reflect.TypeOf((*MockRedis)(nil).HPExpireWithArgs), varargs...)
}
// HPTTL mocks base method.
func (m *MockRedis) HPTTL(ctx context.Context, key string, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPTTL", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPTTL indicates an expected call of HPTTL.
func (mr *MockRedisMockRecorder) HPTTL(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPTTL", reflect.TypeOf((*MockRedis)(nil).HPTTL), varargs...)
}
// HPersist mocks base method.
func (m *MockRedis) HPersist(ctx context.Context, key string, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HPersist", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HPersist indicates an expected call of HPersist.
func (mr *MockRedisMockRecorder) HPersist(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HPersist", reflect.TypeOf((*MockRedis)(nil).HPersist), varargs...)
}
// HRandField mocks base method.
func (m *MockRedis) HRandField(ctx context.Context, key string, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HRandField", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HRandField indicates an expected call of HRandField.
func (mr *MockRedisMockRecorder) HRandField(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HRandField", reflect.TypeOf((*MockRedis)(nil).HRandField), ctx, key, count)
}
// HRandFieldWithValues mocks base method.
func (m *MockRedis) HRandFieldWithValues(ctx context.Context, key string, count int) *redis.KeyValueSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HRandFieldWithValues", ctx, key, count)
ret0, _ := ret[0].(*redis.KeyValueSliceCmd)
return ret0
}
// HRandFieldWithValues indicates an expected call of HRandFieldWithValues.
func (mr *MockRedisMockRecorder) HRandFieldWithValues(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HRandFieldWithValues", reflect.TypeOf((*MockRedis)(nil).HRandFieldWithValues), ctx, key, count)
}
// HScan mocks base method.
func (m *MockRedis) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HScan", ctx, key, cursor, match, count)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// HScan indicates an expected call of HScan.
func (mr *MockRedisMockRecorder) HScan(ctx, key, cursor, match, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HScan", reflect.TypeOf((*MockRedis)(nil).HScan), ctx, key, cursor, match, count)
}
// HScanNoValues mocks base method.
func (m *MockRedis) HScanNoValues(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HScanNoValues", ctx, key, cursor, match, count)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// HScanNoValues indicates an expected call of HScanNoValues.
func (mr *MockRedisMockRecorder) HScanNoValues(ctx, key, cursor, match, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HScanNoValues", reflect.TypeOf((*MockRedis)(nil).HScanNoValues), ctx, key, cursor, match, count)
}
// HSet mocks base method.
func (m *MockRedis) HSet(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HSet", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HSet indicates an expected call of HSet.
func (mr *MockRedisMockRecorder) HSet(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSet", reflect.TypeOf((*MockRedis)(nil).HSet), varargs...)
}
// HSetEX mocks base method.
func (m *MockRedis) HSetEX(ctx context.Context, key string, fieldsAndValues ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fieldsAndValues {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HSetEX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HSetEX indicates an expected call of HSetEX.
func (mr *MockRedisMockRecorder) HSetEX(ctx, key any, fieldsAndValues ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fieldsAndValues...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSetEX", reflect.TypeOf((*MockRedis)(nil).HSetEX), varargs...)
}
// HSetEXWithArgs mocks base method.
func (m *MockRedis) HSetEXWithArgs(ctx context.Context, key string, options *redis.HSetEXOptions, fieldsAndValues ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range fieldsAndValues {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HSetEXWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HSetEXWithArgs indicates an expected call of HSetEXWithArgs.
func (mr *MockRedisMockRecorder) HSetEXWithArgs(ctx, key, options any, fieldsAndValues ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, fieldsAndValues...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSetEXWithArgs", reflect.TypeOf((*MockRedis)(nil).HSetEXWithArgs), varargs...)
}
// HSetNX mocks base method.
func (m *MockRedis) HSetNX(ctx context.Context, key, field string, value any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HSetNX", ctx, key, field, value)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// HSetNX indicates an expected call of HSetNX.
func (mr *MockRedisMockRecorder) HSetNX(ctx, key, field, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HSetNX", reflect.TypeOf((*MockRedis)(nil).HSetNX), ctx, key, field, value)
}
// HStrLen mocks base method.
func (m *MockRedis) HStrLen(ctx context.Context, key, field string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HStrLen", ctx, key, field)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// HStrLen indicates an expected call of HStrLen.
func (mr *MockRedisMockRecorder) HStrLen(ctx, key, field any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HStrLen", reflect.TypeOf((*MockRedis)(nil).HStrLen), ctx, key, field)
}
// HTTL mocks base method.
func (m *MockRedis) HTTL(ctx context.Context, key string, fields ...string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range fields {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "HTTL", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// HTTL indicates an expected call of HTTL.
func (mr *MockRedisMockRecorder) HTTL(ctx, key any, fields ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, fields...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HTTL", reflect.TypeOf((*MockRedis)(nil).HTTL), varargs...)
}
// HVals mocks base method.
func (m *MockRedis) HVals(ctx context.Context, key string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HVals", ctx, key)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// HVals indicates an expected call of HVals.
func (mr *MockRedisMockRecorder) HVals(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HVals", reflect.TypeOf((*MockRedis)(nil).HVals), ctx, key)
}
// HealthCheck mocks base method.
func (m *MockRedis) HealthCheck() datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck")
ret0, _ := ret[0].(datasource.Health)
return ret0
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockRedisMockRecorder) HealthCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockRedis)(nil).HealthCheck))
}
// Incr mocks base method.
func (m *MockRedis) Incr(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Incr", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Incr indicates an expected call of Incr.
func (mr *MockRedisMockRecorder) Incr(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Incr", reflect.TypeOf((*MockRedis)(nil).Incr), ctx, key)
}
// IncrBy mocks base method.
func (m *MockRedis) IncrBy(ctx context.Context, key string, value int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IncrBy", ctx, key, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// IncrBy indicates an expected call of IncrBy.
func (mr *MockRedisMockRecorder) IncrBy(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrBy", reflect.TypeOf((*MockRedis)(nil).IncrBy), ctx, key, value)
}
// IncrByFloat mocks base method.
func (m *MockRedis) IncrByFloat(ctx context.Context, key string, value float64) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IncrByFloat", ctx, key, value)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// IncrByFloat indicates an expected call of IncrByFloat.
func (mr *MockRedisMockRecorder) IncrByFloat(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrByFloat", reflect.TypeOf((*MockRedis)(nil).IncrByFloat), ctx, key, value)
}
// Info mocks base method.
func (m *MockRedis) Info(ctx context.Context, section ...string) *redis.StringCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range section {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Info", varargs...)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Info indicates an expected call of Info.
func (mr *MockRedisMockRecorder) Info(ctx any, section ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, section...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockRedis)(nil).Info), varargs...)
}
// JSONArrAppend mocks base method.
func (m *MockRedis) JSONArrAppend(ctx context.Context, key, path string, values ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, path}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONArrAppend", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrAppend indicates an expected call of JSONArrAppend.
func (mr *MockRedisMockRecorder) JSONArrAppend(ctx, key, path any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, path}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrAppend", reflect.TypeOf((*MockRedis)(nil).JSONArrAppend), varargs...)
}
// JSONArrIndex mocks base method.
func (m *MockRedis) JSONArrIndex(ctx context.Context, key, path string, value ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, path}
for _, a := range value {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONArrIndex", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrIndex indicates an expected call of JSONArrIndex.
func (mr *MockRedisMockRecorder) JSONArrIndex(ctx, key, path any, value ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, path}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrIndex", reflect.TypeOf((*MockRedis)(nil).JSONArrIndex), varargs...)
}
// JSONArrIndexWithArgs mocks base method.
func (m *MockRedis) JSONArrIndexWithArgs(ctx context.Context, key, path string, options *redis.JSONArrIndexArgs, value ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, path, options}
for _, a := range value {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONArrIndexWithArgs", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrIndexWithArgs indicates an expected call of JSONArrIndexWithArgs.
func (mr *MockRedisMockRecorder) JSONArrIndexWithArgs(ctx, key, path, options any, value ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, path, options}, value...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrIndexWithArgs", reflect.TypeOf((*MockRedis)(nil).JSONArrIndexWithArgs), varargs...)
}
// JSONArrInsert mocks base method.
func (m *MockRedis) JSONArrInsert(ctx context.Context, key, path string, index int64, values ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, path, index}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONArrInsert", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrInsert indicates an expected call of JSONArrInsert.
func (mr *MockRedisMockRecorder) JSONArrInsert(ctx, key, path, index any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, path, index}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrInsert", reflect.TypeOf((*MockRedis)(nil).JSONArrInsert), varargs...)
}
// JSONArrLen mocks base method.
func (m *MockRedis) JSONArrLen(ctx context.Context, key, path string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONArrLen", ctx, key, path)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrLen indicates an expected call of JSONArrLen.
func (mr *MockRedisMockRecorder) JSONArrLen(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrLen", reflect.TypeOf((*MockRedis)(nil).JSONArrLen), ctx, key, path)
}
// JSONArrPop mocks base method.
func (m *MockRedis) JSONArrPop(ctx context.Context, key, path string, index int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONArrPop", ctx, key, path, index)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// JSONArrPop indicates an expected call of JSONArrPop.
func (mr *MockRedisMockRecorder) JSONArrPop(ctx, key, path, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrPop", reflect.TypeOf((*MockRedis)(nil).JSONArrPop), ctx, key, path, index)
}
// JSONArrTrim mocks base method.
func (m *MockRedis) JSONArrTrim(ctx context.Context, key, path string) *redis.IntSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONArrTrim", ctx, key, path)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrTrim indicates an expected call of JSONArrTrim.
func (mr *MockRedisMockRecorder) JSONArrTrim(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrTrim", reflect.TypeOf((*MockRedis)(nil).JSONArrTrim), ctx, key, path)
}
// JSONArrTrimWithArgs mocks base method.
func (m *MockRedis) JSONArrTrimWithArgs(ctx context.Context, key, path string, options *redis.JSONArrTrimArgs) *redis.IntSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONArrTrimWithArgs", ctx, key, path, options)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// JSONArrTrimWithArgs indicates an expected call of JSONArrTrimWithArgs.
func (mr *MockRedisMockRecorder) JSONArrTrimWithArgs(ctx, key, path, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONArrTrimWithArgs", reflect.TypeOf((*MockRedis)(nil).JSONArrTrimWithArgs), ctx, key, path, options)
}
// JSONClear mocks base method.
func (m *MockRedis) JSONClear(ctx context.Context, key, path string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONClear", ctx, key, path)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// JSONClear indicates an expected call of JSONClear.
func (mr *MockRedisMockRecorder) JSONClear(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONClear", reflect.TypeOf((*MockRedis)(nil).JSONClear), ctx, key, path)
}
// JSONDebugMemory mocks base method.
func (m *MockRedis) JSONDebugMemory(ctx context.Context, key, path string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONDebugMemory", ctx, key, path)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// JSONDebugMemory indicates an expected call of JSONDebugMemory.
func (mr *MockRedisMockRecorder) JSONDebugMemory(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONDebugMemory", reflect.TypeOf((*MockRedis)(nil).JSONDebugMemory), ctx, key, path)
}
// JSONDel mocks base method.
func (m *MockRedis) JSONDel(ctx context.Context, key, path string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONDel", ctx, key, path)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// JSONDel indicates an expected call of JSONDel.
func (mr *MockRedisMockRecorder) JSONDel(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONDel", reflect.TypeOf((*MockRedis)(nil).JSONDel), ctx, key, path)
}
// JSONForget mocks base method.
func (m *MockRedis) JSONForget(ctx context.Context, key, path string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONForget", ctx, key, path)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// JSONForget indicates an expected call of JSONForget.
func (mr *MockRedisMockRecorder) JSONForget(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONForget", reflect.TypeOf((*MockRedis)(nil).JSONForget), ctx, key, path)
}
// JSONGet mocks base method.
func (m *MockRedis) JSONGet(ctx context.Context, key string, paths ...string) *redis.JSONCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range paths {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONGet", varargs...)
ret0, _ := ret[0].(*redis.JSONCmd)
return ret0
}
// JSONGet indicates an expected call of JSONGet.
func (mr *MockRedisMockRecorder) JSONGet(ctx, key any, paths ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, paths...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONGet", reflect.TypeOf((*MockRedis)(nil).JSONGet), varargs...)
}
// JSONGetWithArgs mocks base method.
func (m *MockRedis) JSONGetWithArgs(ctx context.Context, key string, options *redis.JSONGetArgs, paths ...string) *redis.JSONCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key, options}
for _, a := range paths {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONGetWithArgs", varargs...)
ret0, _ := ret[0].(*redis.JSONCmd)
return ret0
}
// JSONGetWithArgs indicates an expected call of JSONGetWithArgs.
func (mr *MockRedisMockRecorder) JSONGetWithArgs(ctx, key, options any, paths ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key, options}, paths...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONGetWithArgs", reflect.TypeOf((*MockRedis)(nil).JSONGetWithArgs), varargs...)
}
// JSONMGet mocks base method.
func (m *MockRedis) JSONMGet(ctx context.Context, path string, keys ...string) *redis.JSONSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, path}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONMGet", varargs...)
ret0, _ := ret[0].(*redis.JSONSliceCmd)
return ret0
}
// JSONMGet indicates an expected call of JSONMGet.
func (mr *MockRedisMockRecorder) JSONMGet(ctx, path any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, path}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONMGet", reflect.TypeOf((*MockRedis)(nil).JSONMGet), varargs...)
}
// JSONMSet mocks base method.
func (m *MockRedis) JSONMSet(ctx context.Context, params ...any) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range params {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "JSONMSet", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// JSONMSet indicates an expected call of JSONMSet.
func (mr *MockRedisMockRecorder) JSONMSet(ctx any, params ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, params...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONMSet", reflect.TypeOf((*MockRedis)(nil).JSONMSet), varargs...)
}
// JSONMSetArgs mocks base method.
func (m *MockRedis) JSONMSetArgs(ctx context.Context, docs []redis.JSONSetArgs) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONMSetArgs", ctx, docs)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// JSONMSetArgs indicates an expected call of JSONMSetArgs.
func (mr *MockRedisMockRecorder) JSONMSetArgs(ctx, docs any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONMSetArgs", reflect.TypeOf((*MockRedis)(nil).JSONMSetArgs), ctx, docs)
}
// JSONMerge mocks base method.
func (m *MockRedis) JSONMerge(ctx context.Context, key, path, value string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONMerge", ctx, key, path, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// JSONMerge indicates an expected call of JSONMerge.
func (mr *MockRedisMockRecorder) JSONMerge(ctx, key, path, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONMerge", reflect.TypeOf((*MockRedis)(nil).JSONMerge), ctx, key, path, value)
}
// JSONNumIncrBy mocks base method.
func (m *MockRedis) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *redis.JSONCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONNumIncrBy", ctx, key, path, value)
ret0, _ := ret[0].(*redis.JSONCmd)
return ret0
}
// JSONNumIncrBy indicates an expected call of JSONNumIncrBy.
func (mr *MockRedisMockRecorder) JSONNumIncrBy(ctx, key, path, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONNumIncrBy", reflect.TypeOf((*MockRedis)(nil).JSONNumIncrBy), ctx, key, path, value)
}
// JSONObjKeys mocks base method.
func (m *MockRedis) JSONObjKeys(ctx context.Context, key, path string) *redis.SliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONObjKeys", ctx, key, path)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// JSONObjKeys indicates an expected call of JSONObjKeys.
func (mr *MockRedisMockRecorder) JSONObjKeys(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONObjKeys", reflect.TypeOf((*MockRedis)(nil).JSONObjKeys), ctx, key, path)
}
// JSONObjLen mocks base method.
func (m *MockRedis) JSONObjLen(ctx context.Context, key, path string) *redis.IntPointerSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONObjLen", ctx, key, path)
ret0, _ := ret[0].(*redis.IntPointerSliceCmd)
return ret0
}
// JSONObjLen indicates an expected call of JSONObjLen.
func (mr *MockRedisMockRecorder) JSONObjLen(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONObjLen", reflect.TypeOf((*MockRedis)(nil).JSONObjLen), ctx, key, path)
}
// JSONSet mocks base method.
func (m *MockRedis) JSONSet(ctx context.Context, key, path string, value any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONSet", ctx, key, path, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// JSONSet indicates an expected call of JSONSet.
func (mr *MockRedisMockRecorder) JSONSet(ctx, key, path, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONSet", reflect.TypeOf((*MockRedis)(nil).JSONSet), ctx, key, path, value)
}
// JSONSetMode mocks base method.
func (m *MockRedis) JSONSetMode(ctx context.Context, key, path string, value any, mode string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONSetMode", ctx, key, path, value, mode)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// JSONSetMode indicates an expected call of JSONSetMode.
func (mr *MockRedisMockRecorder) JSONSetMode(ctx, key, path, value, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONSetMode", reflect.TypeOf((*MockRedis)(nil).JSONSetMode), ctx, key, path, value, mode)
}
// JSONStrAppend mocks base method.
func (m *MockRedis) JSONStrAppend(ctx context.Context, key, path, value string) *redis.IntPointerSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONStrAppend", ctx, key, path, value)
ret0, _ := ret[0].(*redis.IntPointerSliceCmd)
return ret0
}
// JSONStrAppend indicates an expected call of JSONStrAppend.
func (mr *MockRedisMockRecorder) JSONStrAppend(ctx, key, path, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONStrAppend", reflect.TypeOf((*MockRedis)(nil).JSONStrAppend), ctx, key, path, value)
}
// JSONStrLen mocks base method.
func (m *MockRedis) JSONStrLen(ctx context.Context, key, path string) *redis.IntPointerSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONStrLen", ctx, key, path)
ret0, _ := ret[0].(*redis.IntPointerSliceCmd)
return ret0
}
// JSONStrLen indicates an expected call of JSONStrLen.
func (mr *MockRedisMockRecorder) JSONStrLen(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONStrLen", reflect.TypeOf((*MockRedis)(nil).JSONStrLen), ctx, key, path)
}
// JSONToggle mocks base method.
func (m *MockRedis) JSONToggle(ctx context.Context, key, path string) *redis.IntPointerSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONToggle", ctx, key, path)
ret0, _ := ret[0].(*redis.IntPointerSliceCmd)
return ret0
}
// JSONToggle indicates an expected call of JSONToggle.
func (mr *MockRedisMockRecorder) JSONToggle(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONToggle", reflect.TypeOf((*MockRedis)(nil).JSONToggle), ctx, key, path)
}
// JSONType mocks base method.
func (m *MockRedis) JSONType(ctx context.Context, key, path string) *redis.JSONSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "JSONType", ctx, key, path)
ret0, _ := ret[0].(*redis.JSONSliceCmd)
return ret0
}
// JSONType indicates an expected call of JSONType.
func (mr *MockRedisMockRecorder) JSONType(ctx, key, path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "JSONType", reflect.TypeOf((*MockRedis)(nil).JSONType), ctx, key, path)
}
// Keys mocks base method.
func (m *MockRedis) Keys(ctx context.Context, pattern string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Keys", ctx, pattern)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// Keys indicates an expected call of Keys.
func (mr *MockRedisMockRecorder) Keys(ctx, pattern any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Keys", reflect.TypeOf((*MockRedis)(nil).Keys), ctx, pattern)
}
// LCS mocks base method.
func (m *MockRedis) LCS(ctx context.Context, q *redis.LCSQuery) *redis.LCSCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LCS", ctx, q)
ret0, _ := ret[0].(*redis.LCSCmd)
return ret0
}
// LCS indicates an expected call of LCS.
func (mr *MockRedisMockRecorder) LCS(ctx, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LCS", reflect.TypeOf((*MockRedis)(nil).LCS), ctx, q)
}
// LIndex mocks base method.
func (m *MockRedis) LIndex(ctx context.Context, key string, index int64) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LIndex", ctx, key, index)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// LIndex indicates an expected call of LIndex.
func (mr *MockRedisMockRecorder) LIndex(ctx, key, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LIndex", reflect.TypeOf((*MockRedis)(nil).LIndex), ctx, key, index)
}
// LInsert mocks base method.
func (m *MockRedis) LInsert(ctx context.Context, key, op string, pivot, value any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LInsert", ctx, key, op, pivot, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LInsert indicates an expected call of LInsert.
func (mr *MockRedisMockRecorder) LInsert(ctx, key, op, pivot, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsert", reflect.TypeOf((*MockRedis)(nil).LInsert), ctx, key, op, pivot, value)
}
// LInsertAfter mocks base method.
func (m *MockRedis) LInsertAfter(ctx context.Context, key string, pivot, value any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LInsertAfter", ctx, key, pivot, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LInsertAfter indicates an expected call of LInsertAfter.
func (mr *MockRedisMockRecorder) LInsertAfter(ctx, key, pivot, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsertAfter", reflect.TypeOf((*MockRedis)(nil).LInsertAfter), ctx, key, pivot, value)
}
// LInsertBefore mocks base method.
func (m *MockRedis) LInsertBefore(ctx context.Context, key string, pivot, value any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LInsertBefore", ctx, key, pivot, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LInsertBefore indicates an expected call of LInsertBefore.
func (mr *MockRedisMockRecorder) LInsertBefore(ctx, key, pivot, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LInsertBefore", reflect.TypeOf((*MockRedis)(nil).LInsertBefore), ctx, key, pivot, value)
}
// LLen mocks base method.
func (m *MockRedis) LLen(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LLen", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LLen indicates an expected call of LLen.
func (mr *MockRedisMockRecorder) LLen(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LLen", reflect.TypeOf((*MockRedis)(nil).LLen), ctx, key)
}
// LMPop mocks base method.
func (m *MockRedis) LMPop(ctx context.Context, direction string, count int64, keys ...string) *redis.KeyValuesCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, direction, count}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "LMPop", varargs...)
ret0, _ := ret[0].(*redis.KeyValuesCmd)
return ret0
}
// LMPop indicates an expected call of LMPop.
func (mr *MockRedisMockRecorder) LMPop(ctx, direction, count any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, direction, count}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LMPop", reflect.TypeOf((*MockRedis)(nil).LMPop), varargs...)
}
// LMove mocks base method.
func (m *MockRedis) LMove(ctx context.Context, source, destination, srcpos, destpos string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LMove", ctx, source, destination, srcpos, destpos)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// LMove indicates an expected call of LMove.
func (mr *MockRedisMockRecorder) LMove(ctx, source, destination, srcpos, destpos any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LMove", reflect.TypeOf((*MockRedis)(nil).LMove), ctx, source, destination, srcpos, destpos)
}
// LPop mocks base method.
func (m *MockRedis) LPop(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LPop", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// LPop indicates an expected call of LPop.
func (mr *MockRedisMockRecorder) LPop(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPop", reflect.TypeOf((*MockRedis)(nil).LPop), ctx, key)
}
// LPopCount mocks base method.
func (m *MockRedis) LPopCount(ctx context.Context, key string, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LPopCount", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// LPopCount indicates an expected call of LPopCount.
func (mr *MockRedisMockRecorder) LPopCount(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPopCount", reflect.TypeOf((*MockRedis)(nil).LPopCount), ctx, key, count)
}
// LPos mocks base method.
func (m *MockRedis) LPos(ctx context.Context, key, value string, args redis.LPosArgs) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LPos", ctx, key, value, args)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LPos indicates an expected call of LPos.
func (mr *MockRedisMockRecorder) LPos(ctx, key, value, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPos", reflect.TypeOf((*MockRedis)(nil).LPos), ctx, key, value, args)
}
// LPosCount mocks base method.
func (m *MockRedis) LPosCount(ctx context.Context, key, value string, count int64, args redis.LPosArgs) *redis.IntSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LPosCount", ctx, key, value, count, args)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// LPosCount indicates an expected call of LPosCount.
func (mr *MockRedisMockRecorder) LPosCount(ctx, key, value, count, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPosCount", reflect.TypeOf((*MockRedis)(nil).LPosCount), ctx, key, value, count, args)
}
// LPush mocks base method.
func (m *MockRedis) LPush(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "LPush", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LPush indicates an expected call of LPush.
func (mr *MockRedisMockRecorder) LPush(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPush", reflect.TypeOf((*MockRedis)(nil).LPush), varargs...)
}
// LPushX mocks base method.
func (m *MockRedis) LPushX(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "LPushX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LPushX indicates an expected call of LPushX.
func (mr *MockRedisMockRecorder) LPushX(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LPushX", reflect.TypeOf((*MockRedis)(nil).LPushX), varargs...)
}
// LRange mocks base method.
func (m *MockRedis) LRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LRange", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// LRange indicates an expected call of LRange.
func (mr *MockRedisMockRecorder) LRange(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LRange", reflect.TypeOf((*MockRedis)(nil).LRange), ctx, key, start, stop)
}
// LRem mocks base method.
func (m *MockRedis) LRem(ctx context.Context, key string, count int64, value any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LRem", ctx, key, count, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LRem indicates an expected call of LRem.
func (mr *MockRedisMockRecorder) LRem(ctx, key, count, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LRem", reflect.TypeOf((*MockRedis)(nil).LRem), ctx, key, count, value)
}
// LSet mocks base method.
func (m *MockRedis) LSet(ctx context.Context, key string, index int64, value any) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LSet", ctx, key, index, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// LSet indicates an expected call of LSet.
func (mr *MockRedisMockRecorder) LSet(ctx, key, index, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LSet", reflect.TypeOf((*MockRedis)(nil).LSet), ctx, key, index, value)
}
// LTrim mocks base method.
func (m *MockRedis) LTrim(ctx context.Context, key string, start, stop int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LTrim", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// LTrim indicates an expected call of LTrim.
func (mr *MockRedisMockRecorder) LTrim(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LTrim", reflect.TypeOf((*MockRedis)(nil).LTrim), ctx, key, start, stop)
}
// LastSave mocks base method.
func (m *MockRedis) LastSave(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LastSave", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// LastSave indicates an expected call of LastSave.
func (mr *MockRedisMockRecorder) LastSave(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastSave", reflect.TypeOf((*MockRedis)(nil).LastSave), ctx)
}
// Latency mocks base method.
func (m *MockRedis) Latency(ctx context.Context) *redis.LatencyCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Latency", ctx)
ret0, _ := ret[0].(*redis.LatencyCmd)
return ret0
}
// Latency indicates an expected call of Latency.
func (mr *MockRedisMockRecorder) Latency(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Latency", reflect.TypeOf((*MockRedis)(nil).Latency), ctx)
}
// LatencyReset mocks base method.
func (m *MockRedis) LatencyReset(ctx context.Context, events ...any) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range events {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "LatencyReset", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// LatencyReset indicates an expected call of LatencyReset.
func (mr *MockRedisMockRecorder) LatencyReset(ctx any, events ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, events...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LatencyReset", reflect.TypeOf((*MockRedis)(nil).LatencyReset), varargs...)
}
// MGet mocks base method.
func (m *MockRedis) MGet(ctx context.Context, keys ...string) *redis.SliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "MGet", varargs...)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// MGet indicates an expected call of MGet.
func (mr *MockRedisMockRecorder) MGet(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MGet", reflect.TypeOf((*MockRedis)(nil).MGet), varargs...)
}
// MSet mocks base method.
func (m *MockRedis) MSet(ctx context.Context, values ...any) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "MSet", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// MSet indicates an expected call of MSet.
func (mr *MockRedisMockRecorder) MSet(ctx any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MSet", reflect.TypeOf((*MockRedis)(nil).MSet), varargs...)
}
// MSetEX mocks base method.
func (m *MockRedis) MSetEX(ctx context.Context, args redis.MSetEXArgs, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, args}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "MSetEX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// MSetEX indicates an expected call of MSetEX.
func (mr *MockRedisMockRecorder) MSetEX(ctx, args any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, args}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MSetEX", reflect.TypeOf((*MockRedis)(nil).MSetEX), varargs...)
}
// MSetNX mocks base method.
func (m *MockRedis) MSetNX(ctx context.Context, values ...any) *redis.BoolCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "MSetNX", varargs...)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// MSetNX indicates an expected call of MSetNX.
func (mr *MockRedisMockRecorder) MSetNX(ctx any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MSetNX", reflect.TypeOf((*MockRedis)(nil).MSetNX), varargs...)
}
// MemoryUsage mocks base method.
func (m *MockRedis) MemoryUsage(ctx context.Context, key string, samples ...int) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range samples {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "MemoryUsage", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// MemoryUsage indicates an expected call of MemoryUsage.
func (mr *MockRedisMockRecorder) MemoryUsage(ctx, key any, samples ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, samples...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MemoryUsage", reflect.TypeOf((*MockRedis)(nil).MemoryUsage), varargs...)
}
// Migrate mocks base method.
func (m *MockRedis) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Migrate", ctx, host, port, key, db, timeout)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Migrate indicates an expected call of Migrate.
func (mr *MockRedisMockRecorder) Migrate(ctx, host, port, key, db, timeout any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockRedis)(nil).Migrate), ctx, host, port, key, db, timeout)
}
// ModuleLoadex mocks base method.
func (m *MockRedis) ModuleLoadex(ctx context.Context, conf *redis.ModuleLoadexConfig) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ModuleLoadex", ctx, conf)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ModuleLoadex indicates an expected call of ModuleLoadex.
func (mr *MockRedisMockRecorder) ModuleLoadex(ctx, conf any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModuleLoadex", reflect.TypeOf((*MockRedis)(nil).ModuleLoadex), ctx, conf)
}
// Move mocks base method.
func (m *MockRedis) Move(ctx context.Context, key string, db int) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Move", ctx, key, db)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// Move indicates an expected call of Move.
func (mr *MockRedisMockRecorder) Move(ctx, key, db any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Move", reflect.TypeOf((*MockRedis)(nil).Move), ctx, key, db)
}
// ObjectEncoding mocks base method.
func (m *MockRedis) ObjectEncoding(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectEncoding", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ObjectEncoding indicates an expected call of ObjectEncoding.
func (mr *MockRedisMockRecorder) ObjectEncoding(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectEncoding", reflect.TypeOf((*MockRedis)(nil).ObjectEncoding), ctx, key)
}
// ObjectFreq mocks base method.
func (m *MockRedis) ObjectFreq(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectFreq", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ObjectFreq indicates an expected call of ObjectFreq.
func (mr *MockRedisMockRecorder) ObjectFreq(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectFreq", reflect.TypeOf((*MockRedis)(nil).ObjectFreq), ctx, key)
}
// ObjectIdleTime mocks base method.
func (m *MockRedis) ObjectIdleTime(ctx context.Context, key string) *redis.DurationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectIdleTime", ctx, key)
ret0, _ := ret[0].(*redis.DurationCmd)
return ret0
}
// ObjectIdleTime indicates an expected call of ObjectIdleTime.
func (mr *MockRedisMockRecorder) ObjectIdleTime(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectIdleTime", reflect.TypeOf((*MockRedis)(nil).ObjectIdleTime), ctx, key)
}
// ObjectRefCount mocks base method.
func (m *MockRedis) ObjectRefCount(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectRefCount", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ObjectRefCount indicates an expected call of ObjectRefCount.
func (mr *MockRedisMockRecorder) ObjectRefCount(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectRefCount", reflect.TypeOf((*MockRedis)(nil).ObjectRefCount), ctx, key)
}
// PExpire mocks base method.
func (m *MockRedis) PExpire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PExpire", ctx, key, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// PExpire indicates an expected call of PExpire.
func (mr *MockRedisMockRecorder) PExpire(ctx, key, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpire", reflect.TypeOf((*MockRedis)(nil).PExpire), ctx, key, expiration)
}
// PExpireAt mocks base method.
func (m *MockRedis) PExpireAt(ctx context.Context, key string, tm time.Time) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PExpireAt", ctx, key, tm)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// PExpireAt indicates an expected call of PExpireAt.
func (mr *MockRedisMockRecorder) PExpireAt(ctx, key, tm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpireAt", reflect.TypeOf((*MockRedis)(nil).PExpireAt), ctx, key, tm)
}
// PExpireTime mocks base method.
func (m *MockRedis) PExpireTime(ctx context.Context, key string) *redis.DurationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PExpireTime", ctx, key)
ret0, _ := ret[0].(*redis.DurationCmd)
return ret0
}
// PExpireTime indicates an expected call of PExpireTime.
func (mr *MockRedisMockRecorder) PExpireTime(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PExpireTime", reflect.TypeOf((*MockRedis)(nil).PExpireTime), ctx, key)
}
// PFAdd mocks base method.
func (m *MockRedis) PFAdd(ctx context.Context, key string, els ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range els {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PFAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// PFAdd indicates an expected call of PFAdd.
func (mr *MockRedisMockRecorder) PFAdd(ctx, key any, els ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, els...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFAdd", reflect.TypeOf((*MockRedis)(nil).PFAdd), varargs...)
}
// PFCount mocks base method.
func (m *MockRedis) PFCount(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PFCount", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// PFCount indicates an expected call of PFCount.
func (mr *MockRedisMockRecorder) PFCount(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFCount", reflect.TypeOf((*MockRedis)(nil).PFCount), varargs...)
}
// PFMerge mocks base method.
func (m *MockRedis) PFMerge(ctx context.Context, dest string, keys ...string) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, dest}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PFMerge", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// PFMerge indicates an expected call of PFMerge.
func (mr *MockRedisMockRecorder) PFMerge(ctx, dest any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PFMerge", reflect.TypeOf((*MockRedis)(nil).PFMerge), varargs...)
}
// PTTL mocks base method.
func (m *MockRedis) PTTL(ctx context.Context, key string) *redis.DurationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PTTL", ctx, key)
ret0, _ := ret[0].(*redis.DurationCmd)
return ret0
}
// PTTL indicates an expected call of PTTL.
func (mr *MockRedisMockRecorder) PTTL(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PTTL", reflect.TypeOf((*MockRedis)(nil).PTTL), ctx, key)
}
// Persist mocks base method.
func (m *MockRedis) Persist(ctx context.Context, key string) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Persist", ctx, key)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// Persist indicates an expected call of Persist.
func (mr *MockRedisMockRecorder) Persist(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Persist", reflect.TypeOf((*MockRedis)(nil).Persist), ctx, key)
}
// Ping mocks base method.
func (m *MockRedis) Ping(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Ping indicates an expected call of Ping.
func (mr *MockRedisMockRecorder) Ping(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockRedis)(nil).Ping), ctx)
}
// Pipeline mocks base method.
func (m *MockRedis) Pipeline() redis.Pipeliner {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Pipeline")
ret0, _ := ret[0].(redis.Pipeliner)
return ret0
}
// Pipeline indicates an expected call of Pipeline.
func (mr *MockRedisMockRecorder) Pipeline() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipeline", reflect.TypeOf((*MockRedis)(nil).Pipeline))
}
// Pipelined mocks base method.
func (m *MockRedis) Pipelined(ctx context.Context, fn func(redis.Pipeliner) error) ([]redis.Cmder, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Pipelined", ctx, fn)
ret0, _ := ret[0].([]redis.Cmder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Pipelined indicates an expected call of Pipelined.
func (mr *MockRedisMockRecorder) Pipelined(ctx, fn any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pipelined", reflect.TypeOf((*MockRedis)(nil).Pipelined), ctx, fn)
}
// PubSubChannels mocks base method.
func (m *MockRedis) PubSubChannels(ctx context.Context, pattern string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PubSubChannels", ctx, pattern)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// PubSubChannels indicates an expected call of PubSubChannels.
func (mr *MockRedisMockRecorder) PubSubChannels(ctx, pattern any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubChannels", reflect.TypeOf((*MockRedis)(nil).PubSubChannels), ctx, pattern)
}
// PubSubNumPat mocks base method.
func (m *MockRedis) PubSubNumPat(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PubSubNumPat", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// PubSubNumPat indicates an expected call of PubSubNumPat.
func (mr *MockRedisMockRecorder) PubSubNumPat(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubNumPat", reflect.TypeOf((*MockRedis)(nil).PubSubNumPat), ctx)
}
// PubSubNumSub mocks base method.
func (m *MockRedis) PubSubNumSub(ctx context.Context, channels ...string) *redis.MapStringIntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range channels {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PubSubNumSub", varargs...)
ret0, _ := ret[0].(*redis.MapStringIntCmd)
return ret0
}
// PubSubNumSub indicates an expected call of PubSubNumSub.
func (mr *MockRedisMockRecorder) PubSubNumSub(ctx any, channels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, channels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubNumSub", reflect.TypeOf((*MockRedis)(nil).PubSubNumSub), varargs...)
}
// PubSubShardChannels mocks base method.
func (m *MockRedis) PubSubShardChannels(ctx context.Context, pattern string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PubSubShardChannels", ctx, pattern)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// PubSubShardChannels indicates an expected call of PubSubShardChannels.
func (mr *MockRedisMockRecorder) PubSubShardChannels(ctx, pattern any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubShardChannels", reflect.TypeOf((*MockRedis)(nil).PubSubShardChannels), ctx, pattern)
}
// PubSubShardNumSub mocks base method.
func (m *MockRedis) PubSubShardNumSub(ctx context.Context, channels ...string) *redis.MapStringIntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range channels {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PubSubShardNumSub", varargs...)
ret0, _ := ret[0].(*redis.MapStringIntCmd)
return ret0
}
// PubSubShardNumSub indicates an expected call of PubSubShardNumSub.
func (mr *MockRedisMockRecorder) PubSubShardNumSub(ctx any, channels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, channels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PubSubShardNumSub", reflect.TypeOf((*MockRedis)(nil).PubSubShardNumSub), varargs...)
}
// Publish mocks base method.
func (m *MockRedis) Publish(ctx context.Context, channel string, message any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, channel, message)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockRedisMockRecorder) Publish(ctx, channel, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockRedis)(nil).Publish), ctx, channel, message)
}
// Quit mocks base method.
func (m *MockRedis) Quit(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Quit", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Quit indicates an expected call of Quit.
func (mr *MockRedisMockRecorder) Quit(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Quit", reflect.TypeOf((*MockRedis)(nil).Quit), ctx)
}
// RPop mocks base method.
func (m *MockRedis) RPop(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RPop", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// RPop indicates an expected call of RPop.
func (mr *MockRedisMockRecorder) RPop(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPop", reflect.TypeOf((*MockRedis)(nil).RPop), ctx, key)
}
// RPopCount mocks base method.
func (m *MockRedis) RPopCount(ctx context.Context, key string, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RPopCount", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// RPopCount indicates an expected call of RPopCount.
func (mr *MockRedisMockRecorder) RPopCount(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPopCount", reflect.TypeOf((*MockRedis)(nil).RPopCount), ctx, key, count)
}
// RPopLPush mocks base method.
func (m *MockRedis) RPopLPush(ctx context.Context, source, destination string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RPopLPush", ctx, source, destination)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// RPopLPush indicates an expected call of RPopLPush.
func (mr *MockRedisMockRecorder) RPopLPush(ctx, source, destination any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPopLPush", reflect.TypeOf((*MockRedis)(nil).RPopLPush), ctx, source, destination)
}
// RPush mocks base method.
func (m *MockRedis) RPush(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "RPush", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// RPush indicates an expected call of RPush.
func (mr *MockRedisMockRecorder) RPush(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPush", reflect.TypeOf((*MockRedis)(nil).RPush), varargs...)
}
// RPushX mocks base method.
func (m *MockRedis) RPushX(ctx context.Context, key string, values ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "RPushX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// RPushX indicates an expected call of RPushX.
func (mr *MockRedisMockRecorder) RPushX(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RPushX", reflect.TypeOf((*MockRedis)(nil).RPushX), varargs...)
}
// RandomKey mocks base method.
func (m *MockRedis) RandomKey(ctx context.Context) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RandomKey", ctx)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// RandomKey indicates an expected call of RandomKey.
func (mr *MockRedisMockRecorder) RandomKey(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RandomKey", reflect.TypeOf((*MockRedis)(nil).RandomKey), ctx)
}
// ReadOnly mocks base method.
func (m *MockRedis) ReadOnly(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadOnly", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ReadOnly indicates an expected call of ReadOnly.
func (mr *MockRedisMockRecorder) ReadOnly(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadOnly", reflect.TypeOf((*MockRedis)(nil).ReadOnly), ctx)
}
// ReadWrite mocks base method.
func (m *MockRedis) ReadWrite(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadWrite", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ReadWrite indicates an expected call of ReadWrite.
func (mr *MockRedisMockRecorder) ReadWrite(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadWrite", reflect.TypeOf((*MockRedis)(nil).ReadWrite), ctx)
}
// Rename mocks base method.
func (m *MockRedis) Rename(ctx context.Context, key, newkey string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", ctx, key, newkey)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Rename indicates an expected call of Rename.
func (mr *MockRedisMockRecorder) Rename(ctx, key, newkey any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockRedis)(nil).Rename), ctx, key, newkey)
}
// RenameNX mocks base method.
func (m *MockRedis) RenameNX(ctx context.Context, key, newkey string) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RenameNX", ctx, key, newkey)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// RenameNX indicates an expected call of RenameNX.
func (mr *MockRedisMockRecorder) RenameNX(ctx, key, newkey any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RenameNX", reflect.TypeOf((*MockRedis)(nil).RenameNX), ctx, key, newkey)
}
// Restore mocks base method.
func (m *MockRedis) Restore(ctx context.Context, key string, ttl time.Duration, value string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Restore", ctx, key, ttl, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Restore indicates an expected call of Restore.
func (mr *MockRedisMockRecorder) Restore(ctx, key, ttl, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Restore", reflect.TypeOf((*MockRedis)(nil).Restore), ctx, key, ttl, value)
}
// RestoreReplace mocks base method.
func (m *MockRedis) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RestoreReplace", ctx, key, ttl, value)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// RestoreReplace indicates an expected call of RestoreReplace.
func (mr *MockRedisMockRecorder) RestoreReplace(ctx, key, ttl, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreReplace", reflect.TypeOf((*MockRedis)(nil).RestoreReplace), ctx, key, ttl, value)
}
// SAdd mocks base method.
func (m *MockRedis) SAdd(ctx context.Context, key string, members ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SAdd indicates an expected call of SAdd.
func (mr *MockRedisMockRecorder) SAdd(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SAdd", reflect.TypeOf((*MockRedis)(nil).SAdd), varargs...)
}
// SCard mocks base method.
func (m *MockRedis) SCard(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SCard", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SCard indicates an expected call of SCard.
func (mr *MockRedisMockRecorder) SCard(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SCard", reflect.TypeOf((*MockRedis)(nil).SCard), ctx, key)
}
// SDiff mocks base method.
func (m *MockRedis) SDiff(ctx context.Context, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SDiff", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SDiff indicates an expected call of SDiff.
func (mr *MockRedisMockRecorder) SDiff(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SDiff", reflect.TypeOf((*MockRedis)(nil).SDiff), varargs...)
}
// SDiffStore mocks base method.
func (m *MockRedis) SDiffStore(ctx context.Context, destination string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destination}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SDiffStore", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SDiffStore indicates an expected call of SDiffStore.
func (mr *MockRedisMockRecorder) SDiffStore(ctx, destination any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destination}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SDiffStore", reflect.TypeOf((*MockRedis)(nil).SDiffStore), varargs...)
}
// SInter mocks base method.
func (m *MockRedis) SInter(ctx context.Context, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SInter", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SInter indicates an expected call of SInter.
func (mr *MockRedisMockRecorder) SInter(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInter", reflect.TypeOf((*MockRedis)(nil).SInter), varargs...)
}
// SInterCard mocks base method.
func (m *MockRedis) SInterCard(ctx context.Context, limit int64, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, limit}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SInterCard", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SInterCard indicates an expected call of SInterCard.
func (mr *MockRedisMockRecorder) SInterCard(ctx, limit any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, limit}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInterCard", reflect.TypeOf((*MockRedis)(nil).SInterCard), varargs...)
}
// SInterStore mocks base method.
func (m *MockRedis) SInterStore(ctx context.Context, destination string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destination}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SInterStore", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SInterStore indicates an expected call of SInterStore.
func (mr *MockRedisMockRecorder) SInterStore(ctx, destination any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destination}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SInterStore", reflect.TypeOf((*MockRedis)(nil).SInterStore), varargs...)
}
// SIsMember mocks base method.
func (m *MockRedis) SIsMember(ctx context.Context, key string, member any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SIsMember", ctx, key, member)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// SIsMember indicates an expected call of SIsMember.
func (mr *MockRedisMockRecorder) SIsMember(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SIsMember", reflect.TypeOf((*MockRedis)(nil).SIsMember), ctx, key, member)
}
// SMIsMember mocks base method.
func (m *MockRedis) SMIsMember(ctx context.Context, key string, members ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SMIsMember", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// SMIsMember indicates an expected call of SMIsMember.
func (mr *MockRedisMockRecorder) SMIsMember(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMIsMember", reflect.TypeOf((*MockRedis)(nil).SMIsMember), varargs...)
}
// SMembers mocks base method.
func (m *MockRedis) SMembers(ctx context.Context, key string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SMembers", ctx, key)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SMembers indicates an expected call of SMembers.
func (mr *MockRedisMockRecorder) SMembers(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMembers", reflect.TypeOf((*MockRedis)(nil).SMembers), ctx, key)
}
// SMembersMap mocks base method.
func (m *MockRedis) SMembersMap(ctx context.Context, key string) *redis.StringStructMapCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SMembersMap", ctx, key)
ret0, _ := ret[0].(*redis.StringStructMapCmd)
return ret0
}
// SMembersMap indicates an expected call of SMembersMap.
func (mr *MockRedisMockRecorder) SMembersMap(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMembersMap", reflect.TypeOf((*MockRedis)(nil).SMembersMap), ctx, key)
}
// SMove mocks base method.
func (m *MockRedis) SMove(ctx context.Context, source, destination string, member any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SMove", ctx, source, destination, member)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// SMove indicates an expected call of SMove.
func (mr *MockRedisMockRecorder) SMove(ctx, source, destination, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SMove", reflect.TypeOf((*MockRedis)(nil).SMove), ctx, source, destination, member)
}
// SPop mocks base method.
func (m *MockRedis) SPop(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SPop", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SPop indicates an expected call of SPop.
func (mr *MockRedisMockRecorder) SPop(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPop", reflect.TypeOf((*MockRedis)(nil).SPop), ctx, key)
}
// SPopN mocks base method.
func (m *MockRedis) SPopN(ctx context.Context, key string, count int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SPopN", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SPopN indicates an expected call of SPopN.
func (mr *MockRedisMockRecorder) SPopN(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPopN", reflect.TypeOf((*MockRedis)(nil).SPopN), ctx, key, count)
}
// SPublish mocks base method.
func (m *MockRedis) SPublish(ctx context.Context, channel string, message any) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SPublish", ctx, channel, message)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SPublish indicates an expected call of SPublish.
func (mr *MockRedisMockRecorder) SPublish(ctx, channel, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SPublish", reflect.TypeOf((*MockRedis)(nil).SPublish), ctx, channel, message)
}
// SRandMember mocks base method.
func (m *MockRedis) SRandMember(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SRandMember", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SRandMember indicates an expected call of SRandMember.
func (mr *MockRedisMockRecorder) SRandMember(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRandMember", reflect.TypeOf((*MockRedis)(nil).SRandMember), ctx, key)
}
// SRandMemberN mocks base method.
func (m *MockRedis) SRandMemberN(ctx context.Context, key string, count int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SRandMemberN", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SRandMemberN indicates an expected call of SRandMemberN.
func (mr *MockRedisMockRecorder) SRandMemberN(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRandMemberN", reflect.TypeOf((*MockRedis)(nil).SRandMemberN), ctx, key, count)
}
// SRem mocks base method.
func (m *MockRedis) SRem(ctx context.Context, key string, members ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SRem", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SRem indicates an expected call of SRem.
func (mr *MockRedisMockRecorder) SRem(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SRem", reflect.TypeOf((*MockRedis)(nil).SRem), varargs...)
}
// SScan mocks base method.
func (m *MockRedis) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SScan", ctx, key, cursor, match, count)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// SScan indicates an expected call of SScan.
func (mr *MockRedisMockRecorder) SScan(ctx, key, cursor, match, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SScan", reflect.TypeOf((*MockRedis)(nil).SScan), ctx, key, cursor, match, count)
}
// SUnion mocks base method.
func (m *MockRedis) SUnion(ctx context.Context, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SUnion", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SUnion indicates an expected call of SUnion.
func (mr *MockRedisMockRecorder) SUnion(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SUnion", reflect.TypeOf((*MockRedis)(nil).SUnion), varargs...)
}
// SUnionStore mocks base method.
func (m *MockRedis) SUnionStore(ctx context.Context, destination string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destination}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SUnionStore", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SUnionStore indicates an expected call of SUnionStore.
func (mr *MockRedisMockRecorder) SUnionStore(ctx, destination any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destination}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SUnionStore", reflect.TypeOf((*MockRedis)(nil).SUnionStore), varargs...)
}
// Save mocks base method.
func (m *MockRedis) Save(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Save", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Save indicates an expected call of Save.
func (mr *MockRedisMockRecorder) Save(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Save", reflect.TypeOf((*MockRedis)(nil).Save), ctx)
}
// Scan mocks base method.
func (m *MockRedis) Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Scan", ctx, cursor, match, count)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// Scan indicates an expected call of Scan.
func (mr *MockRedisMockRecorder) Scan(ctx, cursor, match, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockRedis)(nil).Scan), ctx, cursor, match, count)
}
// ScanType mocks base method.
func (m *MockRedis) ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ScanType", ctx, cursor, match, count, keyType)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// ScanType indicates an expected call of ScanType.
func (mr *MockRedisMockRecorder) ScanType(ctx, cursor, match, count, keyType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanType", reflect.TypeOf((*MockRedis)(nil).ScanType), ctx, cursor, match, count, keyType)
}
// ScriptExists mocks base method.
func (m *MockRedis) ScriptExists(ctx context.Context, hashes ...string) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range hashes {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ScriptExists", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// ScriptExists indicates an expected call of ScriptExists.
func (mr *MockRedisMockRecorder) ScriptExists(ctx any, hashes ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, hashes...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptExists", reflect.TypeOf((*MockRedis)(nil).ScriptExists), varargs...)
}
// ScriptFlush mocks base method.
func (m *MockRedis) ScriptFlush(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ScriptFlush", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ScriptFlush indicates an expected call of ScriptFlush.
func (mr *MockRedisMockRecorder) ScriptFlush(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptFlush", reflect.TypeOf((*MockRedis)(nil).ScriptFlush), ctx)
}
// ScriptKill mocks base method.
func (m *MockRedis) ScriptKill(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ScriptKill", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ScriptKill indicates an expected call of ScriptKill.
func (mr *MockRedisMockRecorder) ScriptKill(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptKill", reflect.TypeOf((*MockRedis)(nil).ScriptKill), ctx)
}
// ScriptLoad mocks base method.
func (m *MockRedis) ScriptLoad(ctx context.Context, script string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ScriptLoad", ctx, script)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// ScriptLoad indicates an expected call of ScriptLoad.
func (mr *MockRedisMockRecorder) ScriptLoad(ctx, script any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScriptLoad", reflect.TypeOf((*MockRedis)(nil).ScriptLoad), ctx, script)
}
// Set mocks base method.
func (m *MockRedis) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockRedisMockRecorder) Set(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRedis)(nil).Set), ctx, key, value, expiration)
}
// SetArgs mocks base method.
func (m *MockRedis) SetArgs(ctx context.Context, key string, value any, a redis.SetArgs) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetArgs", ctx, key, value, a)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetArgs indicates an expected call of SetArgs.
func (mr *MockRedisMockRecorder) SetArgs(ctx, key, value, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetArgs", reflect.TypeOf((*MockRedis)(nil).SetArgs), ctx, key, value, a)
}
// SetBit mocks base method.
func (m *MockRedis) SetBit(ctx context.Context, key string, offset int64, value int) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetBit", ctx, key, offset, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SetBit indicates an expected call of SetBit.
func (mr *MockRedisMockRecorder) SetBit(ctx, key, offset, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBit", reflect.TypeOf((*MockRedis)(nil).SetBit), ctx, key, offset, value)
}
// SetEx mocks base method.
func (m *MockRedis) SetEx(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetEx", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetEx indicates an expected call of SetEx.
func (mr *MockRedisMockRecorder) SetEx(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEx", reflect.TypeOf((*MockRedis)(nil).SetEx), ctx, key, value, expiration)
}
// SetIFDEQ mocks base method.
func (m *MockRedis) SetIFDEQ(ctx context.Context, key string, value any, matchDigest uint64, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFDEQ", ctx, key, value, matchDigest, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetIFDEQ indicates an expected call of SetIFDEQ.
func (mr *MockRedisMockRecorder) SetIFDEQ(ctx, key, value, matchDigest, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFDEQ", reflect.TypeOf((*MockRedis)(nil).SetIFDEQ), ctx, key, value, matchDigest, expiration)
}
// SetIFDEQGet mocks base method.
func (m *MockRedis) SetIFDEQGet(ctx context.Context, key string, value any, matchDigest uint64, expiration time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFDEQGet", ctx, key, value, matchDigest, expiration)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SetIFDEQGet indicates an expected call of SetIFDEQGet.
func (mr *MockRedisMockRecorder) SetIFDEQGet(ctx, key, value, matchDigest, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFDEQGet", reflect.TypeOf((*MockRedis)(nil).SetIFDEQGet), ctx, key, value, matchDigest, expiration)
}
// SetIFDNE mocks base method.
func (m *MockRedis) SetIFDNE(ctx context.Context, key string, value any, matchDigest uint64, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFDNE", ctx, key, value, matchDigest, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetIFDNE indicates an expected call of SetIFDNE.
func (mr *MockRedisMockRecorder) SetIFDNE(ctx, key, value, matchDigest, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFDNE", reflect.TypeOf((*MockRedis)(nil).SetIFDNE), ctx, key, value, matchDigest, expiration)
}
// SetIFDNEGet mocks base method.
func (m *MockRedis) SetIFDNEGet(ctx context.Context, key string, value any, matchDigest uint64, expiration time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFDNEGet", ctx, key, value, matchDigest, expiration)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SetIFDNEGet indicates an expected call of SetIFDNEGet.
func (mr *MockRedisMockRecorder) SetIFDNEGet(ctx, key, value, matchDigest, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFDNEGet", reflect.TypeOf((*MockRedis)(nil).SetIFDNEGet), ctx, key, value, matchDigest, expiration)
}
// SetIFEQ mocks base method.
func (m *MockRedis) SetIFEQ(ctx context.Context, key string, value, matchValue any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFEQ", ctx, key, value, matchValue, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetIFEQ indicates an expected call of SetIFEQ.
func (mr *MockRedisMockRecorder) SetIFEQ(ctx, key, value, matchValue, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFEQ", reflect.TypeOf((*MockRedis)(nil).SetIFEQ), ctx, key, value, matchValue, expiration)
}
// SetIFEQGet mocks base method.
func (m *MockRedis) SetIFEQGet(ctx context.Context, key string, value, matchValue any, expiration time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFEQGet", ctx, key, value, matchValue, expiration)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SetIFEQGet indicates an expected call of SetIFEQGet.
func (mr *MockRedisMockRecorder) SetIFEQGet(ctx, key, value, matchValue, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFEQGet", reflect.TypeOf((*MockRedis)(nil).SetIFEQGet), ctx, key, value, matchValue, expiration)
}
// SetIFNE mocks base method.
func (m *MockRedis) SetIFNE(ctx context.Context, key string, value, matchValue any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFNE", ctx, key, value, matchValue, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SetIFNE indicates an expected call of SetIFNE.
func (mr *MockRedisMockRecorder) SetIFNE(ctx, key, value, matchValue, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFNE", reflect.TypeOf((*MockRedis)(nil).SetIFNE), ctx, key, value, matchValue, expiration)
}
// SetIFNEGet mocks base method.
func (m *MockRedis) SetIFNEGet(ctx context.Context, key string, value, matchValue any, expiration time.Duration) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetIFNEGet", ctx, key, value, matchValue, expiration)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// SetIFNEGet indicates an expected call of SetIFNEGet.
func (mr *MockRedisMockRecorder) SetIFNEGet(ctx, key, value, matchValue, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetIFNEGet", reflect.TypeOf((*MockRedis)(nil).SetIFNEGet), ctx, key, value, matchValue, expiration)
}
// SetNX mocks base method.
func (m *MockRedis) SetNX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetNX", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// SetNX indicates an expected call of SetNX.
func (mr *MockRedisMockRecorder) SetNX(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetNX", reflect.TypeOf((*MockRedis)(nil).SetNX), ctx, key, value, expiration)
}
// SetRange mocks base method.
func (m *MockRedis) SetRange(ctx context.Context, key string, offset int64, value string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetRange", ctx, key, offset, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SetRange indicates an expected call of SetRange.
func (mr *MockRedisMockRecorder) SetRange(ctx, key, offset, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetRange", reflect.TypeOf((*MockRedis)(nil).SetRange), ctx, key, offset, value)
}
// SetXX mocks base method.
func (m *MockRedis) SetXX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetXX", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// SetXX indicates an expected call of SetXX.
func (mr *MockRedisMockRecorder) SetXX(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetXX", reflect.TypeOf((*MockRedis)(nil).SetXX), ctx, key, value, expiration)
}
// Shutdown mocks base method.
func (m *MockRedis) Shutdown(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Shutdown", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Shutdown indicates an expected call of Shutdown.
func (mr *MockRedisMockRecorder) Shutdown(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shutdown", reflect.TypeOf((*MockRedis)(nil).Shutdown), ctx)
}
// ShutdownNoSave mocks base method.
func (m *MockRedis) ShutdownNoSave(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ShutdownNoSave", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ShutdownNoSave indicates an expected call of ShutdownNoSave.
func (mr *MockRedisMockRecorder) ShutdownNoSave(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownNoSave", reflect.TypeOf((*MockRedis)(nil).ShutdownNoSave), ctx)
}
// ShutdownSave mocks base method.
func (m *MockRedis) ShutdownSave(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ShutdownSave", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// ShutdownSave indicates an expected call of ShutdownSave.
func (mr *MockRedisMockRecorder) ShutdownSave(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShutdownSave", reflect.TypeOf((*MockRedis)(nil).ShutdownSave), ctx)
}
// SlaveOf mocks base method.
func (m *MockRedis) SlaveOf(ctx context.Context, host, port string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SlaveOf", ctx, host, port)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SlaveOf indicates an expected call of SlaveOf.
func (mr *MockRedisMockRecorder) SlaveOf(ctx, host, port any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlaveOf", reflect.TypeOf((*MockRedis)(nil).SlaveOf), ctx, host, port)
}
// SlowLogGet mocks base method.
func (m *MockRedis) SlowLogGet(ctx context.Context, num int64) *redis.SlowLogCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SlowLogGet", ctx, num)
ret0, _ := ret[0].(*redis.SlowLogCmd)
return ret0
}
// SlowLogGet indicates an expected call of SlowLogGet.
func (mr *MockRedisMockRecorder) SlowLogGet(ctx, num any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlowLogGet", reflect.TypeOf((*MockRedis)(nil).SlowLogGet), ctx, num)
}
// SlowLogLen mocks base method.
func (m *MockRedis) SlowLogLen(ctx context.Context) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SlowLogLen", ctx)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SlowLogLen indicates an expected call of SlowLogLen.
func (mr *MockRedisMockRecorder) SlowLogLen(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlowLogLen", reflect.TypeOf((*MockRedis)(nil).SlowLogLen), ctx)
}
// SlowLogReset mocks base method.
func (m *MockRedis) SlowLogReset(ctx context.Context) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SlowLogReset", ctx)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// SlowLogReset indicates an expected call of SlowLogReset.
func (mr *MockRedisMockRecorder) SlowLogReset(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SlowLogReset", reflect.TypeOf((*MockRedis)(nil).SlowLogReset), ctx)
}
// Sort mocks base method.
func (m *MockRedis) Sort(ctx context.Context, key string, sort *redis.Sort) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Sort", ctx, key, sort)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// Sort indicates an expected call of Sort.
func (mr *MockRedisMockRecorder) Sort(ctx, key, sort any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sort", reflect.TypeOf((*MockRedis)(nil).Sort), ctx, key, sort)
}
// SortInterfaces mocks base method.
func (m *MockRedis) SortInterfaces(ctx context.Context, key string, sort *redis.Sort) *redis.SliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SortInterfaces", ctx, key, sort)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// SortInterfaces indicates an expected call of SortInterfaces.
func (mr *MockRedisMockRecorder) SortInterfaces(ctx, key, sort any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortInterfaces", reflect.TypeOf((*MockRedis)(nil).SortInterfaces), ctx, key, sort)
}
// SortRO mocks base method.
func (m *MockRedis) SortRO(ctx context.Context, key string, sort *redis.Sort) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SortRO", ctx, key, sort)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// SortRO indicates an expected call of SortRO.
func (mr *MockRedisMockRecorder) SortRO(ctx, key, sort any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortRO", reflect.TypeOf((*MockRedis)(nil).SortRO), ctx, key, sort)
}
// SortStore mocks base method.
func (m *MockRedis) SortStore(ctx context.Context, key, store string, sort *redis.Sort) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SortStore", ctx, key, store, sort)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// SortStore indicates an expected call of SortStore.
func (mr *MockRedisMockRecorder) SortStore(ctx, key, store, sort any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SortStore", reflect.TypeOf((*MockRedis)(nil).SortStore), ctx, key, store, sort)
}
// StrLen mocks base method.
func (m *MockRedis) StrLen(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StrLen", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// StrLen indicates an expected call of StrLen.
func (mr *MockRedisMockRecorder) StrLen(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StrLen", reflect.TypeOf((*MockRedis)(nil).StrLen), ctx, key)
}
// TDigestAdd mocks base method.
func (m *MockRedis) TDigestAdd(ctx context.Context, key string, elements ...float64) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestAdd", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TDigestAdd indicates an expected call of TDigestAdd.
func (mr *MockRedisMockRecorder) TDigestAdd(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestAdd", reflect.TypeOf((*MockRedis)(nil).TDigestAdd), varargs...)
}
// TDigestByRank mocks base method.
func (m *MockRedis) TDigestByRank(ctx context.Context, key string, rank ...uint64) *redis.FloatSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range rank {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestByRank", varargs...)
ret0, _ := ret[0].(*redis.FloatSliceCmd)
return ret0
}
// TDigestByRank indicates an expected call of TDigestByRank.
func (mr *MockRedisMockRecorder) TDigestByRank(ctx, key any, rank ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, rank...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestByRank", reflect.TypeOf((*MockRedis)(nil).TDigestByRank), varargs...)
}
// TDigestByRevRank mocks base method.
func (m *MockRedis) TDigestByRevRank(ctx context.Context, key string, rank ...uint64) *redis.FloatSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range rank {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestByRevRank", varargs...)
ret0, _ := ret[0].(*redis.FloatSliceCmd)
return ret0
}
// TDigestByRevRank indicates an expected call of TDigestByRevRank.
func (mr *MockRedisMockRecorder) TDigestByRevRank(ctx, key any, rank ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, rank...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestByRevRank", reflect.TypeOf((*MockRedis)(nil).TDigestByRevRank), varargs...)
}
// TDigestCDF mocks base method.
func (m *MockRedis) TDigestCDF(ctx context.Context, key string, elements ...float64) *redis.FloatSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestCDF", varargs...)
ret0, _ := ret[0].(*redis.FloatSliceCmd)
return ret0
}
// TDigestCDF indicates an expected call of TDigestCDF.
func (mr *MockRedisMockRecorder) TDigestCDF(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestCDF", reflect.TypeOf((*MockRedis)(nil).TDigestCDF), varargs...)
}
// TDigestCreate mocks base method.
func (m *MockRedis) TDigestCreate(ctx context.Context, key string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestCreate", ctx, key)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TDigestCreate indicates an expected call of TDigestCreate.
func (mr *MockRedisMockRecorder) TDigestCreate(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestCreate", reflect.TypeOf((*MockRedis)(nil).TDigestCreate), ctx, key)
}
// TDigestCreateWithCompression mocks base method.
func (m *MockRedis) TDigestCreateWithCompression(ctx context.Context, key string, compression int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestCreateWithCompression", ctx, key, compression)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TDigestCreateWithCompression indicates an expected call of TDigestCreateWithCompression.
func (mr *MockRedisMockRecorder) TDigestCreateWithCompression(ctx, key, compression any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestCreateWithCompression", reflect.TypeOf((*MockRedis)(nil).TDigestCreateWithCompression), ctx, key, compression)
}
// TDigestInfo mocks base method.
func (m *MockRedis) TDigestInfo(ctx context.Context, key string) *redis.TDigestInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestInfo", ctx, key)
ret0, _ := ret[0].(*redis.TDigestInfoCmd)
return ret0
}
// TDigestInfo indicates an expected call of TDigestInfo.
func (mr *MockRedisMockRecorder) TDigestInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestInfo", reflect.TypeOf((*MockRedis)(nil).TDigestInfo), ctx, key)
}
// TDigestMax mocks base method.
func (m *MockRedis) TDigestMax(ctx context.Context, key string) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestMax", ctx, key)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// TDigestMax indicates an expected call of TDigestMax.
func (mr *MockRedisMockRecorder) TDigestMax(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestMax", reflect.TypeOf((*MockRedis)(nil).TDigestMax), ctx, key)
}
// TDigestMerge mocks base method.
func (m *MockRedis) TDigestMerge(ctx context.Context, destKey string, options *redis.TDigestMergeOptions, sourceKeys ...string) *redis.StatusCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destKey, options}
for _, a := range sourceKeys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestMerge", varargs...)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TDigestMerge indicates an expected call of TDigestMerge.
func (mr *MockRedisMockRecorder) TDigestMerge(ctx, destKey, options any, sourceKeys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destKey, options}, sourceKeys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestMerge", reflect.TypeOf((*MockRedis)(nil).TDigestMerge), varargs...)
}
// TDigestMin mocks base method.
func (m *MockRedis) TDigestMin(ctx context.Context, key string) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestMin", ctx, key)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// TDigestMin indicates an expected call of TDigestMin.
func (mr *MockRedisMockRecorder) TDigestMin(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestMin", reflect.TypeOf((*MockRedis)(nil).TDigestMin), ctx, key)
}
// TDigestQuantile mocks base method.
func (m *MockRedis) TDigestQuantile(ctx context.Context, key string, elements ...float64) *redis.FloatSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestQuantile", varargs...)
ret0, _ := ret[0].(*redis.FloatSliceCmd)
return ret0
}
// TDigestQuantile indicates an expected call of TDigestQuantile.
func (mr *MockRedisMockRecorder) TDigestQuantile(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestQuantile", reflect.TypeOf((*MockRedis)(nil).TDigestQuantile), varargs...)
}
// TDigestRank mocks base method.
func (m *MockRedis) TDigestRank(ctx context.Context, key string, values ...float64) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestRank", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// TDigestRank indicates an expected call of TDigestRank.
func (mr *MockRedisMockRecorder) TDigestRank(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestRank", reflect.TypeOf((*MockRedis)(nil).TDigestRank), varargs...)
}
// TDigestReset mocks base method.
func (m *MockRedis) TDigestReset(ctx context.Context, key string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestReset", ctx, key)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TDigestReset indicates an expected call of TDigestReset.
func (mr *MockRedisMockRecorder) TDigestReset(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestReset", reflect.TypeOf((*MockRedis)(nil).TDigestReset), ctx, key)
}
// TDigestRevRank mocks base method.
func (m *MockRedis) TDigestRevRank(ctx context.Context, key string, values ...float64) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TDigestRevRank", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// TDigestRevRank indicates an expected call of TDigestRevRank.
func (mr *MockRedisMockRecorder) TDigestRevRank(ctx, key any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestRevRank", reflect.TypeOf((*MockRedis)(nil).TDigestRevRank), varargs...)
}
// TDigestTrimmedMean mocks base method.
func (m *MockRedis) TDigestTrimmedMean(ctx context.Context, key string, lowCutQuantile, highCutQuantile float64) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TDigestTrimmedMean", ctx, key, lowCutQuantile, highCutQuantile)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// TDigestTrimmedMean indicates an expected call of TDigestTrimmedMean.
func (mr *MockRedisMockRecorder) TDigestTrimmedMean(ctx, key, lowCutQuantile, highCutQuantile any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TDigestTrimmedMean", reflect.TypeOf((*MockRedis)(nil).TDigestTrimmedMean), ctx, key, lowCutQuantile, highCutQuantile)
}
// TSAdd mocks base method.
func (m *MockRedis) TSAdd(ctx context.Context, key string, timestamp any, value float64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSAdd", ctx, key, timestamp, value)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSAdd indicates an expected call of TSAdd.
func (mr *MockRedisMockRecorder) TSAdd(ctx, key, timestamp, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSAdd", reflect.TypeOf((*MockRedis)(nil).TSAdd), ctx, key, timestamp, value)
}
// TSAddWithArgs mocks base method.
func (m *MockRedis) TSAddWithArgs(ctx context.Context, key string, timestamp any, value float64, options *redis.TSOptions) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSAddWithArgs", ctx, key, timestamp, value, options)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSAddWithArgs indicates an expected call of TSAddWithArgs.
func (mr *MockRedisMockRecorder) TSAddWithArgs(ctx, key, timestamp, value, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSAddWithArgs", reflect.TypeOf((*MockRedis)(nil).TSAddWithArgs), ctx, key, timestamp, value, options)
}
// TSAlter mocks base method.
func (m *MockRedis) TSAlter(ctx context.Context, key string, options *redis.TSAlterOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSAlter", ctx, key, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSAlter indicates an expected call of TSAlter.
func (mr *MockRedisMockRecorder) TSAlter(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSAlter", reflect.TypeOf((*MockRedis)(nil).TSAlter), ctx, key, options)
}
// TSCreate mocks base method.
func (m *MockRedis) TSCreate(ctx context.Context, key string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSCreate", ctx, key)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSCreate indicates an expected call of TSCreate.
func (mr *MockRedisMockRecorder) TSCreate(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSCreate", reflect.TypeOf((*MockRedis)(nil).TSCreate), ctx, key)
}
// TSCreateRule mocks base method.
func (m *MockRedis) TSCreateRule(ctx context.Context, sourceKey, destKey string, aggregator redis.Aggregator, bucketDuration int) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSCreateRule", ctx, sourceKey, destKey, aggregator, bucketDuration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSCreateRule indicates an expected call of TSCreateRule.
func (mr *MockRedisMockRecorder) TSCreateRule(ctx, sourceKey, destKey, aggregator, bucketDuration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSCreateRule", reflect.TypeOf((*MockRedis)(nil).TSCreateRule), ctx, sourceKey, destKey, aggregator, bucketDuration)
}
// TSCreateRuleWithArgs mocks base method.
func (m *MockRedis) TSCreateRuleWithArgs(ctx context.Context, sourceKey, destKey string, aggregator redis.Aggregator, bucketDuration int, options *redis.TSCreateRuleOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSCreateRuleWithArgs", ctx, sourceKey, destKey, aggregator, bucketDuration, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSCreateRuleWithArgs indicates an expected call of TSCreateRuleWithArgs.
func (mr *MockRedisMockRecorder) TSCreateRuleWithArgs(ctx, sourceKey, destKey, aggregator, bucketDuration, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSCreateRuleWithArgs", reflect.TypeOf((*MockRedis)(nil).TSCreateRuleWithArgs), ctx, sourceKey, destKey, aggregator, bucketDuration, options)
}
// TSCreateWithArgs mocks base method.
func (m *MockRedis) TSCreateWithArgs(ctx context.Context, key string, options *redis.TSOptions) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSCreateWithArgs", ctx, key, options)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSCreateWithArgs indicates an expected call of TSCreateWithArgs.
func (mr *MockRedisMockRecorder) TSCreateWithArgs(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSCreateWithArgs", reflect.TypeOf((*MockRedis)(nil).TSCreateWithArgs), ctx, key, options)
}
// TSDecrBy mocks base method.
func (m *MockRedis) TSDecrBy(ctx context.Context, Key string, timestamp float64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSDecrBy", ctx, Key, timestamp)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSDecrBy indicates an expected call of TSDecrBy.
func (mr *MockRedisMockRecorder) TSDecrBy(ctx, Key, timestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDecrBy", reflect.TypeOf((*MockRedis)(nil).TSDecrBy), ctx, Key, timestamp)
}
// TSDecrByWithArgs mocks base method.
func (m *MockRedis) TSDecrByWithArgs(ctx context.Context, key string, timestamp float64, options *redis.TSIncrDecrOptions) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSDecrByWithArgs", ctx, key, timestamp, options)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSDecrByWithArgs indicates an expected call of TSDecrByWithArgs.
func (mr *MockRedisMockRecorder) TSDecrByWithArgs(ctx, key, timestamp, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDecrByWithArgs", reflect.TypeOf((*MockRedis)(nil).TSDecrByWithArgs), ctx, key, timestamp, options)
}
// TSDel mocks base method.
func (m *MockRedis) TSDel(ctx context.Context, Key string, fromTimestamp, toTimestamp int) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSDel", ctx, Key, fromTimestamp, toTimestamp)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSDel indicates an expected call of TSDel.
func (mr *MockRedisMockRecorder) TSDel(ctx, Key, fromTimestamp, toTimestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDel", reflect.TypeOf((*MockRedis)(nil).TSDel), ctx, Key, fromTimestamp, toTimestamp)
}
// TSDeleteRule mocks base method.
func (m *MockRedis) TSDeleteRule(ctx context.Context, sourceKey, destKey string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSDeleteRule", ctx, sourceKey, destKey)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TSDeleteRule indicates an expected call of TSDeleteRule.
func (mr *MockRedisMockRecorder) TSDeleteRule(ctx, sourceKey, destKey any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSDeleteRule", reflect.TypeOf((*MockRedis)(nil).TSDeleteRule), ctx, sourceKey, destKey)
}
// TSGet mocks base method.
func (m *MockRedis) TSGet(ctx context.Context, key string) *redis.TSTimestampValueCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSGet", ctx, key)
ret0, _ := ret[0].(*redis.TSTimestampValueCmd)
return ret0
}
// TSGet indicates an expected call of TSGet.
func (mr *MockRedisMockRecorder) TSGet(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSGet", reflect.TypeOf((*MockRedis)(nil).TSGet), ctx, key)
}
// TSGetWithArgs mocks base method.
func (m *MockRedis) TSGetWithArgs(ctx context.Context, key string, options *redis.TSGetOptions) *redis.TSTimestampValueCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSGetWithArgs", ctx, key, options)
ret0, _ := ret[0].(*redis.TSTimestampValueCmd)
return ret0
}
// TSGetWithArgs indicates an expected call of TSGetWithArgs.
func (mr *MockRedisMockRecorder) TSGetWithArgs(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSGetWithArgs", reflect.TypeOf((*MockRedis)(nil).TSGetWithArgs), ctx, key, options)
}
// TSIncrBy mocks base method.
func (m *MockRedis) TSIncrBy(ctx context.Context, Key string, timestamp float64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSIncrBy", ctx, Key, timestamp)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSIncrBy indicates an expected call of TSIncrBy.
func (mr *MockRedisMockRecorder) TSIncrBy(ctx, Key, timestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSIncrBy", reflect.TypeOf((*MockRedis)(nil).TSIncrBy), ctx, Key, timestamp)
}
// TSIncrByWithArgs mocks base method.
func (m *MockRedis) TSIncrByWithArgs(ctx context.Context, key string, timestamp float64, options *redis.TSIncrDecrOptions) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSIncrByWithArgs", ctx, key, timestamp, options)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// TSIncrByWithArgs indicates an expected call of TSIncrByWithArgs.
func (mr *MockRedisMockRecorder) TSIncrByWithArgs(ctx, key, timestamp, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSIncrByWithArgs", reflect.TypeOf((*MockRedis)(nil).TSIncrByWithArgs), ctx, key, timestamp, options)
}
// TSInfo mocks base method.
func (m *MockRedis) TSInfo(ctx context.Context, key string) *redis.MapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSInfo", ctx, key)
ret0, _ := ret[0].(*redis.MapStringInterfaceCmd)
return ret0
}
// TSInfo indicates an expected call of TSInfo.
func (mr *MockRedisMockRecorder) TSInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSInfo", reflect.TypeOf((*MockRedis)(nil).TSInfo), ctx, key)
}
// TSInfoWithArgs mocks base method.
func (m *MockRedis) TSInfoWithArgs(ctx context.Context, key string, options *redis.TSInfoOptions) *redis.MapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSInfoWithArgs", ctx, key, options)
ret0, _ := ret[0].(*redis.MapStringInterfaceCmd)
return ret0
}
// TSInfoWithArgs indicates an expected call of TSInfoWithArgs.
func (mr *MockRedisMockRecorder) TSInfoWithArgs(ctx, key, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSInfoWithArgs", reflect.TypeOf((*MockRedis)(nil).TSInfoWithArgs), ctx, key, options)
}
// TSMAdd mocks base method.
func (m *MockRedis) TSMAdd(ctx context.Context, ktvSlices [][]any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMAdd", ctx, ktvSlices)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// TSMAdd indicates an expected call of TSMAdd.
func (mr *MockRedisMockRecorder) TSMAdd(ctx, ktvSlices any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMAdd", reflect.TypeOf((*MockRedis)(nil).TSMAdd), ctx, ktvSlices)
}
// TSMGet mocks base method.
func (m *MockRedis) TSMGet(ctx context.Context, filters []string) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMGet", ctx, filters)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMGet indicates an expected call of TSMGet.
func (mr *MockRedisMockRecorder) TSMGet(ctx, filters any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMGet", reflect.TypeOf((*MockRedis)(nil).TSMGet), ctx, filters)
}
// TSMGetWithArgs mocks base method.
func (m *MockRedis) TSMGetWithArgs(ctx context.Context, filters []string, options *redis.TSMGetOptions) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMGetWithArgs", ctx, filters, options)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMGetWithArgs indicates an expected call of TSMGetWithArgs.
func (mr *MockRedisMockRecorder) TSMGetWithArgs(ctx, filters, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMGetWithArgs", reflect.TypeOf((*MockRedis)(nil).TSMGetWithArgs), ctx, filters, options)
}
// TSMRange mocks base method.
func (m *MockRedis) TSMRange(ctx context.Context, fromTimestamp, toTimestamp int, filterExpr []string) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMRange", ctx, fromTimestamp, toTimestamp, filterExpr)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMRange indicates an expected call of TSMRange.
func (mr *MockRedisMockRecorder) TSMRange(ctx, fromTimestamp, toTimestamp, filterExpr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMRange", reflect.TypeOf((*MockRedis)(nil).TSMRange), ctx, fromTimestamp, toTimestamp, filterExpr)
}
// TSMRangeWithArgs mocks base method.
func (m *MockRedis) TSMRangeWithArgs(ctx context.Context, fromTimestamp, toTimestamp int, filterExpr []string, options *redis.TSMRangeOptions) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMRangeWithArgs", ctx, fromTimestamp, toTimestamp, filterExpr, options)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMRangeWithArgs indicates an expected call of TSMRangeWithArgs.
func (mr *MockRedisMockRecorder) TSMRangeWithArgs(ctx, fromTimestamp, toTimestamp, filterExpr, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMRangeWithArgs", reflect.TypeOf((*MockRedis)(nil).TSMRangeWithArgs), ctx, fromTimestamp, toTimestamp, filterExpr, options)
}
// TSMRevRange mocks base method.
func (m *MockRedis) TSMRevRange(ctx context.Context, fromTimestamp, toTimestamp int, filterExpr []string) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMRevRange", ctx, fromTimestamp, toTimestamp, filterExpr)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMRevRange indicates an expected call of TSMRevRange.
func (mr *MockRedisMockRecorder) TSMRevRange(ctx, fromTimestamp, toTimestamp, filterExpr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMRevRange", reflect.TypeOf((*MockRedis)(nil).TSMRevRange), ctx, fromTimestamp, toTimestamp, filterExpr)
}
// TSMRevRangeWithArgs mocks base method.
func (m *MockRedis) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp, toTimestamp int, filterExpr []string, options *redis.TSMRevRangeOptions) *redis.MapStringSliceInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSMRevRangeWithArgs", ctx, fromTimestamp, toTimestamp, filterExpr, options)
ret0, _ := ret[0].(*redis.MapStringSliceInterfaceCmd)
return ret0
}
// TSMRevRangeWithArgs indicates an expected call of TSMRevRangeWithArgs.
func (mr *MockRedisMockRecorder) TSMRevRangeWithArgs(ctx, fromTimestamp, toTimestamp, filterExpr, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSMRevRangeWithArgs", reflect.TypeOf((*MockRedis)(nil).TSMRevRangeWithArgs), ctx, fromTimestamp, toTimestamp, filterExpr, options)
}
// TSQueryIndex mocks base method.
func (m *MockRedis) TSQueryIndex(ctx context.Context, filterExpr []string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSQueryIndex", ctx, filterExpr)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// TSQueryIndex indicates an expected call of TSQueryIndex.
func (mr *MockRedisMockRecorder) TSQueryIndex(ctx, filterExpr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSQueryIndex", reflect.TypeOf((*MockRedis)(nil).TSQueryIndex), ctx, filterExpr)
}
// TSRange mocks base method.
func (m *MockRedis) TSRange(ctx context.Context, key string, fromTimestamp, toTimestamp int) *redis.TSTimestampValueSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSRange", ctx, key, fromTimestamp, toTimestamp)
ret0, _ := ret[0].(*redis.TSTimestampValueSliceCmd)
return ret0
}
// TSRange indicates an expected call of TSRange.
func (mr *MockRedisMockRecorder) TSRange(ctx, key, fromTimestamp, toTimestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSRange", reflect.TypeOf((*MockRedis)(nil).TSRange), ctx, key, fromTimestamp, toTimestamp)
}
// TSRangeWithArgs mocks base method.
func (m *MockRedis) TSRangeWithArgs(ctx context.Context, key string, fromTimestamp, toTimestamp int, options *redis.TSRangeOptions) *redis.TSTimestampValueSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSRangeWithArgs", ctx, key, fromTimestamp, toTimestamp, options)
ret0, _ := ret[0].(*redis.TSTimestampValueSliceCmd)
return ret0
}
// TSRangeWithArgs indicates an expected call of TSRangeWithArgs.
func (mr *MockRedisMockRecorder) TSRangeWithArgs(ctx, key, fromTimestamp, toTimestamp, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSRangeWithArgs", reflect.TypeOf((*MockRedis)(nil).TSRangeWithArgs), ctx, key, fromTimestamp, toTimestamp, options)
}
// TSRevRange mocks base method.
func (m *MockRedis) TSRevRange(ctx context.Context, key string, fromTimestamp, toTimestamp int) *redis.TSTimestampValueSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSRevRange", ctx, key, fromTimestamp, toTimestamp)
ret0, _ := ret[0].(*redis.TSTimestampValueSliceCmd)
return ret0
}
// TSRevRange indicates an expected call of TSRevRange.
func (mr *MockRedisMockRecorder) TSRevRange(ctx, key, fromTimestamp, toTimestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSRevRange", reflect.TypeOf((*MockRedis)(nil).TSRevRange), ctx, key, fromTimestamp, toTimestamp)
}
// TSRevRangeWithArgs mocks base method.
func (m *MockRedis) TSRevRangeWithArgs(ctx context.Context, key string, fromTimestamp, toTimestamp int, options *redis.TSRevRangeOptions) *redis.TSTimestampValueSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TSRevRangeWithArgs", ctx, key, fromTimestamp, toTimestamp, options)
ret0, _ := ret[0].(*redis.TSTimestampValueSliceCmd)
return ret0
}
// TSRevRangeWithArgs indicates an expected call of TSRevRangeWithArgs.
func (mr *MockRedisMockRecorder) TSRevRangeWithArgs(ctx, key, fromTimestamp, toTimestamp, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TSRevRangeWithArgs", reflect.TypeOf((*MockRedis)(nil).TSRevRangeWithArgs), ctx, key, fromTimestamp, toTimestamp, options)
}
// TTL mocks base method.
func (m *MockRedis) TTL(ctx context.Context, key string) *redis.DurationCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TTL", ctx, key)
ret0, _ := ret[0].(*redis.DurationCmd)
return ret0
}
// TTL indicates an expected call of TTL.
func (mr *MockRedisMockRecorder) TTL(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TTL", reflect.TypeOf((*MockRedis)(nil).TTL), ctx, key)
}
// Time mocks base method.
func (m *MockRedis) Time(ctx context.Context) *redis.TimeCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Time", ctx)
ret0, _ := ret[0].(*redis.TimeCmd)
return ret0
}
// Time indicates an expected call of Time.
func (mr *MockRedisMockRecorder) Time(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Time", reflect.TypeOf((*MockRedis)(nil).Time), ctx)
}
// TopKAdd mocks base method.
func (m *MockRedis) TopKAdd(ctx context.Context, key string, elements ...any) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TopKAdd", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// TopKAdd indicates an expected call of TopKAdd.
func (mr *MockRedisMockRecorder) TopKAdd(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKAdd", reflect.TypeOf((*MockRedis)(nil).TopKAdd), varargs...)
}
// TopKCount mocks base method.
func (m *MockRedis) TopKCount(ctx context.Context, key string, elements ...any) *redis.IntSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TopKCount", varargs...)
ret0, _ := ret[0].(*redis.IntSliceCmd)
return ret0
}
// TopKCount indicates an expected call of TopKCount.
func (mr *MockRedisMockRecorder) TopKCount(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKCount", reflect.TypeOf((*MockRedis)(nil).TopKCount), varargs...)
}
// TopKIncrBy mocks base method.
func (m *MockRedis) TopKIncrBy(ctx context.Context, key string, elements ...any) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TopKIncrBy", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// TopKIncrBy indicates an expected call of TopKIncrBy.
func (mr *MockRedisMockRecorder) TopKIncrBy(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKIncrBy", reflect.TypeOf((*MockRedis)(nil).TopKIncrBy), varargs...)
}
// TopKInfo mocks base method.
func (m *MockRedis) TopKInfo(ctx context.Context, key string) *redis.TopKInfoCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TopKInfo", ctx, key)
ret0, _ := ret[0].(*redis.TopKInfoCmd)
return ret0
}
// TopKInfo indicates an expected call of TopKInfo.
func (mr *MockRedisMockRecorder) TopKInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKInfo", reflect.TypeOf((*MockRedis)(nil).TopKInfo), ctx, key)
}
// TopKList mocks base method.
func (m *MockRedis) TopKList(ctx context.Context, key string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TopKList", ctx, key)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// TopKList indicates an expected call of TopKList.
func (mr *MockRedisMockRecorder) TopKList(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKList", reflect.TypeOf((*MockRedis)(nil).TopKList), ctx, key)
}
// TopKListWithCount mocks base method.
func (m *MockRedis) TopKListWithCount(ctx context.Context, key string) *redis.MapStringIntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TopKListWithCount", ctx, key)
ret0, _ := ret[0].(*redis.MapStringIntCmd)
return ret0
}
// TopKListWithCount indicates an expected call of TopKListWithCount.
func (mr *MockRedisMockRecorder) TopKListWithCount(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKListWithCount", reflect.TypeOf((*MockRedis)(nil).TopKListWithCount), ctx, key)
}
// TopKQuery mocks base method.
func (m *MockRedis) TopKQuery(ctx context.Context, key string, elements ...any) *redis.BoolSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range elements {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "TopKQuery", varargs...)
ret0, _ := ret[0].(*redis.BoolSliceCmd)
return ret0
}
// TopKQuery indicates an expected call of TopKQuery.
func (mr *MockRedisMockRecorder) TopKQuery(ctx, key any, elements ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, elements...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKQuery", reflect.TypeOf((*MockRedis)(nil).TopKQuery), varargs...)
}
// TopKReserve mocks base method.
func (m *MockRedis) TopKReserve(ctx context.Context, key string, k int64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TopKReserve", ctx, key, k)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TopKReserve indicates an expected call of TopKReserve.
func (mr *MockRedisMockRecorder) TopKReserve(ctx, key, k any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKReserve", reflect.TypeOf((*MockRedis)(nil).TopKReserve), ctx, key, k)
}
// TopKReserveWithOptions mocks base method.
func (m *MockRedis) TopKReserveWithOptions(ctx context.Context, key string, k, width, depth int64, decay float64) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TopKReserveWithOptions", ctx, key, k, width, depth, decay)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// TopKReserveWithOptions indicates an expected call of TopKReserveWithOptions.
func (mr *MockRedisMockRecorder) TopKReserveWithOptions(ctx, key, k, width, depth, decay any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TopKReserveWithOptions", reflect.TypeOf((*MockRedis)(nil).TopKReserveWithOptions), ctx, key, k, width, depth, decay)
}
// Touch mocks base method.
func (m *MockRedis) Touch(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Touch", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Touch indicates an expected call of Touch.
func (mr *MockRedisMockRecorder) Touch(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Touch", reflect.TypeOf((*MockRedis)(nil).Touch), varargs...)
}
// TxPipeline mocks base method.
func (m *MockRedis) TxPipeline() redis.Pipeliner {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TxPipeline")
ret0, _ := ret[0].(redis.Pipeliner)
return ret0
}
// TxPipeline indicates an expected call of TxPipeline.
func (mr *MockRedisMockRecorder) TxPipeline() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPipeline", reflect.TypeOf((*MockRedis)(nil).TxPipeline))
}
// TxPipelined mocks base method.
func (m *MockRedis) TxPipelined(ctx context.Context, fn func(redis.Pipeliner) error) ([]redis.Cmder, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TxPipelined", ctx, fn)
ret0, _ := ret[0].([]redis.Cmder)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TxPipelined indicates an expected call of TxPipelined.
func (mr *MockRedisMockRecorder) TxPipelined(ctx, fn any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxPipelined", reflect.TypeOf((*MockRedis)(nil).TxPipelined), ctx, fn)
}
// Type mocks base method.
func (m *MockRedis) Type(ctx context.Context, key string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Type", ctx, key)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Type indicates an expected call of Type.
func (mr *MockRedisMockRecorder) Type(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Type", reflect.TypeOf((*MockRedis)(nil).Type), ctx, key)
}
// Unlink mocks base method.
func (m *MockRedis) Unlink(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Unlink", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Unlink indicates an expected call of Unlink.
func (mr *MockRedisMockRecorder) Unlink(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlink", reflect.TypeOf((*MockRedis)(nil).Unlink), varargs...)
}
// VAdd mocks base method.
func (m *MockRedis) VAdd(ctx context.Context, key, element string, val redis.Vector) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VAdd", ctx, key, element, val)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// VAdd indicates an expected call of VAdd.
func (mr *MockRedisMockRecorder) VAdd(ctx, key, element, val any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VAdd", reflect.TypeOf((*MockRedis)(nil).VAdd), ctx, key, element, val)
}
// VAddWithArgs mocks base method.
func (m *MockRedis) VAddWithArgs(ctx context.Context, key, element string, val redis.Vector, addArgs *redis.VAddArgs) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VAddWithArgs", ctx, key, element, val, addArgs)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// VAddWithArgs indicates an expected call of VAddWithArgs.
func (mr *MockRedisMockRecorder) VAddWithArgs(ctx, key, element, val, addArgs any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VAddWithArgs", reflect.TypeOf((*MockRedis)(nil).VAddWithArgs), ctx, key, element, val, addArgs)
}
// VCard mocks base method.
func (m *MockRedis) VCard(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VCard", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// VCard indicates an expected call of VCard.
func (mr *MockRedisMockRecorder) VCard(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VCard", reflect.TypeOf((*MockRedis)(nil).VCard), ctx, key)
}
// VClearAttributes mocks base method.
func (m *MockRedis) VClearAttributes(ctx context.Context, key, element string) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VClearAttributes", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// VClearAttributes indicates an expected call of VClearAttributes.
func (mr *MockRedisMockRecorder) VClearAttributes(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VClearAttributes", reflect.TypeOf((*MockRedis)(nil).VClearAttributes), ctx, key, element)
}
// VDim mocks base method.
func (m *MockRedis) VDim(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VDim", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// VDim indicates an expected call of VDim.
func (mr *MockRedisMockRecorder) VDim(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VDim", reflect.TypeOf((*MockRedis)(nil).VDim), ctx, key)
}
// VEmb mocks base method.
func (m *MockRedis) VEmb(ctx context.Context, key, element string, raw bool) *redis.SliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VEmb", ctx, key, element, raw)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// VEmb indicates an expected call of VEmb.
func (mr *MockRedisMockRecorder) VEmb(ctx, key, element, raw any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VEmb", reflect.TypeOf((*MockRedis)(nil).VEmb), ctx, key, element, raw)
}
// VGetAttr mocks base method.
func (m *MockRedis) VGetAttr(ctx context.Context, key, element string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VGetAttr", ctx, key, element)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// VGetAttr indicates an expected call of VGetAttr.
func (mr *MockRedisMockRecorder) VGetAttr(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VGetAttr", reflect.TypeOf((*MockRedis)(nil).VGetAttr), ctx, key, element)
}
// VInfo mocks base method.
func (m *MockRedis) VInfo(ctx context.Context, key string) *redis.MapStringInterfaceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VInfo", ctx, key)
ret0, _ := ret[0].(*redis.MapStringInterfaceCmd)
return ret0
}
// VInfo indicates an expected call of VInfo.
func (mr *MockRedisMockRecorder) VInfo(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VInfo", reflect.TypeOf((*MockRedis)(nil).VInfo), ctx, key)
}
// VLinks mocks base method.
func (m *MockRedis) VLinks(ctx context.Context, key, element string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VLinks", ctx, key, element)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// VLinks indicates an expected call of VLinks.
func (mr *MockRedisMockRecorder) VLinks(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VLinks", reflect.TypeOf((*MockRedis)(nil).VLinks), ctx, key, element)
}
// VLinksWithScores mocks base method.
func (m *MockRedis) VLinksWithScores(ctx context.Context, key, element string) *redis.VectorScoreSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VLinksWithScores", ctx, key, element)
ret0, _ := ret[0].(*redis.VectorScoreSliceCmd)
return ret0
}
// VLinksWithScores indicates an expected call of VLinksWithScores.
func (mr *MockRedisMockRecorder) VLinksWithScores(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VLinksWithScores", reflect.TypeOf((*MockRedis)(nil).VLinksWithScores), ctx, key, element)
}
// VRandMember mocks base method.
func (m *MockRedis) VRandMember(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VRandMember", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// VRandMember indicates an expected call of VRandMember.
func (mr *MockRedisMockRecorder) VRandMember(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VRandMember", reflect.TypeOf((*MockRedis)(nil).VRandMember), ctx, key)
}
// VRandMemberCount mocks base method.
func (m *MockRedis) VRandMemberCount(ctx context.Context, key string, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VRandMemberCount", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// VRandMemberCount indicates an expected call of VRandMemberCount.
func (mr *MockRedisMockRecorder) VRandMemberCount(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VRandMemberCount", reflect.TypeOf((*MockRedis)(nil).VRandMemberCount), ctx, key, count)
}
// VRange mocks base method.
func (m *MockRedis) VRange(ctx context.Context, key, start, end string, count int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VRange", ctx, key, start, end, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// VRange indicates an expected call of VRange.
func (mr *MockRedisMockRecorder) VRange(ctx, key, start, end, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VRange", reflect.TypeOf((*MockRedis)(nil).VRange), ctx, key, start, end, count)
}
// VRem mocks base method.
func (m *MockRedis) VRem(ctx context.Context, key, element string) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VRem", ctx, key, element)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// VRem indicates an expected call of VRem.
func (mr *MockRedisMockRecorder) VRem(ctx, key, element any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VRem", reflect.TypeOf((*MockRedis)(nil).VRem), ctx, key, element)
}
// VSetAttr mocks base method.
func (m *MockRedis) VSetAttr(ctx context.Context, key, element string, attr any) *redis.BoolCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VSetAttr", ctx, key, element, attr)
ret0, _ := ret[0].(*redis.BoolCmd)
return ret0
}
// VSetAttr indicates an expected call of VSetAttr.
func (mr *MockRedisMockRecorder) VSetAttr(ctx, key, element, attr any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VSetAttr", reflect.TypeOf((*MockRedis)(nil).VSetAttr), ctx, key, element, attr)
}
// VSim mocks base method.
func (m *MockRedis) VSim(ctx context.Context, key string, val redis.Vector) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VSim", ctx, key, val)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// VSim indicates an expected call of VSim.
func (mr *MockRedisMockRecorder) VSim(ctx, key, val any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VSim", reflect.TypeOf((*MockRedis)(nil).VSim), ctx, key, val)
}
// VSimWithArgs mocks base method.
func (m *MockRedis) VSimWithArgs(ctx context.Context, key string, val redis.Vector, args *redis.VSimArgs) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VSimWithArgs", ctx, key, val, args)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// VSimWithArgs indicates an expected call of VSimWithArgs.
func (mr *MockRedisMockRecorder) VSimWithArgs(ctx, key, val, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VSimWithArgs", reflect.TypeOf((*MockRedis)(nil).VSimWithArgs), ctx, key, val, args)
}
// VSimWithArgsWithScores mocks base method.
func (m *MockRedis) VSimWithArgsWithScores(ctx context.Context, key string, val redis.Vector, args *redis.VSimArgs) *redis.VectorScoreSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VSimWithArgsWithScores", ctx, key, val, args)
ret0, _ := ret[0].(*redis.VectorScoreSliceCmd)
return ret0
}
// VSimWithArgsWithScores indicates an expected call of VSimWithArgsWithScores.
func (mr *MockRedisMockRecorder) VSimWithArgsWithScores(ctx, key, val, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VSimWithArgsWithScores", reflect.TypeOf((*MockRedis)(nil).VSimWithArgsWithScores), ctx, key, val, args)
}
// VSimWithScores mocks base method.
func (m *MockRedis) VSimWithScores(ctx context.Context, key string, val redis.Vector) *redis.VectorScoreSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VSimWithScores", ctx, key, val)
ret0, _ := ret[0].(*redis.VectorScoreSliceCmd)
return ret0
}
// VSimWithScores indicates an expected call of VSimWithScores.
func (mr *MockRedisMockRecorder) VSimWithScores(ctx, key, val any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VSimWithScores", reflect.TypeOf((*MockRedis)(nil).VSimWithScores), ctx, key, val)
}
// XAck mocks base method.
func (m *MockRedis) XAck(ctx context.Context, stream, group string, ids ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, stream, group}
for _, a := range ids {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "XAck", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XAck indicates an expected call of XAck.
func (mr *MockRedisMockRecorder) XAck(ctx, stream, group any, ids ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stream, group}, ids...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAck", reflect.TypeOf((*MockRedis)(nil).XAck), varargs...)
}
// XAckDel mocks base method.
func (m *MockRedis) XAckDel(ctx context.Context, stream, group, mode string, ids ...string) *redis.SliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, stream, group, mode}
for _, a := range ids {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "XAckDel", varargs...)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// XAckDel indicates an expected call of XAckDel.
func (mr *MockRedisMockRecorder) XAckDel(ctx, stream, group, mode any, ids ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stream, group, mode}, ids...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAckDel", reflect.TypeOf((*MockRedis)(nil).XAckDel), varargs...)
}
// XAdd mocks base method.
func (m *MockRedis) XAdd(ctx context.Context, a *redis.XAddArgs) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XAdd", ctx, a)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// XAdd indicates an expected call of XAdd.
func (mr *MockRedisMockRecorder) XAdd(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAdd", reflect.TypeOf((*MockRedis)(nil).XAdd), ctx, a)
}
// XAutoClaim mocks base method.
func (m *MockRedis) XAutoClaim(ctx context.Context, a *redis.XAutoClaimArgs) *redis.XAutoClaimCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XAutoClaim", ctx, a)
ret0, _ := ret[0].(*redis.XAutoClaimCmd)
return ret0
}
// XAutoClaim indicates an expected call of XAutoClaim.
func (mr *MockRedisMockRecorder) XAutoClaim(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAutoClaim", reflect.TypeOf((*MockRedis)(nil).XAutoClaim), ctx, a)
}
// XAutoClaimJustID mocks base method.
func (m *MockRedis) XAutoClaimJustID(ctx context.Context, a *redis.XAutoClaimArgs) *redis.XAutoClaimJustIDCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XAutoClaimJustID", ctx, a)
ret0, _ := ret[0].(*redis.XAutoClaimJustIDCmd)
return ret0
}
// XAutoClaimJustID indicates an expected call of XAutoClaimJustID.
func (mr *MockRedisMockRecorder) XAutoClaimJustID(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XAutoClaimJustID", reflect.TypeOf((*MockRedis)(nil).XAutoClaimJustID), ctx, a)
}
// XCfgSet mocks base method.
func (m *MockRedis) XCfgSet(ctx context.Context, a *redis.XCfgSetArgs) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XCfgSet", ctx, a)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// XCfgSet indicates an expected call of XCfgSet.
func (mr *MockRedisMockRecorder) XCfgSet(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XCfgSet", reflect.TypeOf((*MockRedis)(nil).XCfgSet), ctx, a)
}
// XClaim mocks base method.
func (m *MockRedis) XClaim(ctx context.Context, a *redis.XClaimArgs) *redis.XMessageSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XClaim", ctx, a)
ret0, _ := ret[0].(*redis.XMessageSliceCmd)
return ret0
}
// XClaim indicates an expected call of XClaim.
func (mr *MockRedisMockRecorder) XClaim(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XClaim", reflect.TypeOf((*MockRedis)(nil).XClaim), ctx, a)
}
// XClaimJustID mocks base method.
func (m *MockRedis) XClaimJustID(ctx context.Context, a *redis.XClaimArgs) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XClaimJustID", ctx, a)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// XClaimJustID indicates an expected call of XClaimJustID.
func (mr *MockRedisMockRecorder) XClaimJustID(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XClaimJustID", reflect.TypeOf((*MockRedis)(nil).XClaimJustID), ctx, a)
}
// XDel mocks base method.
func (m *MockRedis) XDel(ctx context.Context, stream string, ids ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, stream}
for _, a := range ids {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "XDel", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XDel indicates an expected call of XDel.
func (mr *MockRedisMockRecorder) XDel(ctx, stream any, ids ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stream}, ids...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XDel", reflect.TypeOf((*MockRedis)(nil).XDel), varargs...)
}
// XDelEx mocks base method.
func (m *MockRedis) XDelEx(ctx context.Context, stream, mode string, ids ...string) *redis.SliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, stream, mode}
for _, a := range ids {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "XDelEx", varargs...)
ret0, _ := ret[0].(*redis.SliceCmd)
return ret0
}
// XDelEx indicates an expected call of XDelEx.
func (mr *MockRedisMockRecorder) XDelEx(ctx, stream, mode any, ids ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stream, mode}, ids...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XDelEx", reflect.TypeOf((*MockRedis)(nil).XDelEx), varargs...)
}
// XGroupCreate mocks base method.
func (m *MockRedis) XGroupCreate(ctx context.Context, stream, group, start string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupCreate", ctx, stream, group, start)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// XGroupCreate indicates an expected call of XGroupCreate.
func (mr *MockRedisMockRecorder) XGroupCreate(ctx, stream, group, start any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreate", reflect.TypeOf((*MockRedis)(nil).XGroupCreate), ctx, stream, group, start)
}
// XGroupCreateConsumer mocks base method.
func (m *MockRedis) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupCreateConsumer", ctx, stream, group, consumer)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XGroupCreateConsumer indicates an expected call of XGroupCreateConsumer.
func (mr *MockRedisMockRecorder) XGroupCreateConsumer(ctx, stream, group, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreateConsumer", reflect.TypeOf((*MockRedis)(nil).XGroupCreateConsumer), ctx, stream, group, consumer)
}
// XGroupCreateMkStream mocks base method.
func (m *MockRedis) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupCreateMkStream", ctx, stream, group, start)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// XGroupCreateMkStream indicates an expected call of XGroupCreateMkStream.
func (mr *MockRedisMockRecorder) XGroupCreateMkStream(ctx, stream, group, start any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupCreateMkStream", reflect.TypeOf((*MockRedis)(nil).XGroupCreateMkStream), ctx, stream, group, start)
}
// XGroupDelConsumer mocks base method.
func (m *MockRedis) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupDelConsumer", ctx, stream, group, consumer)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XGroupDelConsumer indicates an expected call of XGroupDelConsumer.
func (mr *MockRedisMockRecorder) XGroupDelConsumer(ctx, stream, group, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupDelConsumer", reflect.TypeOf((*MockRedis)(nil).XGroupDelConsumer), ctx, stream, group, consumer)
}
// XGroupDestroy mocks base method.
func (m *MockRedis) XGroupDestroy(ctx context.Context, stream, group string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupDestroy", ctx, stream, group)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XGroupDestroy indicates an expected call of XGroupDestroy.
func (mr *MockRedisMockRecorder) XGroupDestroy(ctx, stream, group any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupDestroy", reflect.TypeOf((*MockRedis)(nil).XGroupDestroy), ctx, stream, group)
}
// XGroupSetID mocks base method.
func (m *MockRedis) XGroupSetID(ctx context.Context, stream, group, start string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XGroupSetID", ctx, stream, group, start)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// XGroupSetID indicates an expected call of XGroupSetID.
func (mr *MockRedisMockRecorder) XGroupSetID(ctx, stream, group, start any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XGroupSetID", reflect.TypeOf((*MockRedis)(nil).XGroupSetID), ctx, stream, group, start)
}
// XInfoConsumers mocks base method.
func (m *MockRedis) XInfoConsumers(ctx context.Context, key, group string) *redis.XInfoConsumersCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XInfoConsumers", ctx, key, group)
ret0, _ := ret[0].(*redis.XInfoConsumersCmd)
return ret0
}
// XInfoConsumers indicates an expected call of XInfoConsumers.
func (mr *MockRedisMockRecorder) XInfoConsumers(ctx, key, group any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoConsumers", reflect.TypeOf((*MockRedis)(nil).XInfoConsumers), ctx, key, group)
}
// XInfoGroups mocks base method.
func (m *MockRedis) XInfoGroups(ctx context.Context, key string) *redis.XInfoGroupsCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XInfoGroups", ctx, key)
ret0, _ := ret[0].(*redis.XInfoGroupsCmd)
return ret0
}
// XInfoGroups indicates an expected call of XInfoGroups.
func (mr *MockRedisMockRecorder) XInfoGroups(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoGroups", reflect.TypeOf((*MockRedis)(nil).XInfoGroups), ctx, key)
}
// XInfoStream mocks base method.
func (m *MockRedis) XInfoStream(ctx context.Context, key string) *redis.XInfoStreamCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XInfoStream", ctx, key)
ret0, _ := ret[0].(*redis.XInfoStreamCmd)
return ret0
}
// XInfoStream indicates an expected call of XInfoStream.
func (mr *MockRedisMockRecorder) XInfoStream(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoStream", reflect.TypeOf((*MockRedis)(nil).XInfoStream), ctx, key)
}
// XInfoStreamFull mocks base method.
func (m *MockRedis) XInfoStreamFull(ctx context.Context, key string, count int) *redis.XInfoStreamFullCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XInfoStreamFull", ctx, key, count)
ret0, _ := ret[0].(*redis.XInfoStreamFullCmd)
return ret0
}
// XInfoStreamFull indicates an expected call of XInfoStreamFull.
func (mr *MockRedisMockRecorder) XInfoStreamFull(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XInfoStreamFull", reflect.TypeOf((*MockRedis)(nil).XInfoStreamFull), ctx, key, count)
}
// XLen mocks base method.
func (m *MockRedis) XLen(ctx context.Context, stream string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XLen", ctx, stream)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XLen indicates an expected call of XLen.
func (mr *MockRedisMockRecorder) XLen(ctx, stream any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XLen", reflect.TypeOf((*MockRedis)(nil).XLen), ctx, stream)
}
// XPending mocks base method.
func (m *MockRedis) XPending(ctx context.Context, stream, group string) *redis.XPendingCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XPending", ctx, stream, group)
ret0, _ := ret[0].(*redis.XPendingCmd)
return ret0
}
// XPending indicates an expected call of XPending.
func (mr *MockRedisMockRecorder) XPending(ctx, stream, group any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XPending", reflect.TypeOf((*MockRedis)(nil).XPending), ctx, stream, group)
}
// XPendingExt mocks base method.
func (m *MockRedis) XPendingExt(ctx context.Context, a *redis.XPendingExtArgs) *redis.XPendingExtCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XPendingExt", ctx, a)
ret0, _ := ret[0].(*redis.XPendingExtCmd)
return ret0
}
// XPendingExt indicates an expected call of XPendingExt.
func (mr *MockRedisMockRecorder) XPendingExt(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XPendingExt", reflect.TypeOf((*MockRedis)(nil).XPendingExt), ctx, a)
}
// XRange mocks base method.
func (m *MockRedis) XRange(ctx context.Context, stream, start, stop string) *redis.XMessageSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XRange", ctx, stream, start, stop)
ret0, _ := ret[0].(*redis.XMessageSliceCmd)
return ret0
}
// XRange indicates an expected call of XRange.
func (mr *MockRedisMockRecorder) XRange(ctx, stream, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRange", reflect.TypeOf((*MockRedis)(nil).XRange), ctx, stream, start, stop)
}
// XRangeN mocks base method.
func (m *MockRedis) XRangeN(ctx context.Context, stream, start, stop string, count int64) *redis.XMessageSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XRangeN", ctx, stream, start, stop, count)
ret0, _ := ret[0].(*redis.XMessageSliceCmd)
return ret0
}
// XRangeN indicates an expected call of XRangeN.
func (mr *MockRedisMockRecorder) XRangeN(ctx, stream, start, stop, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRangeN", reflect.TypeOf((*MockRedis)(nil).XRangeN), ctx, stream, start, stop, count)
}
// XRead mocks base method.
func (m *MockRedis) XRead(ctx context.Context, a *redis.XReadArgs) *redis.XStreamSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XRead", ctx, a)
ret0, _ := ret[0].(*redis.XStreamSliceCmd)
return ret0
}
// XRead indicates an expected call of XRead.
func (mr *MockRedisMockRecorder) XRead(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRead", reflect.TypeOf((*MockRedis)(nil).XRead), ctx, a)
}
// XReadGroup mocks base method.
func (m *MockRedis) XReadGroup(ctx context.Context, a *redis.XReadGroupArgs) *redis.XStreamSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XReadGroup", ctx, a)
ret0, _ := ret[0].(*redis.XStreamSliceCmd)
return ret0
}
// XReadGroup indicates an expected call of XReadGroup.
func (mr *MockRedisMockRecorder) XReadGroup(ctx, a any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XReadGroup", reflect.TypeOf((*MockRedis)(nil).XReadGroup), ctx, a)
}
// XReadStreams mocks base method.
func (m *MockRedis) XReadStreams(ctx context.Context, streams ...string) *redis.XStreamSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range streams {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "XReadStreams", varargs...)
ret0, _ := ret[0].(*redis.XStreamSliceCmd)
return ret0
}
// XReadStreams indicates an expected call of XReadStreams.
func (mr *MockRedisMockRecorder) XReadStreams(ctx any, streams ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, streams...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XReadStreams", reflect.TypeOf((*MockRedis)(nil).XReadStreams), varargs...)
}
// XRevRange mocks base method.
func (m *MockRedis) XRevRange(ctx context.Context, stream, start, stop string) *redis.XMessageSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XRevRange", ctx, stream, start, stop)
ret0, _ := ret[0].(*redis.XMessageSliceCmd)
return ret0
}
// XRevRange indicates an expected call of XRevRange.
func (mr *MockRedisMockRecorder) XRevRange(ctx, stream, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRevRange", reflect.TypeOf((*MockRedis)(nil).XRevRange), ctx, stream, start, stop)
}
// XRevRangeN mocks base method.
func (m *MockRedis) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *redis.XMessageSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XRevRangeN", ctx, stream, start, stop, count)
ret0, _ := ret[0].(*redis.XMessageSliceCmd)
return ret0
}
// XRevRangeN indicates an expected call of XRevRangeN.
func (mr *MockRedisMockRecorder) XRevRangeN(ctx, stream, start, stop, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XRevRangeN", reflect.TypeOf((*MockRedis)(nil).XRevRangeN), ctx, stream, start, stop, count)
}
// XTrimMaxLen mocks base method.
func (m *MockRedis) XTrimMaxLen(ctx context.Context, key string, maxLen int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMaxLen", ctx, key, maxLen)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMaxLen indicates an expected call of XTrimMaxLen.
func (mr *MockRedisMockRecorder) XTrimMaxLen(ctx, key, maxLen any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLen", reflect.TypeOf((*MockRedis)(nil).XTrimMaxLen), ctx, key, maxLen)
}
// XTrimMaxLenApprox mocks base method.
func (m *MockRedis) XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMaxLenApprox", ctx, key, maxLen, limit)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMaxLenApprox indicates an expected call of XTrimMaxLenApprox.
func (mr *MockRedisMockRecorder) XTrimMaxLenApprox(ctx, key, maxLen, limit any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLenApprox", reflect.TypeOf((*MockRedis)(nil).XTrimMaxLenApprox), ctx, key, maxLen, limit)
}
// XTrimMaxLenApproxMode mocks base method.
func (m *MockRedis) XTrimMaxLenApproxMode(ctx context.Context, key string, maxLen, limit int64, mode string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMaxLenApproxMode", ctx, key, maxLen, limit, mode)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMaxLenApproxMode indicates an expected call of XTrimMaxLenApproxMode.
func (mr *MockRedisMockRecorder) XTrimMaxLenApproxMode(ctx, key, maxLen, limit, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLenApproxMode", reflect.TypeOf((*MockRedis)(nil).XTrimMaxLenApproxMode), ctx, key, maxLen, limit, mode)
}
// XTrimMaxLenMode mocks base method.
func (m *MockRedis) XTrimMaxLenMode(ctx context.Context, key string, maxLen int64, mode string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMaxLenMode", ctx, key, maxLen, mode)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMaxLenMode indicates an expected call of XTrimMaxLenMode.
func (mr *MockRedisMockRecorder) XTrimMaxLenMode(ctx, key, maxLen, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMaxLenMode", reflect.TypeOf((*MockRedis)(nil).XTrimMaxLenMode), ctx, key, maxLen, mode)
}
// XTrimMinID mocks base method.
func (m *MockRedis) XTrimMinID(ctx context.Context, key, minID string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMinID", ctx, key, minID)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMinID indicates an expected call of XTrimMinID.
func (mr *MockRedisMockRecorder) XTrimMinID(ctx, key, minID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinID", reflect.TypeOf((*MockRedis)(nil).XTrimMinID), ctx, key, minID)
}
// XTrimMinIDApprox mocks base method.
func (m *MockRedis) XTrimMinIDApprox(ctx context.Context, key, minID string, limit int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMinIDApprox", ctx, key, minID, limit)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMinIDApprox indicates an expected call of XTrimMinIDApprox.
func (mr *MockRedisMockRecorder) XTrimMinIDApprox(ctx, key, minID, limit any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinIDApprox", reflect.TypeOf((*MockRedis)(nil).XTrimMinIDApprox), ctx, key, minID, limit)
}
// XTrimMinIDApproxMode mocks base method.
func (m *MockRedis) XTrimMinIDApproxMode(ctx context.Context, key, minID string, limit int64, mode string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMinIDApproxMode", ctx, key, minID, limit, mode)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMinIDApproxMode indicates an expected call of XTrimMinIDApproxMode.
func (mr *MockRedisMockRecorder) XTrimMinIDApproxMode(ctx, key, minID, limit, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinIDApproxMode", reflect.TypeOf((*MockRedis)(nil).XTrimMinIDApproxMode), ctx, key, minID, limit, mode)
}
// XTrimMinIDMode mocks base method.
func (m *MockRedis) XTrimMinIDMode(ctx context.Context, key, minID, mode string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "XTrimMinIDMode", ctx, key, minID, mode)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// XTrimMinIDMode indicates an expected call of XTrimMinIDMode.
func (mr *MockRedisMockRecorder) XTrimMinIDMode(ctx, key, minID, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "XTrimMinIDMode", reflect.TypeOf((*MockRedis)(nil).XTrimMinIDMode), ctx, key, minID, mode)
}
// ZAdd mocks base method.
func (m *MockRedis) ZAdd(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZAdd", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAdd indicates an expected call of ZAdd.
func (mr *MockRedisMockRecorder) ZAdd(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAdd", reflect.TypeOf((*MockRedis)(nil).ZAdd), varargs...)
}
// ZAddArgs mocks base method.
func (m *MockRedis) ZAddArgs(ctx context.Context, key string, args redis.ZAddArgs) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZAddArgs", ctx, key, args)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAddArgs indicates an expected call of ZAddArgs.
func (mr *MockRedisMockRecorder) ZAddArgs(ctx, key, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddArgs", reflect.TypeOf((*MockRedis)(nil).ZAddArgs), ctx, key, args)
}
// ZAddArgsIncr mocks base method.
func (m *MockRedis) ZAddArgsIncr(ctx context.Context, key string, args redis.ZAddArgs) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZAddArgsIncr", ctx, key, args)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// ZAddArgsIncr indicates an expected call of ZAddArgsIncr.
func (mr *MockRedisMockRecorder) ZAddArgsIncr(ctx, key, args any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddArgsIncr", reflect.TypeOf((*MockRedis)(nil).ZAddArgsIncr), ctx, key, args)
}
// ZAddGT mocks base method.
func (m *MockRedis) ZAddGT(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZAddGT", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAddGT indicates an expected call of ZAddGT.
func (mr *MockRedisMockRecorder) ZAddGT(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddGT", reflect.TypeOf((*MockRedis)(nil).ZAddGT), varargs...)
}
// ZAddLT mocks base method.
func (m *MockRedis) ZAddLT(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZAddLT", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAddLT indicates an expected call of ZAddLT.
func (mr *MockRedisMockRecorder) ZAddLT(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddLT", reflect.TypeOf((*MockRedis)(nil).ZAddLT), varargs...)
}
// ZAddNX mocks base method.
func (m *MockRedis) ZAddNX(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZAddNX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAddNX indicates an expected call of ZAddNX.
func (mr *MockRedisMockRecorder) ZAddNX(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddNX", reflect.TypeOf((*MockRedis)(nil).ZAddNX), varargs...)
}
// ZAddXX mocks base method.
func (m *MockRedis) ZAddXX(ctx context.Context, key string, members ...redis.Z) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZAddXX", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZAddXX indicates an expected call of ZAddXX.
func (mr *MockRedisMockRecorder) ZAddXX(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZAddXX", reflect.TypeOf((*MockRedis)(nil).ZAddXX), varargs...)
}
// ZCard mocks base method.
func (m *MockRedis) ZCard(ctx context.Context, key string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZCard", ctx, key)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZCard indicates an expected call of ZCard.
func (mr *MockRedisMockRecorder) ZCard(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZCard", reflect.TypeOf((*MockRedis)(nil).ZCard), ctx, key)
}
// ZCount mocks base method.
func (m *MockRedis) ZCount(ctx context.Context, key, min, max string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZCount", ctx, key, min, max)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZCount indicates an expected call of ZCount.
func (mr *MockRedisMockRecorder) ZCount(ctx, key, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZCount", reflect.TypeOf((*MockRedis)(nil).ZCount), ctx, key, min, max)
}
// ZDiff mocks base method.
func (m *MockRedis) ZDiff(ctx context.Context, keys ...string) *redis.StringSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZDiff", varargs...)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZDiff indicates an expected call of ZDiff.
func (mr *MockRedisMockRecorder) ZDiff(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiff", reflect.TypeOf((*MockRedis)(nil).ZDiff), varargs...)
}
// ZDiffStore mocks base method.
func (m *MockRedis) ZDiffStore(ctx context.Context, destination string, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, destination}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZDiffStore", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZDiffStore indicates an expected call of ZDiffStore.
func (mr *MockRedisMockRecorder) ZDiffStore(ctx, destination any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, destination}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiffStore", reflect.TypeOf((*MockRedis)(nil).ZDiffStore), varargs...)
}
// ZDiffWithScores mocks base method.
func (m *MockRedis) ZDiffWithScores(ctx context.Context, keys ...string) *redis.ZSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZDiffWithScores", varargs...)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZDiffWithScores indicates an expected call of ZDiffWithScores.
func (mr *MockRedisMockRecorder) ZDiffWithScores(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZDiffWithScores", reflect.TypeOf((*MockRedis)(nil).ZDiffWithScores), varargs...)
}
// ZIncrBy mocks base method.
func (m *MockRedis) ZIncrBy(ctx context.Context, key string, increment float64, member string) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZIncrBy", ctx, key, increment, member)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// ZIncrBy indicates an expected call of ZIncrBy.
func (mr *MockRedisMockRecorder) ZIncrBy(ctx, key, increment, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZIncrBy", reflect.TypeOf((*MockRedis)(nil).ZIncrBy), ctx, key, increment, member)
}
// ZInter mocks base method.
func (m *MockRedis) ZInter(ctx context.Context, store *redis.ZStore) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZInter", ctx, store)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZInter indicates an expected call of ZInter.
func (mr *MockRedisMockRecorder) ZInter(ctx, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInter", reflect.TypeOf((*MockRedis)(nil).ZInter), ctx, store)
}
// ZInterCard mocks base method.
func (m *MockRedis) ZInterCard(ctx context.Context, limit int64, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, limit}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZInterCard", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZInterCard indicates an expected call of ZInterCard.
func (mr *MockRedisMockRecorder) ZInterCard(ctx, limit any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, limit}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterCard", reflect.TypeOf((*MockRedis)(nil).ZInterCard), varargs...)
}
// ZInterStore mocks base method.
func (m *MockRedis) ZInterStore(ctx context.Context, destination string, store *redis.ZStore) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZInterStore", ctx, destination, store)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZInterStore indicates an expected call of ZInterStore.
func (mr *MockRedisMockRecorder) ZInterStore(ctx, destination, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterStore", reflect.TypeOf((*MockRedis)(nil).ZInterStore), ctx, destination, store)
}
// ZInterWithScores mocks base method.
func (m *MockRedis) ZInterWithScores(ctx context.Context, store *redis.ZStore) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZInterWithScores", ctx, store)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZInterWithScores indicates an expected call of ZInterWithScores.
func (mr *MockRedisMockRecorder) ZInterWithScores(ctx, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZInterWithScores", reflect.TypeOf((*MockRedis)(nil).ZInterWithScores), ctx, store)
}
// ZLexCount mocks base method.
func (m *MockRedis) ZLexCount(ctx context.Context, key, min, max string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZLexCount", ctx, key, min, max)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZLexCount indicates an expected call of ZLexCount.
func (mr *MockRedisMockRecorder) ZLexCount(ctx, key, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZLexCount", reflect.TypeOf((*MockRedis)(nil).ZLexCount), ctx, key, min, max)
}
// ZMPop mocks base method.
func (m *MockRedis) ZMPop(ctx context.Context, order string, count int64, keys ...string) *redis.ZSliceWithKeyCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, order, count}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZMPop", varargs...)
ret0, _ := ret[0].(*redis.ZSliceWithKeyCmd)
return ret0
}
// ZMPop indicates an expected call of ZMPop.
func (mr *MockRedisMockRecorder) ZMPop(ctx, order, count any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, order, count}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZMPop", reflect.TypeOf((*MockRedis)(nil).ZMPop), varargs...)
}
// ZMScore mocks base method.
func (m *MockRedis) ZMScore(ctx context.Context, key string, members ...string) *redis.FloatSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZMScore", varargs...)
ret0, _ := ret[0].(*redis.FloatSliceCmd)
return ret0
}
// ZMScore indicates an expected call of ZMScore.
func (mr *MockRedisMockRecorder) ZMScore(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZMScore", reflect.TypeOf((*MockRedis)(nil).ZMScore), varargs...)
}
// ZPopMax mocks base method.
func (m *MockRedis) ZPopMax(ctx context.Context, key string, count ...int64) *redis.ZSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range count {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZPopMax", varargs...)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZPopMax indicates an expected call of ZPopMax.
func (mr *MockRedisMockRecorder) ZPopMax(ctx, key any, count ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, count...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZPopMax", reflect.TypeOf((*MockRedis)(nil).ZPopMax), varargs...)
}
// ZPopMin mocks base method.
func (m *MockRedis) ZPopMin(ctx context.Context, key string, count ...int64) *redis.ZSliceCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range count {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZPopMin", varargs...)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZPopMin indicates an expected call of ZPopMin.
func (mr *MockRedisMockRecorder) ZPopMin(ctx, key any, count ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, count...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZPopMin", reflect.TypeOf((*MockRedis)(nil).ZPopMin), varargs...)
}
// ZRandMember mocks base method.
func (m *MockRedis) ZRandMember(ctx context.Context, key string, count int) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRandMember", ctx, key, count)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRandMember indicates an expected call of ZRandMember.
func (mr *MockRedisMockRecorder) ZRandMember(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRandMember", reflect.TypeOf((*MockRedis)(nil).ZRandMember), ctx, key, count)
}
// ZRandMemberWithScores mocks base method.
func (m *MockRedis) ZRandMemberWithScores(ctx context.Context, key string, count int) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRandMemberWithScores", ctx, key, count)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRandMemberWithScores indicates an expected call of ZRandMemberWithScores.
func (mr *MockRedisMockRecorder) ZRandMemberWithScores(ctx, key, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRandMemberWithScores", reflect.TypeOf((*MockRedis)(nil).ZRandMemberWithScores), ctx, key, count)
}
// ZRange mocks base method.
func (m *MockRedis) ZRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRange", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRange indicates an expected call of ZRange.
func (mr *MockRedisMockRecorder) ZRange(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRange", reflect.TypeOf((*MockRedis)(nil).ZRange), ctx, key, start, stop)
}
// ZRangeArgs mocks base method.
func (m *MockRedis) ZRangeArgs(ctx context.Context, z redis.ZRangeArgs) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeArgs", ctx, z)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRangeArgs indicates an expected call of ZRangeArgs.
func (mr *MockRedisMockRecorder) ZRangeArgs(ctx, z any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeArgs", reflect.TypeOf((*MockRedis)(nil).ZRangeArgs), ctx, z)
}
// ZRangeArgsWithScores mocks base method.
func (m *MockRedis) ZRangeArgsWithScores(ctx context.Context, z redis.ZRangeArgs) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeArgsWithScores", ctx, z)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRangeArgsWithScores indicates an expected call of ZRangeArgsWithScores.
func (mr *MockRedisMockRecorder) ZRangeArgsWithScores(ctx, z any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeArgsWithScores", reflect.TypeOf((*MockRedis)(nil).ZRangeArgsWithScores), ctx, z)
}
// ZRangeByLex mocks base method.
func (m *MockRedis) ZRangeByLex(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeByLex", ctx, key, opt)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRangeByLex indicates an expected call of ZRangeByLex.
func (mr *MockRedisMockRecorder) ZRangeByLex(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByLex", reflect.TypeOf((*MockRedis)(nil).ZRangeByLex), ctx, key, opt)
}
// ZRangeByScore mocks base method.
func (m *MockRedis) ZRangeByScore(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeByScore", ctx, key, opt)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRangeByScore indicates an expected call of ZRangeByScore.
func (mr *MockRedisMockRecorder) ZRangeByScore(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByScore", reflect.TypeOf((*MockRedis)(nil).ZRangeByScore), ctx, key, opt)
}
// ZRangeByScoreWithScores mocks base method.
func (m *MockRedis) ZRangeByScoreWithScores(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeByScoreWithScores", ctx, key, opt)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRangeByScoreWithScores indicates an expected call of ZRangeByScoreWithScores.
func (mr *MockRedisMockRecorder) ZRangeByScoreWithScores(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeByScoreWithScores", reflect.TypeOf((*MockRedis)(nil).ZRangeByScoreWithScores), ctx, key, opt)
}
// ZRangeStore mocks base method.
func (m *MockRedis) ZRangeStore(ctx context.Context, dst string, z redis.ZRangeArgs) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeStore", ctx, dst, z)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRangeStore indicates an expected call of ZRangeStore.
func (mr *MockRedisMockRecorder) ZRangeStore(ctx, dst, z any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeStore", reflect.TypeOf((*MockRedis)(nil).ZRangeStore), ctx, dst, z)
}
// ZRangeWithScores mocks base method.
func (m *MockRedis) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRangeWithScores", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRangeWithScores indicates an expected call of ZRangeWithScores.
func (mr *MockRedisMockRecorder) ZRangeWithScores(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRangeWithScores", reflect.TypeOf((*MockRedis)(nil).ZRangeWithScores), ctx, key, start, stop)
}
// ZRank mocks base method.
func (m *MockRedis) ZRank(ctx context.Context, key, member string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRank", ctx, key, member)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRank indicates an expected call of ZRank.
func (mr *MockRedisMockRecorder) ZRank(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRank", reflect.TypeOf((*MockRedis)(nil).ZRank), ctx, key, member)
}
// ZRankWithScore mocks base method.
func (m *MockRedis) ZRankWithScore(ctx context.Context, key, member string) *redis.RankWithScoreCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRankWithScore", ctx, key, member)
ret0, _ := ret[0].(*redis.RankWithScoreCmd)
return ret0
}
// ZRankWithScore indicates an expected call of ZRankWithScore.
func (mr *MockRedisMockRecorder) ZRankWithScore(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRankWithScore", reflect.TypeOf((*MockRedis)(nil).ZRankWithScore), ctx, key, member)
}
// ZRem mocks base method.
func (m *MockRedis) ZRem(ctx context.Context, key string, members ...any) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx, key}
for _, a := range members {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ZRem", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRem indicates an expected call of ZRem.
func (mr *MockRedisMockRecorder) ZRem(ctx, key any, members ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, key}, members...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRem", reflect.TypeOf((*MockRedis)(nil).ZRem), varargs...)
}
// ZRemRangeByLex mocks base method.
func (m *MockRedis) ZRemRangeByLex(ctx context.Context, key, min, max string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRemRangeByLex", ctx, key, min, max)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRemRangeByLex indicates an expected call of ZRemRangeByLex.
func (mr *MockRedisMockRecorder) ZRemRangeByLex(ctx, key, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByLex", reflect.TypeOf((*MockRedis)(nil).ZRemRangeByLex), ctx, key, min, max)
}
// ZRemRangeByRank mocks base method.
func (m *MockRedis) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRemRangeByRank", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRemRangeByRank indicates an expected call of ZRemRangeByRank.
func (mr *MockRedisMockRecorder) ZRemRangeByRank(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByRank", reflect.TypeOf((*MockRedis)(nil).ZRemRangeByRank), ctx, key, start, stop)
}
// ZRemRangeByScore mocks base method.
func (m *MockRedis) ZRemRangeByScore(ctx context.Context, key, min, max string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRemRangeByScore", ctx, key, min, max)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRemRangeByScore indicates an expected call of ZRemRangeByScore.
func (mr *MockRedisMockRecorder) ZRemRangeByScore(ctx, key, min, max any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRemRangeByScore", reflect.TypeOf((*MockRedis)(nil).ZRemRangeByScore), ctx, key, min, max)
}
// ZRevRange mocks base method.
func (m *MockRedis) ZRevRange(ctx context.Context, key string, start, stop int64) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRange", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRevRange indicates an expected call of ZRevRange.
func (mr *MockRedisMockRecorder) ZRevRange(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRange", reflect.TypeOf((*MockRedis)(nil).ZRevRange), ctx, key, start, stop)
}
// ZRevRangeByLex mocks base method.
func (m *MockRedis) ZRevRangeByLex(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRangeByLex", ctx, key, opt)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRevRangeByLex indicates an expected call of ZRevRangeByLex.
func (mr *MockRedisMockRecorder) ZRevRangeByLex(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByLex", reflect.TypeOf((*MockRedis)(nil).ZRevRangeByLex), ctx, key, opt)
}
// ZRevRangeByScore mocks base method.
func (m *MockRedis) ZRevRangeByScore(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRangeByScore", ctx, key, opt)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZRevRangeByScore indicates an expected call of ZRevRangeByScore.
func (mr *MockRedisMockRecorder) ZRevRangeByScore(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByScore", reflect.TypeOf((*MockRedis)(nil).ZRevRangeByScore), ctx, key, opt)
}
// ZRevRangeByScoreWithScores mocks base method.
func (m *MockRedis) ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *redis.ZRangeBy) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRangeByScoreWithScores", ctx, key, opt)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRevRangeByScoreWithScores indicates an expected call of ZRevRangeByScoreWithScores.
func (mr *MockRedisMockRecorder) ZRevRangeByScoreWithScores(ctx, key, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeByScoreWithScores", reflect.TypeOf((*MockRedis)(nil).ZRevRangeByScoreWithScores), ctx, key, opt)
}
// ZRevRangeWithScores mocks base method.
func (m *MockRedis) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRangeWithScores", ctx, key, start, stop)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZRevRangeWithScores indicates an expected call of ZRevRangeWithScores.
func (mr *MockRedisMockRecorder) ZRevRangeWithScores(ctx, key, start, stop any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRangeWithScores", reflect.TypeOf((*MockRedis)(nil).ZRevRangeWithScores), ctx, key, start, stop)
}
// ZRevRank mocks base method.
func (m *MockRedis) ZRevRank(ctx context.Context, key, member string) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRank", ctx, key, member)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZRevRank indicates an expected call of ZRevRank.
func (mr *MockRedisMockRecorder) ZRevRank(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRank", reflect.TypeOf((*MockRedis)(nil).ZRevRank), ctx, key, member)
}
// ZRevRankWithScore mocks base method.
func (m *MockRedis) ZRevRankWithScore(ctx context.Context, key, member string) *redis.RankWithScoreCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZRevRankWithScore", ctx, key, member)
ret0, _ := ret[0].(*redis.RankWithScoreCmd)
return ret0
}
// ZRevRankWithScore indicates an expected call of ZRevRankWithScore.
func (mr *MockRedisMockRecorder) ZRevRankWithScore(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZRevRankWithScore", reflect.TypeOf((*MockRedis)(nil).ZRevRankWithScore), ctx, key, member)
}
// ZScan mocks base method.
func (m *MockRedis) ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *redis.ScanCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZScan", ctx, key, cursor, match, count)
ret0, _ := ret[0].(*redis.ScanCmd)
return ret0
}
// ZScan indicates an expected call of ZScan.
func (mr *MockRedisMockRecorder) ZScan(ctx, key, cursor, match, count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZScan", reflect.TypeOf((*MockRedis)(nil).ZScan), ctx, key, cursor, match, count)
}
// ZScore mocks base method.
func (m *MockRedis) ZScore(ctx context.Context, key, member string) *redis.FloatCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZScore", ctx, key, member)
ret0, _ := ret[0].(*redis.FloatCmd)
return ret0
}
// ZScore indicates an expected call of ZScore.
func (mr *MockRedisMockRecorder) ZScore(ctx, key, member any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZScore", reflect.TypeOf((*MockRedis)(nil).ZScore), ctx, key, member)
}
// ZUnion mocks base method.
func (m *MockRedis) ZUnion(ctx context.Context, store redis.ZStore) *redis.StringSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZUnion", ctx, store)
ret0, _ := ret[0].(*redis.StringSliceCmd)
return ret0
}
// ZUnion indicates an expected call of ZUnion.
func (mr *MockRedisMockRecorder) ZUnion(ctx, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnion", reflect.TypeOf((*MockRedis)(nil).ZUnion), ctx, store)
}
// ZUnionStore mocks base method.
func (m *MockRedis) ZUnionStore(ctx context.Context, dest string, store *redis.ZStore) *redis.IntCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZUnionStore", ctx, dest, store)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// ZUnionStore indicates an expected call of ZUnionStore.
func (mr *MockRedisMockRecorder) ZUnionStore(ctx, dest, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnionStore", reflect.TypeOf((*MockRedis)(nil).ZUnionStore), ctx, dest, store)
}
// ZUnionWithScores mocks base method.
func (m *MockRedis) ZUnionWithScores(ctx context.Context, store redis.ZStore) *redis.ZSliceCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ZUnionWithScores", ctx, store)
ret0, _ := ret[0].(*redis.ZSliceCmd)
return ret0
}
// ZUnionWithScores indicates an expected call of ZUnionWithScores.
func (mr *MockRedisMockRecorder) ZUnionWithScores(ctx, store any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ZUnionWithScores", reflect.TypeOf((*MockRedis)(nil).ZUnionWithScores), ctx, store)
}
// MockCassandra is a mock of Cassandra interface.
type MockCassandra struct {
ctrl *gomock.Controller
recorder *MockCassandraMockRecorder
isgomock struct{}
}
// MockCassandraMockRecorder is the mock recorder for MockCassandra.
type MockCassandraMockRecorder struct {
mock *MockCassandra
}
// NewMockCassandra creates a new mock instance.
func NewMockCassandra(ctrl *gomock.Controller) *MockCassandra {
mock := &MockCassandra{ctrl: ctrl}
mock.recorder = &MockCassandraMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandra) EXPECT() *MockCassandraMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockCassandra) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockCassandraMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockCassandra)(nil).BatchQuery), varargs...)
}
// Exec mocks base method.
func (m *MockCassandra) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockCassandraMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandra)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockCassandra) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockCassandraMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandra)(nil).ExecCAS), varargs...)
}
// ExecuteBatch mocks base method.
func (m *MockCassandra) ExecuteBatch(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatch", name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatch indicates an expected call of ExecuteBatch.
func (mr *MockCassandraMockRecorder) ExecuteBatch(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatch", reflect.TypeOf((*MockCassandra)(nil).ExecuteBatch), name)
}
// ExecuteBatchCAS mocks base method.
func (m *MockCassandra) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCAS indicates an expected call of ExecuteBatchCAS.
func (mr *MockCassandraMockRecorder) ExecuteBatchCAS(name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCAS", reflect.TypeOf((*MockCassandra)(nil).ExecuteBatchCAS), varargs...)
}
// HealthCheck mocks base method.
func (m *MockCassandra) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCassandraMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandra)(nil).HealthCheck), arg0)
}
// NewBatch mocks base method.
func (m *MockCassandra) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockCassandraMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockCassandra)(nil).NewBatch), name, batchType)
}
// Query mocks base method.
func (m *MockCassandra) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCassandraMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandra)(nil).Query), varargs...)
}
// MockCassandraBatch is a mock of CassandraBatch interface.
type MockCassandraBatch struct {
ctrl *gomock.Controller
recorder *MockCassandraBatchMockRecorder
isgomock struct{}
}
// MockCassandraBatchMockRecorder is the mock recorder for MockCassandraBatch.
type MockCassandraBatchMockRecorder struct {
mock *MockCassandraBatch
}
// NewMockCassandraBatch creates a new mock instance.
func NewMockCassandraBatch(ctrl *gomock.Controller) *MockCassandraBatch {
mock := &MockCassandraBatch{ctrl: ctrl}
mock.recorder = &MockCassandraBatchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandraBatch) EXPECT() *MockCassandraBatchMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockCassandraBatch) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockCassandraBatchMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockCassandraBatch)(nil).BatchQuery), varargs...)
}
// ExecuteBatch mocks base method.
func (m *MockCassandraBatch) ExecuteBatch(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatch", name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatch indicates an expected call of ExecuteBatch.
func (mr *MockCassandraBatchMockRecorder) ExecuteBatch(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatch", reflect.TypeOf((*MockCassandraBatch)(nil).ExecuteBatch), name)
}
// ExecuteBatchCAS mocks base method.
func (m *MockCassandraBatch) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCAS indicates an expected call of ExecuteBatchCAS.
func (mr *MockCassandraBatchMockRecorder) ExecuteBatchCAS(name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCAS", reflect.TypeOf((*MockCassandraBatch)(nil).ExecuteBatchCAS), varargs...)
}
// MockCassandraWithContext is a mock of CassandraWithContext interface.
type MockCassandraWithContext struct {
ctrl *gomock.Controller
recorder *MockCassandraWithContextMockRecorder
isgomock struct{}
}
// MockCassandraWithContextMockRecorder is the mock recorder for MockCassandraWithContext.
type MockCassandraWithContextMockRecorder struct {
mock *MockCassandraWithContext
}
// NewMockCassandraWithContext creates a new mock instance.
func NewMockCassandraWithContext(ctrl *gomock.Controller) *MockCassandraWithContext {
mock := &MockCassandraWithContext{ctrl: ctrl}
mock.recorder = &MockCassandraWithContextMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandraWithContext) EXPECT() *MockCassandraWithContextMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockCassandraWithContext) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockCassandraWithContextMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockCassandraWithContext)(nil).BatchQuery), varargs...)
}
// BatchQueryWithCtx mocks base method.
func (m *MockCassandraWithContext) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockCassandraWithContextMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).BatchQueryWithCtx), varargs...)
}
// Exec mocks base method.
func (m *MockCassandraWithContext) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockCassandraWithContextMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandraWithContext)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockCassandraWithContext) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockCassandraWithContextMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecCAS), varargs...)
}
// ExecCASWithCtx mocks base method.
func (m *MockCassandraWithContext) ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCASWithCtx", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCASWithCtx indicates an expected call of ExecCASWithCtx.
func (mr *MockCassandraWithContextMockRecorder) ExecCASWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCASWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecCASWithCtx), varargs...)
}
// ExecWithCtx mocks base method.
func (m *MockCassandraWithContext) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecWithCtx indicates an expected call of ExecWithCtx.
func (mr *MockCassandraWithContextMockRecorder) ExecWithCtx(ctx, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecWithCtx), varargs...)
}
// ExecuteBatch mocks base method.
func (m *MockCassandraWithContext) ExecuteBatch(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatch", name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatch indicates an expected call of ExecuteBatch.
func (mr *MockCassandraWithContextMockRecorder) ExecuteBatch(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatch", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecuteBatch), name)
}
// ExecuteBatchCAS mocks base method.
func (m *MockCassandraWithContext) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCAS indicates an expected call of ExecuteBatchCAS.
func (mr *MockCassandraWithContextMockRecorder) ExecuteBatchCAS(name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCAS", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecuteBatchCAS), varargs...)
}
// ExecuteBatchCASWithCtx mocks base method.
func (m *MockCassandraWithContext) ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCASWithCtx", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCASWithCtx indicates an expected call of ExecuteBatchCASWithCtx.
func (mr *MockCassandraWithContextMockRecorder) ExecuteBatchCASWithCtx(ctx, name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCASWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecuteBatchCASWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockCassandraWithContext) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockCassandraWithContextMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).ExecuteBatchWithCtx), ctx, name)
}
// HealthCheck mocks base method.
func (m *MockCassandraWithContext) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCassandraWithContextMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandraWithContext)(nil).HealthCheck), arg0)
}
// NewBatch mocks base method.
func (m *MockCassandraWithContext) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockCassandraWithContextMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockCassandraWithContext)(nil).NewBatch), name, batchType)
}
// NewBatchWithCtx mocks base method.
func (m *MockCassandraWithContext) NewBatchWithCtx(ctx context.Context, name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatchWithCtx", ctx, name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatchWithCtx indicates an expected call of NewBatchWithCtx.
func (mr *MockCassandraWithContextMockRecorder) NewBatchWithCtx(ctx, name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).NewBatchWithCtx), ctx, name, batchType)
}
// Query mocks base method.
func (m *MockCassandraWithContext) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCassandraWithContextMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandraWithContext)(nil).Query), varargs...)
}
// QueryWithCtx mocks base method.
func (m *MockCassandraWithContext) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryWithCtx indicates an expected call of QueryWithCtx.
func (mr *MockCassandraWithContextMockRecorder) QueryWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithCtx", reflect.TypeOf((*MockCassandraWithContext)(nil).QueryWithCtx), varargs...)
}
// MockCassandraBatchWithContext is a mock of CassandraBatchWithContext interface.
type MockCassandraBatchWithContext struct {
ctrl *gomock.Controller
recorder *MockCassandraBatchWithContextMockRecorder
isgomock struct{}
}
// MockCassandraBatchWithContextMockRecorder is the mock recorder for MockCassandraBatchWithContext.
type MockCassandraBatchWithContextMockRecorder struct {
mock *MockCassandraBatchWithContext
}
// NewMockCassandraBatchWithContext creates a new mock instance.
func NewMockCassandraBatchWithContext(ctrl *gomock.Controller) *MockCassandraBatchWithContext {
mock := &MockCassandraBatchWithContext{ctrl: ctrl}
mock.recorder = &MockCassandraBatchWithContextMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandraBatchWithContext) EXPECT() *MockCassandraBatchWithContextMockRecorder {
return m.recorder
}
// BatchQueryWithCtx mocks base method.
func (m *MockCassandraBatchWithContext) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockCassandraBatchWithContextMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockCassandraBatchWithContext)(nil).BatchQueryWithCtx), varargs...)
}
// ExecuteBatchCASWithCtx mocks base method.
func (m *MockCassandraBatchWithContext) ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCASWithCtx", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCASWithCtx indicates an expected call of ExecuteBatchCASWithCtx.
func (mr *MockCassandraBatchWithContextMockRecorder) ExecuteBatchCASWithCtx(ctx, name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCASWithCtx", reflect.TypeOf((*MockCassandraBatchWithContext)(nil).ExecuteBatchCASWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockCassandraBatchWithContext) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockCassandraBatchWithContextMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockCassandraBatchWithContext)(nil).ExecuteBatchWithCtx), ctx, name)
}
// MockCassandraProvider is a mock of CassandraProvider interface.
type MockCassandraProvider struct {
ctrl *gomock.Controller
recorder *MockCassandraProviderMockRecorder
isgomock struct{}
}
// MockCassandraProviderMockRecorder is the mock recorder for MockCassandraProvider.
type MockCassandraProviderMockRecorder struct {
mock *MockCassandraProvider
}
// NewMockCassandraProvider creates a new mock instance.
func NewMockCassandraProvider(ctrl *gomock.Controller) *MockCassandraProvider {
mock := &MockCassandraProvider{ctrl: ctrl}
mock.recorder = &MockCassandraProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandraProvider) EXPECT() *MockCassandraProviderMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockCassandraProvider) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockCassandraProviderMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockCassandraProvider)(nil).BatchQuery), varargs...)
}
// BatchQueryWithCtx mocks base method.
func (m *MockCassandraProvider) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockCassandraProviderMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).BatchQueryWithCtx), varargs...)
}
// Connect mocks base method.
func (m *MockCassandraProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockCassandraProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockCassandraProvider)(nil).Connect))
}
// Exec mocks base method.
func (m *MockCassandraProvider) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockCassandraProviderMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandraProvider)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockCassandraProvider) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockCassandraProviderMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockCassandraProvider)(nil).ExecCAS), varargs...)
}
// ExecCASWithCtx mocks base method.
func (m *MockCassandraProvider) ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCASWithCtx", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCASWithCtx indicates an expected call of ExecCASWithCtx.
func (mr *MockCassandraProviderMockRecorder) ExecCASWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCASWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).ExecCASWithCtx), varargs...)
}
// ExecWithCtx mocks base method.
func (m *MockCassandraProvider) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecWithCtx indicates an expected call of ExecWithCtx.
func (mr *MockCassandraProviderMockRecorder) ExecWithCtx(ctx, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).ExecWithCtx), varargs...)
}
// ExecuteBatch mocks base method.
func (m *MockCassandraProvider) ExecuteBatch(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatch", name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatch indicates an expected call of ExecuteBatch.
func (mr *MockCassandraProviderMockRecorder) ExecuteBatch(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatch", reflect.TypeOf((*MockCassandraProvider)(nil).ExecuteBatch), name)
}
// ExecuteBatchCAS mocks base method.
func (m *MockCassandraProvider) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCAS indicates an expected call of ExecuteBatchCAS.
func (mr *MockCassandraProviderMockRecorder) ExecuteBatchCAS(name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCAS", reflect.TypeOf((*MockCassandraProvider)(nil).ExecuteBatchCAS), varargs...)
}
// ExecuteBatchCASWithCtx mocks base method.
func (m *MockCassandraProvider) ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecuteBatchCASWithCtx", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteBatchCASWithCtx indicates an expected call of ExecuteBatchCASWithCtx.
func (mr *MockCassandraProviderMockRecorder) ExecuteBatchCASWithCtx(ctx, name any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchCASWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).ExecuteBatchCASWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockCassandraProvider) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockCassandraProviderMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).ExecuteBatchWithCtx), ctx, name)
}
// HealthCheck mocks base method.
func (m *MockCassandraProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCassandraProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandraProvider)(nil).HealthCheck), arg0)
}
// NewBatch mocks base method.
func (m *MockCassandraProvider) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockCassandraProviderMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockCassandraProvider)(nil).NewBatch), name, batchType)
}
// NewBatchWithCtx mocks base method.
func (m *MockCassandraProvider) NewBatchWithCtx(ctx context.Context, name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatchWithCtx", ctx, name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatchWithCtx indicates an expected call of NewBatchWithCtx.
func (mr *MockCassandraProviderMockRecorder) NewBatchWithCtx(ctx, name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).NewBatchWithCtx), ctx, name, batchType)
}
// Query mocks base method.
func (m *MockCassandraProvider) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCassandraProviderMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCassandraProvider)(nil).Query), varargs...)
}
// QueryWithCtx mocks base method.
func (m *MockCassandraProvider) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryWithCtx indicates an expected call of QueryWithCtx.
func (mr *MockCassandraProviderMockRecorder) QueryWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithCtx", reflect.TypeOf((*MockCassandraProvider)(nil).QueryWithCtx), varargs...)
}
// UseLogger mocks base method.
func (m *MockCassandraProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockCassandraProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockCassandraProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockCassandraProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockCassandraProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockCassandraProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockCassandraProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockCassandraProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockCassandraProvider)(nil).UseTracer), tracer)
}
// MockClickhouse is a mock of Clickhouse interface.
type MockClickhouse struct {
ctrl *gomock.Controller
recorder *MockClickhouseMockRecorder
isgomock struct{}
}
// MockClickhouseMockRecorder is the mock recorder for MockClickhouse.
type MockClickhouseMockRecorder struct {
mock *MockClickhouse
}
// NewMockClickhouse creates a new mock instance.
func NewMockClickhouse(ctrl *gomock.Controller) *MockClickhouse {
mock := &MockClickhouse{ctrl: ctrl}
mock.recorder = &MockClickhouseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClickhouse) EXPECT() *MockClickhouseMockRecorder {
return m.recorder
}
// AsyncInsert mocks base method.
func (m *MockClickhouse) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query, wait}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AsyncInsert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AsyncInsert indicates an expected call of AsyncInsert.
func (mr *MockClickhouseMockRecorder) AsyncInsert(ctx, query, wait any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query, wait}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncInsert", reflect.TypeOf((*MockClickhouse)(nil).AsyncInsert), varargs...)
}
// Exec mocks base method.
func (m *MockClickhouse) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockClickhouseMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouse)(nil).Exec), varargs...)
}
// HealthCheck mocks base method.
func (m *MockClickhouse) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockClickhouseMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouse)(nil).HealthCheck), arg0)
}
// Select mocks base method.
func (m *MockClickhouse) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockClickhouseMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockClickhouse)(nil).Select), varargs...)
}
// MockClickhouseProvider is a mock of ClickhouseProvider interface.
type MockClickhouseProvider struct {
ctrl *gomock.Controller
recorder *MockClickhouseProviderMockRecorder
isgomock struct{}
}
// MockClickhouseProviderMockRecorder is the mock recorder for MockClickhouseProvider.
type MockClickhouseProviderMockRecorder struct {
mock *MockClickhouseProvider
}
// NewMockClickhouseProvider creates a new mock instance.
func NewMockClickhouseProvider(ctrl *gomock.Controller) *MockClickhouseProvider {
mock := &MockClickhouseProvider{ctrl: ctrl}
mock.recorder = &MockClickhouseProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClickhouseProvider) EXPECT() *MockClickhouseProviderMockRecorder {
return m.recorder
}
// AsyncInsert mocks base method.
func (m *MockClickhouseProvider) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query, wait}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AsyncInsert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AsyncInsert indicates an expected call of AsyncInsert.
func (mr *MockClickhouseProviderMockRecorder) AsyncInsert(ctx, query, wait any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query, wait}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncInsert", reflect.TypeOf((*MockClickhouseProvider)(nil).AsyncInsert), varargs...)
}
// Connect mocks base method.
func (m *MockClickhouseProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockClickhouseProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockClickhouseProvider)(nil).Connect))
}
// Exec mocks base method.
func (m *MockClickhouseProvider) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockClickhouseProviderMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouseProvider)(nil).Exec), varargs...)
}
// HealthCheck mocks base method.
func (m *MockClickhouseProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockClickhouseProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouseProvider)(nil).HealthCheck), arg0)
}
// Select mocks base method.
func (m *MockClickhouseProvider) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockClickhouseProviderMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockClickhouseProvider)(nil).Select), varargs...)
}
// UseLogger mocks base method.
func (m *MockClickhouseProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockClickhouseProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockClickhouseProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockClickhouseProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockClickhouseProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockClickhouseProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockClickhouseProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockClickhouseProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockClickhouseProvider)(nil).UseTracer), tracer)
}
// MockOracleDB is a mock of OracleDB interface.
type MockOracleDB struct {
ctrl *gomock.Controller
recorder *MockOracleDBMockRecorder
isgomock struct{}
}
// MockOracleDBMockRecorder is the mock recorder for MockOracleDB.
type MockOracleDBMockRecorder struct {
mock *MockOracleDB
}
// NewMockOracleDB creates a new mock instance.
func NewMockOracleDB(ctrl *gomock.Controller) *MockOracleDB {
mock := &MockOracleDB{ctrl: ctrl}
mock.recorder = &MockOracleDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOracleDB) EXPECT() *MockOracleDBMockRecorder {
return m.recorder
}
// Begin mocks base method.
func (m *MockOracleDB) Begin() (OracleTx, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Begin")
ret0, _ := ret[0].(OracleTx)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Begin indicates an expected call of Begin.
func (mr *MockOracleDBMockRecorder) Begin() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockOracleDB)(nil).Begin))
}
// Exec mocks base method.
func (m *MockOracleDB) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockOracleDBMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockOracleDB)(nil).Exec), varargs...)
}
// HealthCheck mocks base method.
func (m *MockOracleDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockOracleDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOracleDB)(nil).HealthCheck), arg0)
}
// Select mocks base method.
func (m *MockOracleDB) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockOracleDBMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockOracleDB)(nil).Select), varargs...)
}
// MockOracleTx is a mock of OracleTx interface.
type MockOracleTx struct {
ctrl *gomock.Controller
recorder *MockOracleTxMockRecorder
isgomock struct{}
}
// MockOracleTxMockRecorder is the mock recorder for MockOracleTx.
type MockOracleTxMockRecorder struct {
mock *MockOracleTx
}
// NewMockOracleTx creates a new mock instance.
func NewMockOracleTx(ctrl *gomock.Controller) *MockOracleTx {
mock := &MockOracleTx{ctrl: ctrl}
mock.recorder = &MockOracleTxMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOracleTx) EXPECT() *MockOracleTxMockRecorder {
return m.recorder
}
// Commit mocks base method.
func (m *MockOracleTx) Commit() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Commit")
ret0, _ := ret[0].(error)
return ret0
}
// Commit indicates an expected call of Commit.
func (mr *MockOracleTxMockRecorder) Commit() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockOracleTx)(nil).Commit))
}
// ExecContext mocks base method.
func (m *MockOracleTx) ExecContext(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecContext", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecContext indicates an expected call of ExecContext.
func (mr *MockOracleTxMockRecorder) ExecContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockOracleTx)(nil).ExecContext), varargs...)
}
// Rollback mocks base method.
func (m *MockOracleTx) Rollback() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rollback")
ret0, _ := ret[0].(error)
return ret0
}
// Rollback indicates an expected call of Rollback.
func (mr *MockOracleTxMockRecorder) Rollback() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockOracleTx)(nil).Rollback))
}
// SelectContext mocks base method.
func (m *MockOracleTx) SelectContext(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SelectContext", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// SelectContext indicates an expected call of SelectContext.
func (mr *MockOracleTxMockRecorder) SelectContext(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectContext", reflect.TypeOf((*MockOracleTx)(nil).SelectContext), varargs...)
}
// MockOracleProvider is a mock of OracleProvider interface.
type MockOracleProvider struct {
ctrl *gomock.Controller
recorder *MockOracleProviderMockRecorder
isgomock struct{}
}
// MockOracleProviderMockRecorder is the mock recorder for MockOracleProvider.
type MockOracleProviderMockRecorder struct {
mock *MockOracleProvider
}
// NewMockOracleProvider creates a new mock instance.
func NewMockOracleProvider(ctrl *gomock.Controller) *MockOracleProvider {
mock := &MockOracleProvider{ctrl: ctrl}
mock.recorder = &MockOracleProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOracleProvider) EXPECT() *MockOracleProviderMockRecorder {
return m.recorder
}
// Begin mocks base method.
func (m *MockOracleProvider) Begin() (OracleTx, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Begin")
ret0, _ := ret[0].(OracleTx)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Begin indicates an expected call of Begin.
func (mr *MockOracleProviderMockRecorder) Begin() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockOracleProvider)(nil).Begin))
}
// Connect mocks base method.
func (m *MockOracleProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockOracleProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockOracleProvider)(nil).Connect))
}
// Exec mocks base method.
func (m *MockOracleProvider) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockOracleProviderMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockOracleProvider)(nil).Exec), varargs...)
}
// HealthCheck mocks base method.
func (m *MockOracleProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockOracleProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOracleProvider)(nil).HealthCheck), arg0)
}
// Select mocks base method.
func (m *MockOracleProvider) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockOracleProviderMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockOracleProvider)(nil).Select), varargs...)
}
// UseLogger mocks base method.
func (m *MockOracleProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockOracleProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOracleProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockOracleProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockOracleProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOracleProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockOracleProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockOracleProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOracleProvider)(nil).UseTracer), tracer)
}
// MockMongo is a mock of Mongo interface.
type MockMongo struct {
ctrl *gomock.Controller
recorder *MockMongoMockRecorder
isgomock struct{}
}
// MockMongoMockRecorder is the mock recorder for MockMongo.
type MockMongoMockRecorder struct {
mock *MockMongo
}
// NewMockMongo creates a new mock instance.
func NewMockMongo(ctrl *gomock.Controller) *MockMongo {
mock := &MockMongo{ctrl: ctrl}
mock.recorder = &MockMongoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMongo) EXPECT() *MockMongoMockRecorder {
return m.recorder
}
// CountDocuments mocks base method.
func (m *MockMongo) CountDocuments(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountDocuments", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountDocuments indicates an expected call of CountDocuments.
func (mr *MockMongoMockRecorder) CountDocuments(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountDocuments", reflect.TypeOf((*MockMongo)(nil).CountDocuments), ctx, collection, filter)
}
// CreateCollection mocks base method.
func (m *MockMongo) CreateCollection(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockMongoMockRecorder) CreateCollection(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockMongo)(nil).CreateCollection), ctx, name)
}
// DeleteMany mocks base method.
func (m *MockMongo) DeleteMany(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMany", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteMany indicates an expected call of DeleteMany.
func (mr *MockMongoMockRecorder) DeleteMany(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockMongo)(nil).DeleteMany), ctx, collection, filter)
}
// DeleteOne mocks base method.
func (m *MockMongo) DeleteOne(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOne", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOne indicates an expected call of DeleteOne.
func (mr *MockMongoMockRecorder) DeleteOne(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOne", reflect.TypeOf((*MockMongo)(nil).DeleteOne), ctx, collection, filter)
}
// Drop mocks base method.
func (m *MockMongo) Drop(ctx context.Context, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Drop", ctx, collection)
ret0, _ := ret[0].(error)
return ret0
}
// Drop indicates an expected call of Drop.
func (mr *MockMongoMockRecorder) Drop(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Drop", reflect.TypeOf((*MockMongo)(nil).Drop), ctx, collection)
}
// Find mocks base method.
func (m *MockMongo) Find(ctx context.Context, collection string, filter, results any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx, collection, filter, results)
ret0, _ := ret[0].(error)
return ret0
}
// Find indicates an expected call of Find.
func (mr *MockMongoMockRecorder) Find(ctx, collection, filter, results any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockMongo)(nil).Find), ctx, collection, filter, results)
}
// FindOne mocks base method.
func (m *MockMongo) FindOne(ctx context.Context, collection string, filter, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, collection, filter, result)
ret0, _ := ret[0].(error)
return ret0
}
// FindOne indicates an expected call of FindOne.
func (mr *MockMongoMockRecorder) FindOne(ctx, collection, filter, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockMongo)(nil).FindOne), ctx, collection, filter, result)
}
// HealthCheck mocks base method.
func (m *MockMongo) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockMongoMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockMongo)(nil).HealthCheck), arg0)
}
// InsertMany mocks base method.
func (m *MockMongo) InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertMany", ctx, collection, documents)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertMany indicates an expected call of InsertMany.
func (mr *MockMongoMockRecorder) InsertMany(ctx, collection, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMany", reflect.TypeOf((*MockMongo)(nil).InsertMany), ctx, collection, documents)
}
// InsertOne mocks base method.
func (m *MockMongo) InsertOne(ctx context.Context, collection string, document any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertOne", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertOne indicates an expected call of InsertOne.
func (mr *MockMongoMockRecorder) InsertOne(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOne", reflect.TypeOf((*MockMongo)(nil).InsertOne), ctx, collection, document)
}
// StartSession mocks base method.
func (m *MockMongo) StartSession() (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartSession")
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StartSession indicates an expected call of StartSession.
func (mr *MockMongoMockRecorder) StartSession() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartSession", reflect.TypeOf((*MockMongo)(nil).StartSession))
}
// UpdateByID mocks base method.
func (m *MockMongo) UpdateByID(ctx context.Context, collection string, id, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateByID", ctx, collection, id, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateByID indicates an expected call of UpdateByID.
func (mr *MockMongoMockRecorder) UpdateByID(ctx, collection, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateByID", reflect.TypeOf((*MockMongo)(nil).UpdateByID), ctx, collection, id, update)
}
// UpdateMany mocks base method.
func (m *MockMongo) UpdateMany(ctx context.Context, collection string, filter, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateMany", ctx, collection, filter, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateMany indicates an expected call of UpdateMany.
func (mr *MockMongoMockRecorder) UpdateMany(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMany", reflect.TypeOf((*MockMongo)(nil).UpdateMany), ctx, collection, filter, update)
}
// UpdateOne mocks base method.
func (m *MockMongo) UpdateOne(ctx context.Context, collection string, filter, update any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateOne", ctx, collection, filter, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateOne indicates an expected call of UpdateOne.
func (mr *MockMongoMockRecorder) UpdateOne(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOne", reflect.TypeOf((*MockMongo)(nil).UpdateOne), ctx, collection, filter, update)
}
// MockTransaction is a mock of Transaction interface.
type MockTransaction struct {
ctrl *gomock.Controller
recorder *MockTransactionMockRecorder
isgomock struct{}
}
// MockTransactionMockRecorder is the mock recorder for MockTransaction.
type MockTransactionMockRecorder struct {
mock *MockTransaction
}
// NewMockTransaction creates a new mock instance.
func NewMockTransaction(ctrl *gomock.Controller) *MockTransaction {
mock := &MockTransaction{ctrl: ctrl}
mock.recorder = &MockTransactionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTransaction) EXPECT() *MockTransactionMockRecorder {
return m.recorder
}
// AbortTransaction mocks base method.
func (m *MockTransaction) AbortTransaction(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AbortTransaction", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// AbortTransaction indicates an expected call of AbortTransaction.
func (mr *MockTransactionMockRecorder) AbortTransaction(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortTransaction", reflect.TypeOf((*MockTransaction)(nil).AbortTransaction), arg0)
}
// CommitTransaction mocks base method.
func (m *MockTransaction) CommitTransaction(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CommitTransaction", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// CommitTransaction indicates an expected call of CommitTransaction.
func (mr *MockTransactionMockRecorder) CommitTransaction(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitTransaction", reflect.TypeOf((*MockTransaction)(nil).CommitTransaction), arg0)
}
// EndSession mocks base method.
func (m *MockTransaction) EndSession(arg0 context.Context) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "EndSession", arg0)
}
// EndSession indicates an expected call of EndSession.
func (mr *MockTransactionMockRecorder) EndSession(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EndSession", reflect.TypeOf((*MockTransaction)(nil).EndSession), arg0)
}
// StartTransaction mocks base method.
func (m *MockTransaction) StartTransaction() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartTransaction")
ret0, _ := ret[0].(error)
return ret0
}
// StartTransaction indicates an expected call of StartTransaction.
func (mr *MockTransactionMockRecorder) StartTransaction() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTransaction", reflect.TypeOf((*MockTransaction)(nil).StartTransaction))
}
// MockMongoProvider is a mock of MongoProvider interface.
type MockMongoProvider struct {
ctrl *gomock.Controller
recorder *MockMongoProviderMockRecorder
isgomock struct{}
}
// MockMongoProviderMockRecorder is the mock recorder for MockMongoProvider.
type MockMongoProviderMockRecorder struct {
mock *MockMongoProvider
}
// NewMockMongoProvider creates a new mock instance.
func NewMockMongoProvider(ctrl *gomock.Controller) *MockMongoProvider {
mock := &MockMongoProvider{ctrl: ctrl}
mock.recorder = &MockMongoProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMongoProvider) EXPECT() *MockMongoProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockMongoProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockMongoProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockMongoProvider)(nil).Connect))
}
// CountDocuments mocks base method.
func (m *MockMongoProvider) CountDocuments(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CountDocuments", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CountDocuments indicates an expected call of CountDocuments.
func (mr *MockMongoProviderMockRecorder) CountDocuments(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountDocuments", reflect.TypeOf((*MockMongoProvider)(nil).CountDocuments), ctx, collection, filter)
}
// CreateCollection mocks base method.
func (m *MockMongoProvider) CreateCollection(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockMongoProviderMockRecorder) CreateCollection(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockMongoProvider)(nil).CreateCollection), ctx, name)
}
// DeleteMany mocks base method.
func (m *MockMongoProvider) DeleteMany(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMany", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteMany indicates an expected call of DeleteMany.
func (mr *MockMongoProviderMockRecorder) DeleteMany(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockMongoProvider)(nil).DeleteMany), ctx, collection, filter)
}
// DeleteOne mocks base method.
func (m *MockMongoProvider) DeleteOne(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOne", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOne indicates an expected call of DeleteOne.
func (mr *MockMongoProviderMockRecorder) DeleteOne(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOne", reflect.TypeOf((*MockMongoProvider)(nil).DeleteOne), ctx, collection, filter)
}
// Drop mocks base method.
func (m *MockMongoProvider) Drop(ctx context.Context, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Drop", ctx, collection)
ret0, _ := ret[0].(error)
return ret0
}
// Drop indicates an expected call of Drop.
func (mr *MockMongoProviderMockRecorder) Drop(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Drop", reflect.TypeOf((*MockMongoProvider)(nil).Drop), ctx, collection)
}
// Find mocks base method.
func (m *MockMongoProvider) Find(ctx context.Context, collection string, filter, results any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx, collection, filter, results)
ret0, _ := ret[0].(error)
return ret0
}
// Find indicates an expected call of Find.
func (mr *MockMongoProviderMockRecorder) Find(ctx, collection, filter, results any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockMongoProvider)(nil).Find), ctx, collection, filter, results)
}
// FindOne mocks base method.
func (m *MockMongoProvider) FindOne(ctx context.Context, collection string, filter, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, collection, filter, result)
ret0, _ := ret[0].(error)
return ret0
}
// FindOne indicates an expected call of FindOne.
func (mr *MockMongoProviderMockRecorder) FindOne(ctx, collection, filter, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockMongoProvider)(nil).FindOne), ctx, collection, filter, result)
}
// HealthCheck mocks base method.
func (m *MockMongoProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockMongoProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockMongoProvider)(nil).HealthCheck), arg0)
}
// InsertMany mocks base method.
func (m *MockMongoProvider) InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertMany", ctx, collection, documents)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertMany indicates an expected call of InsertMany.
func (mr *MockMongoProviderMockRecorder) InsertMany(ctx, collection, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMany", reflect.TypeOf((*MockMongoProvider)(nil).InsertMany), ctx, collection, documents)
}
// InsertOne mocks base method.
func (m *MockMongoProvider) InsertOne(ctx context.Context, collection string, document any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertOne", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertOne indicates an expected call of InsertOne.
func (mr *MockMongoProviderMockRecorder) InsertOne(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOne", reflect.TypeOf((*MockMongoProvider)(nil).InsertOne), ctx, collection, document)
}
// StartSession mocks base method.
func (m *MockMongoProvider) StartSession() (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartSession")
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StartSession indicates an expected call of StartSession.
func (mr *MockMongoProviderMockRecorder) StartSession() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartSession", reflect.TypeOf((*MockMongoProvider)(nil).StartSession))
}
// UpdateByID mocks base method.
func (m *MockMongoProvider) UpdateByID(ctx context.Context, collection string, id, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateByID", ctx, collection, id, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateByID indicates an expected call of UpdateByID.
func (mr *MockMongoProviderMockRecorder) UpdateByID(ctx, collection, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateByID", reflect.TypeOf((*MockMongoProvider)(nil).UpdateByID), ctx, collection, id, update)
}
// UpdateMany mocks base method.
func (m *MockMongoProvider) UpdateMany(ctx context.Context, collection string, filter, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateMany", ctx, collection, filter, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateMany indicates an expected call of UpdateMany.
func (mr *MockMongoProviderMockRecorder) UpdateMany(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMany", reflect.TypeOf((*MockMongoProvider)(nil).UpdateMany), ctx, collection, filter, update)
}
// UpdateOne mocks base method.
func (m *MockMongoProvider) UpdateOne(ctx context.Context, collection string, filter, update any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateOne", ctx, collection, filter, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateOne indicates an expected call of UpdateOne.
func (mr *MockMongoProviderMockRecorder) UpdateOne(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOne", reflect.TypeOf((*MockMongoProvider)(nil).UpdateOne), ctx, collection, filter, update)
}
// UseLogger mocks base method.
func (m *MockMongoProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockMongoProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockMongoProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockMongoProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockMongoProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockMongoProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockMongoProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockMongoProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockMongoProvider)(nil).UseTracer), tracer)
}
// MockSurrealDB is a mock of SurrealDB interface.
type MockSurrealDB struct {
ctrl *gomock.Controller
recorder *MockSurrealDBMockRecorder
isgomock struct{}
}
// MockSurrealDBMockRecorder is the mock recorder for MockSurrealDB.
type MockSurrealDBMockRecorder struct {
mock *MockSurrealDB
}
// NewMockSurrealDB creates a new mock instance.
func NewMockSurrealDB(ctrl *gomock.Controller) *MockSurrealDB {
mock := &MockSurrealDB{ctrl: ctrl}
mock.recorder = &MockSurrealDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSurrealDB) EXPECT() *MockSurrealDBMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MockSurrealDB) Create(ctx context.Context, table string, data any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, table, data)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockSurrealDBMockRecorder) Create(ctx, table, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSurrealDB)(nil).Create), ctx, table, data)
}
// CreateDatabase mocks base method.
func (m *MockSurrealDB) CreateDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDatabase indicates an expected call of CreateDatabase.
func (mr *MockSurrealDBMockRecorder) CreateDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDatabase", reflect.TypeOf((*MockSurrealDB)(nil).CreateDatabase), ctx, database)
}
// CreateNamespace mocks base method.
func (m *MockSurrealDB) CreateNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// CreateNamespace indicates an expected call of CreateNamespace.
func (mr *MockSurrealDBMockRecorder) CreateNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNamespace", reflect.TypeOf((*MockSurrealDB)(nil).CreateNamespace), ctx, namespace)
}
// Delete mocks base method.
func (m *MockSurrealDB) Delete(ctx context.Context, table, id string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, table, id)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockSurrealDBMockRecorder) Delete(ctx, table, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSurrealDB)(nil).Delete), ctx, table, id)
}
// DropDatabase mocks base method.
func (m *MockSurrealDB) DropDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDatabase indicates an expected call of DropDatabase.
func (mr *MockSurrealDBMockRecorder) DropDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDatabase", reflect.TypeOf((*MockSurrealDB)(nil).DropDatabase), ctx, database)
}
// DropNamespace mocks base method.
func (m *MockSurrealDB) DropNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// DropNamespace indicates an expected call of DropNamespace.
func (mr *MockSurrealDBMockRecorder) DropNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropNamespace", reflect.TypeOf((*MockSurrealDB)(nil).DropNamespace), ctx, namespace)
}
// HealthCheck mocks base method.
func (m *MockSurrealDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockSurrealDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockSurrealDB)(nil).HealthCheck), arg0)
}
// Query mocks base method.
func (m *MockSurrealDB) Query(ctx context.Context, query string, vars map[string]any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query, vars)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockSurrealDBMockRecorder) Query(ctx, query, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockSurrealDB)(nil).Query), ctx, query, vars)
}
// Select mocks base method.
func (m *MockSurrealDB) Select(ctx context.Context, table string) ([]map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", ctx, table)
ret0, _ := ret[0].([]map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Select indicates an expected call of Select.
func (mr *MockSurrealDBMockRecorder) Select(ctx, table any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockSurrealDB)(nil).Select), ctx, table)
}
// Update mocks base method.
func (m *MockSurrealDB) Update(ctx context.Context, table, id string, data any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, table, id, data)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSurrealDBMockRecorder) Update(ctx, table, id, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSurrealDB)(nil).Update), ctx, table, id, data)
}
// MockSurrealBDProvider is a mock of SurrealBDProvider interface.
type MockSurrealBDProvider struct {
ctrl *gomock.Controller
recorder *MockSurrealBDProviderMockRecorder
isgomock struct{}
}
// MockSurrealBDProviderMockRecorder is the mock recorder for MockSurrealBDProvider.
type MockSurrealBDProviderMockRecorder struct {
mock *MockSurrealBDProvider
}
// NewMockSurrealBDProvider creates a new mock instance.
func NewMockSurrealBDProvider(ctrl *gomock.Controller) *MockSurrealBDProvider {
mock := &MockSurrealBDProvider{ctrl: ctrl}
mock.recorder = &MockSurrealBDProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSurrealBDProvider) EXPECT() *MockSurrealBDProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockSurrealBDProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockSurrealBDProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockSurrealBDProvider)(nil).Connect))
}
// Create mocks base method.
func (m *MockSurrealBDProvider) Create(ctx context.Context, table string, data any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, table, data)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockSurrealBDProviderMockRecorder) Create(ctx, table, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSurrealBDProvider)(nil).Create), ctx, table, data)
}
// CreateDatabase mocks base method.
func (m *MockSurrealBDProvider) CreateDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDatabase indicates an expected call of CreateDatabase.
func (mr *MockSurrealBDProviderMockRecorder) CreateDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDatabase", reflect.TypeOf((*MockSurrealBDProvider)(nil).CreateDatabase), ctx, database)
}
// CreateNamespace mocks base method.
func (m *MockSurrealBDProvider) CreateNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// CreateNamespace indicates an expected call of CreateNamespace.
func (mr *MockSurrealBDProviderMockRecorder) CreateNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNamespace", reflect.TypeOf((*MockSurrealBDProvider)(nil).CreateNamespace), ctx, namespace)
}
// Delete mocks base method.
func (m *MockSurrealBDProvider) Delete(ctx context.Context, table, id string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, table, id)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockSurrealBDProviderMockRecorder) Delete(ctx, table, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSurrealBDProvider)(nil).Delete), ctx, table, id)
}
// DropDatabase mocks base method.
func (m *MockSurrealBDProvider) DropDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDatabase indicates an expected call of DropDatabase.
func (mr *MockSurrealBDProviderMockRecorder) DropDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDatabase", reflect.TypeOf((*MockSurrealBDProvider)(nil).DropDatabase), ctx, database)
}
// DropNamespace mocks base method.
func (m *MockSurrealBDProvider) DropNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// DropNamespace indicates an expected call of DropNamespace.
func (mr *MockSurrealBDProviderMockRecorder) DropNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropNamespace", reflect.TypeOf((*MockSurrealBDProvider)(nil).DropNamespace), ctx, namespace)
}
// HealthCheck mocks base method.
func (m *MockSurrealBDProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockSurrealBDProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockSurrealBDProvider)(nil).HealthCheck), arg0)
}
// Query mocks base method.
func (m *MockSurrealBDProvider) Query(ctx context.Context, query string, vars map[string]any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query, vars)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockSurrealBDProviderMockRecorder) Query(ctx, query, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockSurrealBDProvider)(nil).Query), ctx, query, vars)
}
// Select mocks base method.
func (m *MockSurrealBDProvider) Select(ctx context.Context, table string) ([]map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Select", ctx, table)
ret0, _ := ret[0].([]map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Select indicates an expected call of Select.
func (mr *MockSurrealBDProviderMockRecorder) Select(ctx, table any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockSurrealBDProvider)(nil).Select), ctx, table)
}
// Update mocks base method.
func (m *MockSurrealBDProvider) Update(ctx context.Context, table, id string, data any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, table, id, data)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSurrealBDProviderMockRecorder) Update(ctx, table, id, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSurrealBDProvider)(nil).Update), ctx, table, id, data)
}
// UseLogger mocks base method.
func (m *MockSurrealBDProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockSurrealBDProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockSurrealBDProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockSurrealBDProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockSurrealBDProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockSurrealBDProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockSurrealBDProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockSurrealBDProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockSurrealBDProvider)(nil).UseTracer), tracer)
}
// Mockprovider is a mock of provider interface.
type Mockprovider struct {
ctrl *gomock.Controller
recorder *MockproviderMockRecorder
isgomock struct{}
}
// MockproviderMockRecorder is the mock recorder for Mockprovider.
type MockproviderMockRecorder struct {
mock *Mockprovider
}
// NewMockprovider creates a new mock instance.
func NewMockprovider(ctrl *gomock.Controller) *Mockprovider {
mock := &Mockprovider{ctrl: ctrl}
mock.recorder = &MockproviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockprovider) EXPECT() *MockproviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *Mockprovider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockproviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*Mockprovider)(nil).Connect))
}
// UseLogger mocks base method.
func (m *Mockprovider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockproviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*Mockprovider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *Mockprovider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockproviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*Mockprovider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *Mockprovider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockproviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*Mockprovider)(nil).UseTracer), tracer)
}
// MockHealthChecker is a mock of HealthChecker interface.
type MockHealthChecker struct {
ctrl *gomock.Controller
recorder *MockHealthCheckerMockRecorder
isgomock struct{}
}
// MockHealthCheckerMockRecorder is the mock recorder for MockHealthChecker.
type MockHealthCheckerMockRecorder struct {
mock *MockHealthChecker
}
// NewMockHealthChecker creates a new mock instance.
func NewMockHealthChecker(ctrl *gomock.Controller) *MockHealthChecker {
mock := &MockHealthChecker{ctrl: ctrl}
mock.recorder = &MockHealthCheckerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHealthChecker) EXPECT() *MockHealthCheckerMockRecorder {
return m.recorder
}
// HealthCheck mocks base method.
func (m *MockHealthChecker) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockHealthCheckerMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockHealthChecker)(nil).HealthCheck), arg0)
}
// MockKVStore is a mock of KVStore interface.
type MockKVStore struct {
ctrl *gomock.Controller
recorder *MockKVStoreMockRecorder
isgomock struct{}
}
// MockKVStoreMockRecorder is the mock recorder for MockKVStore.
type MockKVStoreMockRecorder struct {
mock *MockKVStore
}
// NewMockKVStore creates a new mock instance.
func NewMockKVStore(ctrl *gomock.Controller) *MockKVStore {
mock := &MockKVStore{ctrl: ctrl}
mock.recorder = &MockKVStoreMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockKVStore) EXPECT() *MockKVStoreMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockKVStore) Delete(ctx context.Context, key string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, key)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockKVStoreMockRecorder) Delete(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKVStore)(nil).Delete), ctx, key)
}
// Get mocks base method.
func (m *MockKVStore) Get(ctx context.Context, key string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockKVStoreMockRecorder) Get(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKVStore)(nil).Get), ctx, key)
}
// HealthCheck mocks base method.
func (m *MockKVStore) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockKVStoreMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockKVStore)(nil).HealthCheck), arg0)
}
// Set mocks base method.
func (m *MockKVStore) Set(ctx context.Context, key, value string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", ctx, key, value)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockKVStoreMockRecorder) Set(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockKVStore)(nil).Set), ctx, key, value)
}
// MockKVStoreProvider is a mock of KVStoreProvider interface.
type MockKVStoreProvider struct {
ctrl *gomock.Controller
recorder *MockKVStoreProviderMockRecorder
isgomock struct{}
}
// MockKVStoreProviderMockRecorder is the mock recorder for MockKVStoreProvider.
type MockKVStoreProviderMockRecorder struct {
mock *MockKVStoreProvider
}
// NewMockKVStoreProvider creates a new mock instance.
func NewMockKVStoreProvider(ctrl *gomock.Controller) *MockKVStoreProvider {
mock := &MockKVStoreProvider{ctrl: ctrl}
mock.recorder = &MockKVStoreProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockKVStoreProvider) EXPECT() *MockKVStoreProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockKVStoreProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockKVStoreProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockKVStoreProvider)(nil).Connect))
}
// Delete mocks base method.
func (m *MockKVStoreProvider) Delete(ctx context.Context, key string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, key)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockKVStoreProviderMockRecorder) Delete(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKVStoreProvider)(nil).Delete), ctx, key)
}
// Get mocks base method.
func (m *MockKVStoreProvider) Get(ctx context.Context, key string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockKVStoreProviderMockRecorder) Get(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKVStoreProvider)(nil).Get), ctx, key)
}
// HealthCheck mocks base method.
func (m *MockKVStoreProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockKVStoreProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockKVStoreProvider)(nil).HealthCheck), arg0)
}
// Set mocks base method.
func (m *MockKVStoreProvider) Set(ctx context.Context, key, value string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", ctx, key, value)
ret0, _ := ret[0].(error)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockKVStoreProviderMockRecorder) Set(ctx, key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockKVStoreProvider)(nil).Set), ctx, key, value)
}
// UseLogger mocks base method.
func (m *MockKVStoreProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockKVStoreProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockKVStoreProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockKVStoreProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockKVStoreProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockKVStoreProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockKVStoreProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockKVStoreProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockKVStoreProvider)(nil).UseTracer), tracer)
}
// MockPubSubProvider is a mock of PubSubProvider interface.
type MockPubSubProvider struct {
ctrl *gomock.Controller
recorder *MockPubSubProviderMockRecorder
isgomock struct{}
}
// MockPubSubProviderMockRecorder is the mock recorder for MockPubSubProvider.
type MockPubSubProviderMockRecorder struct {
mock *MockPubSubProvider
}
// NewMockPubSubProvider creates a new mock instance.
func NewMockPubSubProvider(ctrl *gomock.Controller) *MockPubSubProvider {
mock := &MockPubSubProvider{ctrl: ctrl}
mock.recorder = &MockPubSubProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPubSubProvider) EXPECT() *MockPubSubProviderMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockPubSubProvider) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockPubSubProviderMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockPubSubProvider)(nil).Close))
}
// Connect mocks base method.
func (m *MockPubSubProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockPubSubProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockPubSubProvider)(nil).Connect))
}
// CreateTopic mocks base method.
func (m *MockPubSubProvider) CreateTopic(arg0 context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTopic", arg0, name)
ret0, _ := ret[0].(error)
return ret0
}
// CreateTopic indicates an expected call of CreateTopic.
func (mr *MockPubSubProviderMockRecorder) CreateTopic(arg0, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTopic", reflect.TypeOf((*MockPubSubProvider)(nil).CreateTopic), arg0, name)
}
// DeleteTopic mocks base method.
func (m *MockPubSubProvider) DeleteTopic(arg0 context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTopic", arg0, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteTopic indicates an expected call of DeleteTopic.
func (mr *MockPubSubProviderMockRecorder) DeleteTopic(arg0, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTopic", reflect.TypeOf((*MockPubSubProvider)(nil).DeleteTopic), arg0, name)
}
// Health mocks base method.
func (m *MockPubSubProvider) Health() datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health")
ret0, _ := ret[0].(datasource.Health)
return ret0
}
// Health indicates an expected call of Health.
func (mr *MockPubSubProviderMockRecorder) Health() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockPubSubProvider)(nil).Health))
}
// Publish mocks base method.
func (m *MockPubSubProvider) Publish(ctx context.Context, topic string, message []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, topic, message)
ret0, _ := ret[0].(error)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockPubSubProviderMockRecorder) Publish(ctx, topic, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockPubSubProvider)(nil).Publish), ctx, topic, message)
}
// Query mocks base method.
func (m *MockPubSubProvider) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockPubSubProviderMockRecorder) Query(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockPubSubProvider)(nil).Query), varargs...)
}
// Subscribe mocks base method.
func (m *MockPubSubProvider) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscribe", ctx, topic)
ret0, _ := ret[0].(*pubsub.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Subscribe indicates an expected call of Subscribe.
func (mr *MockPubSubProviderMockRecorder) Subscribe(ctx, topic any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockPubSubProvider)(nil).Subscribe), ctx, topic)
}
// UseLogger mocks base method.
func (m *MockPubSubProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockPubSubProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockPubSubProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockPubSubProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockPubSubProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockPubSubProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockPubSubProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockPubSubProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockPubSubProvider)(nil).UseTracer), tracer)
}
// MockSolr is a mock of Solr interface.
type MockSolr struct {
ctrl *gomock.Controller
recorder *MockSolrMockRecorder
isgomock struct{}
}
// MockSolrMockRecorder is the mock recorder for MockSolr.
type MockSolrMockRecorder struct {
mock *MockSolr
}
// NewMockSolr creates a new mock instance.
func NewMockSolr(ctrl *gomock.Controller) *MockSolr {
mock := &MockSolr{ctrl: ctrl}
mock.recorder = &MockSolrMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSolr) EXPECT() *MockSolrMockRecorder {
return m.recorder
}
// AddField mocks base method.
func (m *MockSolr) AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AddField indicates an expected call of AddField.
func (mr *MockSolrMockRecorder) AddField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddField", reflect.TypeOf((*MockSolr)(nil).AddField), ctx, collection, document)
}
// Create mocks base method.
func (m *MockSolr) Create(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockSolrMockRecorder) Create(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSolr)(nil).Create), ctx, collection, document, params)
}
// Delete mocks base method.
func (m *MockSolr) Delete(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockSolrMockRecorder) Delete(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSolr)(nil).Delete), ctx, collection, document, params)
}
// DeleteField mocks base method.
func (m *MockSolr) DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteField indicates an expected call of DeleteField.
func (mr *MockSolrMockRecorder) DeleteField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteField", reflect.TypeOf((*MockSolr)(nil).DeleteField), ctx, collection, document)
}
// HealthCheck mocks base method.
func (m *MockSolr) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockSolrMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockSolr)(nil).HealthCheck), arg0)
}
// ListFields mocks base method.
func (m *MockSolr) ListFields(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListFields", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListFields indicates an expected call of ListFields.
func (mr *MockSolrMockRecorder) ListFields(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFields", reflect.TypeOf((*MockSolr)(nil).ListFields), ctx, collection, params)
}
// Retrieve mocks base method.
func (m *MockSolr) Retrieve(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Retrieve", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Retrieve indicates an expected call of Retrieve.
func (mr *MockSolrMockRecorder) Retrieve(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockSolr)(nil).Retrieve), ctx, collection, params)
}
// Search mocks base method.
func (m *MockSolr) Search(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search.
func (mr *MockSolrMockRecorder) Search(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockSolr)(nil).Search), ctx, collection, params)
}
// Update mocks base method.
func (m *MockSolr) Update(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSolrMockRecorder) Update(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSolr)(nil).Update), ctx, collection, document, params)
}
// UpdateField mocks base method.
func (m *MockSolr) UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateField indicates an expected call of UpdateField.
func (mr *MockSolrMockRecorder) UpdateField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateField", reflect.TypeOf((*MockSolr)(nil).UpdateField), ctx, collection, document)
}
// MockSolrProvider is a mock of SolrProvider interface.
type MockSolrProvider struct {
ctrl *gomock.Controller
recorder *MockSolrProviderMockRecorder
isgomock struct{}
}
// MockSolrProviderMockRecorder is the mock recorder for MockSolrProvider.
type MockSolrProviderMockRecorder struct {
mock *MockSolrProvider
}
// NewMockSolrProvider creates a new mock instance.
func NewMockSolrProvider(ctrl *gomock.Controller) *MockSolrProvider {
mock := &MockSolrProvider{ctrl: ctrl}
mock.recorder = &MockSolrProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSolrProvider) EXPECT() *MockSolrProviderMockRecorder {
return m.recorder
}
// AddField mocks base method.
func (m *MockSolrProvider) AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AddField indicates an expected call of AddField.
func (mr *MockSolrProviderMockRecorder) AddField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddField", reflect.TypeOf((*MockSolrProvider)(nil).AddField), ctx, collection, document)
}
// Connect mocks base method.
func (m *MockSolrProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockSolrProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockSolrProvider)(nil).Connect))
}
// Create mocks base method.
func (m *MockSolrProvider) Create(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockSolrProviderMockRecorder) Create(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockSolrProvider)(nil).Create), ctx, collection, document, params)
}
// Delete mocks base method.
func (m *MockSolrProvider) Delete(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockSolrProviderMockRecorder) Delete(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockSolrProvider)(nil).Delete), ctx, collection, document, params)
}
// DeleteField mocks base method.
func (m *MockSolrProvider) DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteField indicates an expected call of DeleteField.
func (mr *MockSolrProviderMockRecorder) DeleteField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteField", reflect.TypeOf((*MockSolrProvider)(nil).DeleteField), ctx, collection, document)
}
// HealthCheck mocks base method.
func (m *MockSolrProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockSolrProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockSolrProvider)(nil).HealthCheck), arg0)
}
// ListFields mocks base method.
func (m *MockSolrProvider) ListFields(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListFields", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListFields indicates an expected call of ListFields.
func (mr *MockSolrProviderMockRecorder) ListFields(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFields", reflect.TypeOf((*MockSolrProvider)(nil).ListFields), ctx, collection, params)
}
// Retrieve mocks base method.
func (m *MockSolrProvider) Retrieve(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Retrieve", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Retrieve indicates an expected call of Retrieve.
func (mr *MockSolrProviderMockRecorder) Retrieve(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Retrieve", reflect.TypeOf((*MockSolrProvider)(nil).Retrieve), ctx, collection, params)
}
// Search mocks base method.
func (m *MockSolrProvider) Search(ctx context.Context, collection string, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", ctx, collection, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search.
func (mr *MockSolrProviderMockRecorder) Search(ctx, collection, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockSolrProvider)(nil).Search), ctx, collection, params)
}
// Update mocks base method.
func (m *MockSolrProvider) Update(ctx context.Context, collection string, document *bytes.Buffer, params map[string]any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", ctx, collection, document, params)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockSolrProviderMockRecorder) Update(ctx, collection, document, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockSolrProvider)(nil).Update), ctx, collection, document, params)
}
// UpdateField mocks base method.
func (m *MockSolrProvider) UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateField", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateField indicates an expected call of UpdateField.
func (mr *MockSolrProviderMockRecorder) UpdateField(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateField", reflect.TypeOf((*MockSolrProvider)(nil).UpdateField), ctx, collection, document)
}
// UseLogger mocks base method.
func (m *MockSolrProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockSolrProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockSolrProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockSolrProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockSolrProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockSolrProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockSolrProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockSolrProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockSolrProvider)(nil).UseTracer), tracer)
}
// MockDgraph is a mock of Dgraph interface.
type MockDgraph struct {
ctrl *gomock.Controller
recorder *MockDgraphMockRecorder
isgomock struct{}
}
// MockDgraphMockRecorder is the mock recorder for MockDgraph.
type MockDgraphMockRecorder struct {
mock *MockDgraph
}
// NewMockDgraph creates a new mock instance.
func NewMockDgraph(ctrl *gomock.Controller) *MockDgraph {
mock := &MockDgraph{ctrl: ctrl}
mock.recorder = &MockDgraphMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDgraph) EXPECT() *MockDgraphMockRecorder {
return m.recorder
}
// AddOrUpdateField mocks base method.
func (m *MockDgraph) AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddOrUpdateField", ctx, fieldName, fieldType, directives)
ret0, _ := ret[0].(error)
return ret0
}
// AddOrUpdateField indicates an expected call of AddOrUpdateField.
func (mr *MockDgraphMockRecorder) AddOrUpdateField(ctx, fieldName, fieldType, directives any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateField", reflect.TypeOf((*MockDgraph)(nil).AddOrUpdateField), ctx, fieldName, fieldType, directives)
}
// Alter mocks base method.
func (m *MockDgraph) Alter(ctx context.Context, op any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Alter", ctx, op)
ret0, _ := ret[0].(error)
return ret0
}
// Alter indicates an expected call of Alter.
func (mr *MockDgraphMockRecorder) Alter(ctx, op any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraph)(nil).Alter), ctx, op)
}
// ApplySchema mocks base method.
func (m *MockDgraph) ApplySchema(ctx context.Context, schema string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplySchema", ctx, schema)
ret0, _ := ret[0].(error)
return ret0
}
// ApplySchema indicates an expected call of ApplySchema.
func (mr *MockDgraphMockRecorder) ApplySchema(ctx, schema any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySchema", reflect.TypeOf((*MockDgraph)(nil).ApplySchema), ctx, schema)
}
// DropField mocks base method.
func (m *MockDgraph) DropField(ctx context.Context, fieldName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropField", ctx, fieldName)
ret0, _ := ret[0].(error)
return ret0
}
// DropField indicates an expected call of DropField.
func (mr *MockDgraphMockRecorder) DropField(ctx, fieldName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropField", reflect.TypeOf((*MockDgraph)(nil).DropField), ctx, fieldName)
}
// HealthCheck mocks base method.
func (m *MockDgraph) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDgraphMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDgraph)(nil).HealthCheck), arg0)
}
// Mutate mocks base method.
func (m *MockDgraph) Mutate(ctx context.Context, mu any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mutate", ctx, mu)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Mutate indicates an expected call of Mutate.
func (mr *MockDgraphMockRecorder) Mutate(ctx, mu any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockDgraph)(nil).Mutate), ctx, mu)
}
// NewReadOnlyTxn mocks base method.
func (m *MockDgraph) NewReadOnlyTxn() any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewReadOnlyTxn")
ret0, _ := ret[0].(any)
return ret0
}
// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn.
func (mr *MockDgraphMockRecorder) NewReadOnlyTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraph)(nil).NewReadOnlyTxn))
}
// NewTxn mocks base method.
func (m *MockDgraph) NewTxn() any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewTxn")
ret0, _ := ret[0].(any)
return ret0
}
// NewTxn indicates an expected call of NewTxn.
func (mr *MockDgraphMockRecorder) NewTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraph)(nil).NewTxn))
}
// Query mocks base method.
func (m *MockDgraph) Query(ctx context.Context, query string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockDgraphMockRecorder) Query(ctx, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDgraph)(nil).Query), ctx, query)
}
// QueryWithVars mocks base method.
func (m *MockDgraph) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryWithVars", ctx, query, vars)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryWithVars indicates an expected call of QueryWithVars.
func (mr *MockDgraphMockRecorder) QueryWithVars(ctx, query, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockDgraph)(nil).QueryWithVars), ctx, query, vars)
}
// MockDgraphProvider is a mock of DgraphProvider interface.
type MockDgraphProvider struct {
ctrl *gomock.Controller
recorder *MockDgraphProviderMockRecorder
isgomock struct{}
}
// MockDgraphProviderMockRecorder is the mock recorder for MockDgraphProvider.
type MockDgraphProviderMockRecorder struct {
mock *MockDgraphProvider
}
// NewMockDgraphProvider creates a new mock instance.
func NewMockDgraphProvider(ctrl *gomock.Controller) *MockDgraphProvider {
mock := &MockDgraphProvider{ctrl: ctrl}
mock.recorder = &MockDgraphProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDgraphProvider) EXPECT() *MockDgraphProviderMockRecorder {
return m.recorder
}
// AddOrUpdateField mocks base method.
func (m *MockDgraphProvider) AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddOrUpdateField", ctx, fieldName, fieldType, directives)
ret0, _ := ret[0].(error)
return ret0
}
// AddOrUpdateField indicates an expected call of AddOrUpdateField.
func (mr *MockDgraphProviderMockRecorder) AddOrUpdateField(ctx, fieldName, fieldType, directives any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateField", reflect.TypeOf((*MockDgraphProvider)(nil).AddOrUpdateField), ctx, fieldName, fieldType, directives)
}
// Alter mocks base method.
func (m *MockDgraphProvider) Alter(ctx context.Context, op any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Alter", ctx, op)
ret0, _ := ret[0].(error)
return ret0
}
// Alter indicates an expected call of Alter.
func (mr *MockDgraphProviderMockRecorder) Alter(ctx, op any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraphProvider)(nil).Alter), ctx, op)
}
// ApplySchema mocks base method.
func (m *MockDgraphProvider) ApplySchema(ctx context.Context, schema string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplySchema", ctx, schema)
ret0, _ := ret[0].(error)
return ret0
}
// ApplySchema indicates an expected call of ApplySchema.
func (mr *MockDgraphProviderMockRecorder) ApplySchema(ctx, schema any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySchema", reflect.TypeOf((*MockDgraphProvider)(nil).ApplySchema), ctx, schema)
}
// Connect mocks base method.
func (m *MockDgraphProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockDgraphProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockDgraphProvider)(nil).Connect))
}
// DropField mocks base method.
func (m *MockDgraphProvider) DropField(ctx context.Context, fieldName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropField", ctx, fieldName)
ret0, _ := ret[0].(error)
return ret0
}
// DropField indicates an expected call of DropField.
func (mr *MockDgraphProviderMockRecorder) DropField(ctx, fieldName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropField", reflect.TypeOf((*MockDgraphProvider)(nil).DropField), ctx, fieldName)
}
// HealthCheck mocks base method.
func (m *MockDgraphProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDgraphProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDgraphProvider)(nil).HealthCheck), arg0)
}
// Mutate mocks base method.
func (m *MockDgraphProvider) Mutate(ctx context.Context, mu any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mutate", ctx, mu)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Mutate indicates an expected call of Mutate.
func (mr *MockDgraphProviderMockRecorder) Mutate(ctx, mu any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockDgraphProvider)(nil).Mutate), ctx, mu)
}
// NewReadOnlyTxn mocks base method.
func (m *MockDgraphProvider) NewReadOnlyTxn() any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewReadOnlyTxn")
ret0, _ := ret[0].(any)
return ret0
}
// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn.
func (mr *MockDgraphProviderMockRecorder) NewReadOnlyTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraphProvider)(nil).NewReadOnlyTxn))
}
// NewTxn mocks base method.
func (m *MockDgraphProvider) NewTxn() any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewTxn")
ret0, _ := ret[0].(any)
return ret0
}
// NewTxn indicates an expected call of NewTxn.
func (mr *MockDgraphProviderMockRecorder) NewTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraphProvider)(nil).NewTxn))
}
// Query mocks base method.
func (m *MockDgraphProvider) Query(ctx context.Context, query string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockDgraphProviderMockRecorder) Query(ctx, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDgraphProvider)(nil).Query), ctx, query)
}
// QueryWithVars mocks base method.
func (m *MockDgraphProvider) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryWithVars", ctx, query, vars)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryWithVars indicates an expected call of QueryWithVars.
func (mr *MockDgraphProviderMockRecorder) QueryWithVars(ctx, query, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockDgraphProvider)(nil).QueryWithVars), ctx, query, vars)
}
// UseLogger mocks base method.
func (m *MockDgraphProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockDgraphProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockDgraphProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockDgraphProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockDgraphProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockDgraphProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockDgraphProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockDgraphProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockDgraphProvider)(nil).UseTracer), tracer)
}
// MockOpenTSDBProvider is a mock of OpenTSDBProvider interface.
type MockOpenTSDBProvider struct {
ctrl *gomock.Controller
recorder *MockOpenTSDBProviderMockRecorder
isgomock struct{}
}
// MockOpenTSDBProviderMockRecorder is the mock recorder for MockOpenTSDBProvider.
type MockOpenTSDBProviderMockRecorder struct {
mock *MockOpenTSDBProvider
}
// NewMockOpenTSDBProvider creates a new mock instance.
func NewMockOpenTSDBProvider(ctrl *gomock.Controller) *MockOpenTSDBProvider {
mock := &MockOpenTSDBProvider{ctrl: ctrl}
mock.recorder = &MockOpenTSDBProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOpenTSDBProvider) EXPECT() *MockOpenTSDBProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockOpenTSDBProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockOpenTSDBProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockOpenTSDBProvider)(nil).Connect))
}
// DeleteAnnotation mocks base method.
func (m *MockOpenTSDBProvider) DeleteAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAnnotation indicates an expected call of DeleteAnnotation.
func (mr *MockOpenTSDBProviderMockRecorder) DeleteAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).DeleteAnnotation), ctx, annotation, res)
}
// GetAggregators mocks base method.
func (m *MockOpenTSDBProvider) GetAggregators(ctx context.Context, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregators", ctx, res)
ret0, _ := ret[0].(error)
return ret0
}
// GetAggregators indicates an expected call of GetAggregators.
func (mr *MockOpenTSDBProviderMockRecorder) GetAggregators(ctx, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregators", reflect.TypeOf((*MockOpenTSDBProvider)(nil).GetAggregators), ctx, res)
}
// HealthCheck mocks base method.
func (m *MockOpenTSDBProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockOpenTSDBProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOpenTSDBProvider)(nil).HealthCheck), arg0)
}
// PostAnnotation mocks base method.
func (m *MockOpenTSDBProvider) PostAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PostAnnotation indicates an expected call of PostAnnotation.
func (mr *MockOpenTSDBProviderMockRecorder) PostAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PostAnnotation), ctx, annotation, res)
}
// PutAnnotation mocks base method.
func (m *MockOpenTSDBProvider) PutAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutAnnotation indicates an expected call of PutAnnotation.
func (mr *MockOpenTSDBProviderMockRecorder) PutAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PutAnnotation), ctx, annotation, res)
}
// PutDataPoints mocks base method.
func (m *MockOpenTSDBProvider) PutDataPoints(ctx context.Context, data any, queryParam string, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutDataPoints", ctx, data, queryParam, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutDataPoints indicates an expected call of PutDataPoints.
func (mr *MockOpenTSDBProviderMockRecorder) PutDataPoints(ctx, data, queryParam, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).PutDataPoints), ctx, data, queryParam, res)
}
// QueryAnnotation mocks base method.
func (m *MockOpenTSDBProvider) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryAnnotation", ctx, queryAnnoParam, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryAnnotation indicates an expected call of QueryAnnotation.
func (mr *MockOpenTSDBProviderMockRecorder) QueryAnnotation(ctx, queryAnnoParam, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAnnotation", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryAnnotation), ctx, queryAnnoParam, res)
}
// QueryDataPoints mocks base method.
func (m *MockOpenTSDBProvider) QueryDataPoints(ctx context.Context, param, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryDataPoints", ctx, param, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryDataPoints indicates an expected call of QueryDataPoints.
func (mr *MockOpenTSDBProviderMockRecorder) QueryDataPoints(ctx, param, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryDataPoints), ctx, param, res)
}
// QueryLatestDataPoints mocks base method.
func (m *MockOpenTSDBProvider) QueryLatestDataPoints(ctx context.Context, param, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryLatestDataPoints", ctx, param, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryLatestDataPoints indicates an expected call of QueryLatestDataPoints.
func (mr *MockOpenTSDBProviderMockRecorder) QueryLatestDataPoints(ctx, param, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLatestDataPoints", reflect.TypeOf((*MockOpenTSDBProvider)(nil).QueryLatestDataPoints), ctx, param, res)
}
// UseLogger mocks base method.
func (m *MockOpenTSDBProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockOpenTSDBProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockOpenTSDBProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockOpenTSDBProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockOpenTSDBProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockOpenTSDBProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockOpenTSDBProvider)(nil).UseTracer), tracer)
}
// MockOpenTSDB is a mock of OpenTSDB interface.
type MockOpenTSDB struct {
ctrl *gomock.Controller
recorder *MockOpenTSDBMockRecorder
isgomock struct{}
}
// MockOpenTSDBMockRecorder is the mock recorder for MockOpenTSDB.
type MockOpenTSDBMockRecorder struct {
mock *MockOpenTSDB
}
// NewMockOpenTSDB creates a new mock instance.
func NewMockOpenTSDB(ctrl *gomock.Controller) *MockOpenTSDB {
mock := &MockOpenTSDB{ctrl: ctrl}
mock.recorder = &MockOpenTSDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOpenTSDB) EXPECT() *MockOpenTSDBMockRecorder {
return m.recorder
}
// DeleteAnnotation mocks base method.
func (m *MockOpenTSDB) DeleteAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAnnotation indicates an expected call of DeleteAnnotation.
func (mr *MockOpenTSDBMockRecorder) DeleteAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).DeleteAnnotation), ctx, annotation, res)
}
// GetAggregators mocks base method.
func (m *MockOpenTSDB) GetAggregators(ctx context.Context, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAggregators", ctx, res)
ret0, _ := ret[0].(error)
return ret0
}
// GetAggregators indicates an expected call of GetAggregators.
func (mr *MockOpenTSDBMockRecorder) GetAggregators(ctx, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAggregators", reflect.TypeOf((*MockOpenTSDB)(nil).GetAggregators), ctx, res)
}
// HealthCheck mocks base method.
func (m *MockOpenTSDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockOpenTSDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockOpenTSDB)(nil).HealthCheck), arg0)
}
// PostAnnotation mocks base method.
func (m *MockOpenTSDB) PostAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PostAnnotation indicates an expected call of PostAnnotation.
func (mr *MockOpenTSDBMockRecorder) PostAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PostAnnotation), ctx, annotation, res)
}
// PutAnnotation mocks base method.
func (m *MockOpenTSDB) PutAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutAnnotation indicates an expected call of PutAnnotation.
func (mr *MockOpenTSDBMockRecorder) PutAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PutAnnotation), ctx, annotation, res)
}
// PutDataPoints mocks base method.
func (m *MockOpenTSDB) PutDataPoints(ctx context.Context, data any, queryParam string, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutDataPoints", ctx, data, queryParam, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutDataPoints indicates an expected call of PutDataPoints.
func (mr *MockOpenTSDBMockRecorder) PutDataPoints(ctx, data, queryParam, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).PutDataPoints), ctx, data, queryParam, res)
}
// QueryAnnotation mocks base method.
func (m *MockOpenTSDB) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryAnnotation", ctx, queryAnnoParam, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryAnnotation indicates an expected call of QueryAnnotation.
func (mr *MockOpenTSDBMockRecorder) QueryAnnotation(ctx, queryAnnoParam, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).QueryAnnotation), ctx, queryAnnoParam, res)
}
// QueryDataPoints mocks base method.
func (m *MockOpenTSDB) QueryDataPoints(ctx context.Context, param, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryDataPoints", ctx, param, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryDataPoints indicates an expected call of QueryDataPoints.
func (mr *MockOpenTSDBMockRecorder) QueryDataPoints(ctx, param, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).QueryDataPoints), ctx, param, res)
}
// QueryLatestDataPoints mocks base method.
func (m *MockOpenTSDB) QueryLatestDataPoints(ctx context.Context, param, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryLatestDataPoints", ctx, param, res)
ret0, _ := ret[0].(error)
return ret0
}
// QueryLatestDataPoints indicates an expected call of QueryLatestDataPoints.
func (mr *MockOpenTSDBMockRecorder) QueryLatestDataPoints(ctx, param, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryLatestDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).QueryLatestDataPoints), ctx, param, res)
}
// MockScyllaDB is a mock of ScyllaDB interface.
type MockScyllaDB struct {
ctrl *gomock.Controller
recorder *MockScyllaDBMockRecorder
isgomock struct{}
}
// MockScyllaDBMockRecorder is the mock recorder for MockScyllaDB.
type MockScyllaDBMockRecorder struct {
mock *MockScyllaDB
}
// NewMockScyllaDB creates a new mock instance.
func NewMockScyllaDB(ctrl *gomock.Controller) *MockScyllaDB {
mock := &MockScyllaDB{ctrl: ctrl}
mock.recorder = &MockScyllaDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockScyllaDB) EXPECT() *MockScyllaDBMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockScyllaDB) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockScyllaDBMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockScyllaDB)(nil).BatchQuery), varargs...)
}
// BatchQueryWithCtx mocks base method.
func (m *MockScyllaDB) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockScyllaDBMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).BatchQueryWithCtx), varargs...)
}
// Exec mocks base method.
func (m *MockScyllaDB) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockScyllaDBMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockScyllaDB)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockScyllaDB) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockScyllaDBMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockScyllaDB)(nil).ExecCAS), varargs...)
}
// ExecWithCtx mocks base method.
func (m *MockScyllaDB) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecWithCtx indicates an expected call of ExecWithCtx.
func (mr *MockScyllaDBMockRecorder) ExecWithCtx(ctx, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).ExecWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockScyllaDB) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockScyllaDBMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).ExecuteBatchWithCtx), ctx, name)
}
// HealthCheck mocks base method.
func (m *MockScyllaDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockScyllaDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockScyllaDB)(nil).HealthCheck), arg0)
}
// NewBatch mocks base method.
func (m *MockScyllaDB) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockScyllaDBMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockScyllaDB)(nil).NewBatch), name, batchType)
}
// NewBatchWithCtx mocks base method.
func (m *MockScyllaDB) NewBatchWithCtx(arg0 context.Context, name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatchWithCtx", arg0, name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatchWithCtx indicates an expected call of NewBatchWithCtx.
func (mr *MockScyllaDBMockRecorder) NewBatchWithCtx(arg0, name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).NewBatchWithCtx), arg0, name, batchType)
}
// Query mocks base method.
func (m *MockScyllaDB) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockScyllaDBMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockScyllaDB)(nil).Query), varargs...)
}
// QueryWithCtx mocks base method.
func (m *MockScyllaDB) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryWithCtx indicates an expected call of QueryWithCtx.
func (mr *MockScyllaDBMockRecorder) QueryWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).QueryWithCtx), varargs...)
}
// MockScyllaDBProvider is a mock of ScyllaDBProvider interface.
type MockScyllaDBProvider struct {
ctrl *gomock.Controller
recorder *MockScyllaDBProviderMockRecorder
isgomock struct{}
}
// MockScyllaDBProviderMockRecorder is the mock recorder for MockScyllaDBProvider.
type MockScyllaDBProviderMockRecorder struct {
mock *MockScyllaDBProvider
}
// NewMockScyllaDBProvider creates a new mock instance.
func NewMockScyllaDBProvider(ctrl *gomock.Controller) *MockScyllaDBProvider {
mock := &MockScyllaDBProvider{ctrl: ctrl}
mock.recorder = &MockScyllaDBProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockScyllaDBProvider) EXPECT() *MockScyllaDBProviderMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockScyllaDBProvider) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockScyllaDBProviderMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockScyllaDBProvider)(nil).BatchQuery), varargs...)
}
// BatchQueryWithCtx mocks base method.
func (m *MockScyllaDBProvider) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockScyllaDBProviderMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockScyllaDBProvider)(nil).BatchQueryWithCtx), varargs...)
}
// Connect mocks base method.
func (m *MockScyllaDBProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockScyllaDBProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockScyllaDBProvider)(nil).Connect))
}
// Exec mocks base method.
func (m *MockScyllaDBProvider) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockScyllaDBProviderMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockScyllaDBProvider)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockScyllaDBProvider) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockScyllaDBProviderMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockScyllaDBProvider)(nil).ExecCAS), varargs...)
}
// ExecWithCtx mocks base method.
func (m *MockScyllaDBProvider) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecWithCtx indicates an expected call of ExecWithCtx.
func (mr *MockScyllaDBProviderMockRecorder) ExecWithCtx(ctx, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecWithCtx", reflect.TypeOf((*MockScyllaDBProvider)(nil).ExecWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockScyllaDBProvider) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockScyllaDBProviderMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockScyllaDBProvider)(nil).ExecuteBatchWithCtx), ctx, name)
}
// HealthCheck mocks base method.
func (m *MockScyllaDBProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockScyllaDBProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockScyllaDBProvider)(nil).HealthCheck), arg0)
}
// NewBatch mocks base method.
func (m *MockScyllaDBProvider) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockScyllaDBProviderMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockScyllaDBProvider)(nil).NewBatch), name, batchType)
}
// NewBatchWithCtx mocks base method.
func (m *MockScyllaDBProvider) NewBatchWithCtx(arg0 context.Context, name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatchWithCtx", arg0, name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatchWithCtx indicates an expected call of NewBatchWithCtx.
func (mr *MockScyllaDBProviderMockRecorder) NewBatchWithCtx(arg0, name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithCtx", reflect.TypeOf((*MockScyllaDBProvider)(nil).NewBatchWithCtx), arg0, name, batchType)
}
// Query mocks base method.
func (m *MockScyllaDBProvider) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockScyllaDBProviderMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockScyllaDBProvider)(nil).Query), varargs...)
}
// QueryWithCtx mocks base method.
func (m *MockScyllaDBProvider) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryWithCtx indicates an expected call of QueryWithCtx.
func (mr *MockScyllaDBProviderMockRecorder) QueryWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithCtx", reflect.TypeOf((*MockScyllaDBProvider)(nil).QueryWithCtx), varargs...)
}
// UseLogger mocks base method.
func (m *MockScyllaDBProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockScyllaDBProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockScyllaDBProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockScyllaDBProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockScyllaDBProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockScyllaDBProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockScyllaDBProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockScyllaDBProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockScyllaDBProvider)(nil).UseTracer), tracer)
}
// MockArangoDB is a mock of ArangoDB interface.
type MockArangoDB struct {
ctrl *gomock.Controller
recorder *MockArangoDBMockRecorder
isgomock struct{}
}
// MockArangoDBMockRecorder is the mock recorder for MockArangoDB.
type MockArangoDBMockRecorder struct {
mock *MockArangoDB
}
// NewMockArangoDB creates a new mock instance.
func NewMockArangoDB(ctrl *gomock.Controller) *MockArangoDB {
mock := &MockArangoDB{ctrl: ctrl}
mock.recorder = &MockArangoDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockArangoDB) EXPECT() *MockArangoDBMockRecorder {
return m.recorder
}
// CreateCollection mocks base method.
func (m *MockArangoDB) CreateCollection(ctx context.Context, database, collection string, isEdge bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, database, collection, isEdge)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockArangoDBMockRecorder) CreateCollection(ctx, database, collection, isEdge any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockArangoDB)(nil).CreateCollection), ctx, database, collection, isEdge)
}
// CreateDB mocks base method.
func (m *MockArangoDB) CreateDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDB indicates an expected call of CreateDB.
func (mr *MockArangoDBMockRecorder) CreateDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDB", reflect.TypeOf((*MockArangoDB)(nil).CreateDB), ctx, database)
}
// CreateDocument mocks base method.
func (m *MockArangoDB) CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocument", ctx, dbName, collectionName, document)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocument indicates an expected call of CreateDocument.
func (mr *MockArangoDBMockRecorder) CreateDocument(ctx, dbName, collectionName, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocument", reflect.TypeOf((*MockArangoDB)(nil).CreateDocument), ctx, dbName, collectionName, document)
}
// CreateGraph mocks base method.
func (m *MockArangoDB) CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateGraph", ctx, database, graph, edgeDefinitions)
ret0, _ := ret[0].(error)
return ret0
}
// CreateGraph indicates an expected call of CreateGraph.
func (mr *MockArangoDBMockRecorder) CreateGraph(ctx, database, graph, edgeDefinitions any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGraph", reflect.TypeOf((*MockArangoDB)(nil).CreateGraph), ctx, database, graph, edgeDefinitions)
}
// DeleteDocument mocks base method.
func (m *MockArangoDB) DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, dbName, collectionName, documentID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockArangoDBMockRecorder) DeleteDocument(ctx, dbName, collectionName, documentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockArangoDB)(nil).DeleteDocument), ctx, dbName, collectionName, documentID)
}
// DropCollection mocks base method.
func (m *MockArangoDB) DropCollection(ctx context.Context, database, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropCollection", ctx, database, collection)
ret0, _ := ret[0].(error)
return ret0
}
// DropCollection indicates an expected call of DropCollection.
func (mr *MockArangoDBMockRecorder) DropCollection(ctx, database, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropCollection", reflect.TypeOf((*MockArangoDB)(nil).DropCollection), ctx, database, collection)
}
// DropDB mocks base method.
func (m *MockArangoDB) DropDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDB indicates an expected call of DropDB.
func (mr *MockArangoDBMockRecorder) DropDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDB", reflect.TypeOf((*MockArangoDB)(nil).DropDB), ctx, database)
}
// DropGraph mocks base method.
func (m *MockArangoDB) DropGraph(ctx context.Context, database, graph string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropGraph", ctx, database, graph)
ret0, _ := ret[0].(error)
return ret0
}
// DropGraph indicates an expected call of DropGraph.
func (mr *MockArangoDBMockRecorder) DropGraph(ctx, database, graph any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropGraph", reflect.TypeOf((*MockArangoDB)(nil).DropGraph), ctx, database, graph)
}
// GetDocument mocks base method.
func (m *MockArangoDB) GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDocument", ctx, dbName, collectionName, documentID, result)
ret0, _ := ret[0].(error)
return ret0
}
// GetDocument indicates an expected call of GetDocument.
func (mr *MockArangoDBMockRecorder) GetDocument(ctx, dbName, collectionName, documentID, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockArangoDB)(nil).GetDocument), ctx, dbName, collectionName, documentID, result)
}
// GetEdges mocks base method.
func (m *MockArangoDB) GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string, resp any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdges", ctx, dbName, graphName, edgeCollection, vertexID, resp)
ret0, _ := ret[0].(error)
return ret0
}
// GetEdges indicates an expected call of GetEdges.
func (mr *MockArangoDBMockRecorder) GetEdges(ctx, dbName, graphName, edgeCollection, vertexID, resp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdges", reflect.TypeOf((*MockArangoDB)(nil).GetEdges), ctx, dbName, graphName, edgeCollection, vertexID, resp)
}
// HealthCheck mocks base method.
func (m *MockArangoDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockArangoDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockArangoDB)(nil).HealthCheck), arg0)
}
// Query mocks base method.
func (m *MockArangoDB) Query(ctx context.Context, dbName, query string, bindVars map[string]any, result any, options ...map[string]any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dbName, query, bindVars, result}
for _, a := range options {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockArangoDBMockRecorder) Query(ctx, dbName, query, bindVars, result any, options ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dbName, query, bindVars, result}, options...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockArangoDB)(nil).Query), varargs...)
}
// UpdateDocument mocks base method.
func (m *MockArangoDB) UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, dbName, collectionName, documentID, document)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockArangoDBMockRecorder) UpdateDocument(ctx, dbName, collectionName, documentID, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockArangoDB)(nil).UpdateDocument), ctx, dbName, collectionName, documentID, document)
}
// MockArangoDBProvider is a mock of ArangoDBProvider interface.
type MockArangoDBProvider struct {
ctrl *gomock.Controller
recorder *MockArangoDBProviderMockRecorder
isgomock struct{}
}
// MockArangoDBProviderMockRecorder is the mock recorder for MockArangoDBProvider.
type MockArangoDBProviderMockRecorder struct {
mock *MockArangoDBProvider
}
// NewMockArangoDBProvider creates a new mock instance.
func NewMockArangoDBProvider(ctrl *gomock.Controller) *MockArangoDBProvider {
mock := &MockArangoDBProvider{ctrl: ctrl}
mock.recorder = &MockArangoDBProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockArangoDBProvider) EXPECT() *MockArangoDBProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockArangoDBProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockArangoDBProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockArangoDBProvider)(nil).Connect))
}
// CreateCollection mocks base method.
func (m *MockArangoDBProvider) CreateCollection(ctx context.Context, database, collection string, isEdge bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, database, collection, isEdge)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockArangoDBProviderMockRecorder) CreateCollection(ctx, database, collection, isEdge any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockArangoDBProvider)(nil).CreateCollection), ctx, database, collection, isEdge)
}
// CreateDB mocks base method.
func (m *MockArangoDBProvider) CreateDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDB indicates an expected call of CreateDB.
func (mr *MockArangoDBProviderMockRecorder) CreateDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDB", reflect.TypeOf((*MockArangoDBProvider)(nil).CreateDB), ctx, database)
}
// CreateDocument mocks base method.
func (m *MockArangoDBProvider) CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocument", ctx, dbName, collectionName, document)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocument indicates an expected call of CreateDocument.
func (mr *MockArangoDBProviderMockRecorder) CreateDocument(ctx, dbName, collectionName, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocument", reflect.TypeOf((*MockArangoDBProvider)(nil).CreateDocument), ctx, dbName, collectionName, document)
}
// CreateGraph mocks base method.
func (m *MockArangoDBProvider) CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateGraph", ctx, database, graph, edgeDefinitions)
ret0, _ := ret[0].(error)
return ret0
}
// CreateGraph indicates an expected call of CreateGraph.
func (mr *MockArangoDBProviderMockRecorder) CreateGraph(ctx, database, graph, edgeDefinitions any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGraph", reflect.TypeOf((*MockArangoDBProvider)(nil).CreateGraph), ctx, database, graph, edgeDefinitions)
}
// DeleteDocument mocks base method.
func (m *MockArangoDBProvider) DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, dbName, collectionName, documentID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockArangoDBProviderMockRecorder) DeleteDocument(ctx, dbName, collectionName, documentID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockArangoDBProvider)(nil).DeleteDocument), ctx, dbName, collectionName, documentID)
}
// DropCollection mocks base method.
func (m *MockArangoDBProvider) DropCollection(ctx context.Context, database, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropCollection", ctx, database, collection)
ret0, _ := ret[0].(error)
return ret0
}
// DropCollection indicates an expected call of DropCollection.
func (mr *MockArangoDBProviderMockRecorder) DropCollection(ctx, database, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropCollection", reflect.TypeOf((*MockArangoDBProvider)(nil).DropCollection), ctx, database, collection)
}
// DropDB mocks base method.
func (m *MockArangoDBProvider) DropDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDB indicates an expected call of DropDB.
func (mr *MockArangoDBProviderMockRecorder) DropDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDB", reflect.TypeOf((*MockArangoDBProvider)(nil).DropDB), ctx, database)
}
// DropGraph mocks base method.
func (m *MockArangoDBProvider) DropGraph(ctx context.Context, database, graph string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropGraph", ctx, database, graph)
ret0, _ := ret[0].(error)
return ret0
}
// DropGraph indicates an expected call of DropGraph.
func (mr *MockArangoDBProviderMockRecorder) DropGraph(ctx, database, graph any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropGraph", reflect.TypeOf((*MockArangoDBProvider)(nil).DropGraph), ctx, database, graph)
}
// GetDocument mocks base method.
func (m *MockArangoDBProvider) GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDocument", ctx, dbName, collectionName, documentID, result)
ret0, _ := ret[0].(error)
return ret0
}
// GetDocument indicates an expected call of GetDocument.
func (mr *MockArangoDBProviderMockRecorder) GetDocument(ctx, dbName, collectionName, documentID, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockArangoDBProvider)(nil).GetDocument), ctx, dbName, collectionName, documentID, result)
}
// GetEdges mocks base method.
func (m *MockArangoDBProvider) GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string, resp any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdges", ctx, dbName, graphName, edgeCollection, vertexID, resp)
ret0, _ := ret[0].(error)
return ret0
}
// GetEdges indicates an expected call of GetEdges.
func (mr *MockArangoDBProviderMockRecorder) GetEdges(ctx, dbName, graphName, edgeCollection, vertexID, resp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdges", reflect.TypeOf((*MockArangoDBProvider)(nil).GetEdges), ctx, dbName, graphName, edgeCollection, vertexID, resp)
}
// HealthCheck mocks base method.
func (m *MockArangoDBProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockArangoDBProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockArangoDBProvider)(nil).HealthCheck), arg0)
}
// Query mocks base method.
func (m *MockArangoDBProvider) Query(ctx context.Context, dbName, query string, bindVars map[string]any, result any, options ...map[string]any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dbName, query, bindVars, result}
for _, a := range options {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockArangoDBProviderMockRecorder) Query(ctx, dbName, query, bindVars, result any, options ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dbName, query, bindVars, result}, options...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockArangoDBProvider)(nil).Query), varargs...)
}
// UpdateDocument mocks base method.
func (m *MockArangoDBProvider) UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, dbName, collectionName, documentID, document)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockArangoDBProviderMockRecorder) UpdateDocument(ctx, dbName, collectionName, documentID, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockArangoDBProvider)(nil).UpdateDocument), ctx, dbName, collectionName, documentID, document)
}
// UseLogger mocks base method.
func (m *MockArangoDBProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockArangoDBProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockArangoDBProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockArangoDBProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockArangoDBProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockArangoDBProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockArangoDBProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockArangoDBProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockArangoDBProvider)(nil).UseTracer), tracer)
}
// MockElasticsearch is a mock of Elasticsearch interface.
type MockElasticsearch struct {
ctrl *gomock.Controller
recorder *MockElasticsearchMockRecorder
isgomock struct{}
}
// MockElasticsearchMockRecorder is the mock recorder for MockElasticsearch.
type MockElasticsearchMockRecorder struct {
mock *MockElasticsearch
}
// NewMockElasticsearch creates a new mock instance.
func NewMockElasticsearch(ctrl *gomock.Controller) *MockElasticsearch {
mock := &MockElasticsearch{ctrl: ctrl}
mock.recorder = &MockElasticsearchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockElasticsearch) EXPECT() *MockElasticsearchMockRecorder {
return m.recorder
}
// Bulk mocks base method.
func (m *MockElasticsearch) Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bulk", ctx, operations)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Bulk indicates an expected call of Bulk.
func (mr *MockElasticsearchMockRecorder) Bulk(ctx, operations any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bulk", reflect.TypeOf((*MockElasticsearch)(nil).Bulk), ctx, operations)
}
// CreateIndex mocks base method.
func (m *MockElasticsearch) CreateIndex(ctx context.Context, index string, settings map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateIndex", ctx, index, settings)
ret0, _ := ret[0].(error)
return ret0
}
// CreateIndex indicates an expected call of CreateIndex.
func (mr *MockElasticsearchMockRecorder) CreateIndex(ctx, index, settings any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateIndex", reflect.TypeOf((*MockElasticsearch)(nil).CreateIndex), ctx, index, settings)
}
// DeleteDocument mocks base method.
func (m *MockElasticsearch) DeleteDocument(ctx context.Context, index, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, index, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockElasticsearchMockRecorder) DeleteDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockElasticsearch)(nil).DeleteDocument), ctx, index, id)
}
// DeleteIndex mocks base method.
func (m *MockElasticsearch) DeleteIndex(ctx context.Context, index string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIndex", ctx, index)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIndex indicates an expected call of DeleteIndex.
func (mr *MockElasticsearchMockRecorder) DeleteIndex(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndex", reflect.TypeOf((*MockElasticsearch)(nil).DeleteIndex), ctx, index)
}
// GetDocument mocks base method.
func (m *MockElasticsearch) GetDocument(ctx context.Context, index, id string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDocument", ctx, index, id)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDocument indicates an expected call of GetDocument.
func (mr *MockElasticsearchMockRecorder) GetDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockElasticsearch)(nil).GetDocument), ctx, index, id)
}
// HealthCheck mocks base method.
func (m *MockElasticsearch) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockElasticsearchMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockElasticsearch)(nil).HealthCheck), arg0)
}
// IndexDocument mocks base method.
func (m *MockElasticsearch) IndexDocument(ctx context.Context, index, id string, document any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IndexDocument", ctx, index, id, document)
ret0, _ := ret[0].(error)
return ret0
}
// IndexDocument indicates an expected call of IndexDocument.
func (mr *MockElasticsearchMockRecorder) IndexDocument(ctx, index, id, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexDocument", reflect.TypeOf((*MockElasticsearch)(nil).IndexDocument), ctx, index, id, document)
}
// Search mocks base method.
func (m *MockElasticsearch) Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", ctx, indices, query)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search.
func (mr *MockElasticsearchMockRecorder) Search(ctx, indices, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockElasticsearch)(nil).Search), ctx, indices, query)
}
// UpdateDocument mocks base method.
func (m *MockElasticsearch) UpdateDocument(ctx context.Context, index, id string, update map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, index, id, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockElasticsearchMockRecorder) UpdateDocument(ctx, index, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockElasticsearch)(nil).UpdateDocument), ctx, index, id, update)
}
// MockElasticsearchProvider is a mock of ElasticsearchProvider interface.
type MockElasticsearchProvider struct {
ctrl *gomock.Controller
recorder *MockElasticsearchProviderMockRecorder
isgomock struct{}
}
// MockElasticsearchProviderMockRecorder is the mock recorder for MockElasticsearchProvider.
type MockElasticsearchProviderMockRecorder struct {
mock *MockElasticsearchProvider
}
// NewMockElasticsearchProvider creates a new mock instance.
func NewMockElasticsearchProvider(ctrl *gomock.Controller) *MockElasticsearchProvider {
mock := &MockElasticsearchProvider{ctrl: ctrl}
mock.recorder = &MockElasticsearchProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockElasticsearchProvider) EXPECT() *MockElasticsearchProviderMockRecorder {
return m.recorder
}
// Bulk mocks base method.
func (m *MockElasticsearchProvider) Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bulk", ctx, operations)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Bulk indicates an expected call of Bulk.
func (mr *MockElasticsearchProviderMockRecorder) Bulk(ctx, operations any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bulk", reflect.TypeOf((*MockElasticsearchProvider)(nil).Bulk), ctx, operations)
}
// Connect mocks base method.
func (m *MockElasticsearchProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockElasticsearchProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockElasticsearchProvider)(nil).Connect))
}
// CreateIndex mocks base method.
func (m *MockElasticsearchProvider) CreateIndex(ctx context.Context, index string, settings map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateIndex", ctx, index, settings)
ret0, _ := ret[0].(error)
return ret0
}
// CreateIndex indicates an expected call of CreateIndex.
func (mr *MockElasticsearchProviderMockRecorder) CreateIndex(ctx, index, settings any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateIndex", reflect.TypeOf((*MockElasticsearchProvider)(nil).CreateIndex), ctx, index, settings)
}
// DeleteDocument mocks base method.
func (m *MockElasticsearchProvider) DeleteDocument(ctx context.Context, index, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, index, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockElasticsearchProviderMockRecorder) DeleteDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockElasticsearchProvider)(nil).DeleteDocument), ctx, index, id)
}
// DeleteIndex mocks base method.
func (m *MockElasticsearchProvider) DeleteIndex(ctx context.Context, index string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIndex", ctx, index)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIndex indicates an expected call of DeleteIndex.
func (mr *MockElasticsearchProviderMockRecorder) DeleteIndex(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndex", reflect.TypeOf((*MockElasticsearchProvider)(nil).DeleteIndex), ctx, index)
}
// GetDocument mocks base method.
func (m *MockElasticsearchProvider) GetDocument(ctx context.Context, index, id string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDocument", ctx, index, id)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDocument indicates an expected call of GetDocument.
func (mr *MockElasticsearchProviderMockRecorder) GetDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockElasticsearchProvider)(nil).GetDocument), ctx, index, id)
}
// HealthCheck mocks base method.
func (m *MockElasticsearchProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockElasticsearchProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockElasticsearchProvider)(nil).HealthCheck), arg0)
}
// IndexDocument mocks base method.
func (m *MockElasticsearchProvider) IndexDocument(ctx context.Context, index, id string, document any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IndexDocument", ctx, index, id, document)
ret0, _ := ret[0].(error)
return ret0
}
// IndexDocument indicates an expected call of IndexDocument.
func (mr *MockElasticsearchProviderMockRecorder) IndexDocument(ctx, index, id, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexDocument", reflect.TypeOf((*MockElasticsearchProvider)(nil).IndexDocument), ctx, index, id, document)
}
// Search mocks base method.
func (m *MockElasticsearchProvider) Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", ctx, indices, query)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search.
func (mr *MockElasticsearchProviderMockRecorder) Search(ctx, indices, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockElasticsearchProvider)(nil).Search), ctx, indices, query)
}
// UpdateDocument mocks base method.
func (m *MockElasticsearchProvider) UpdateDocument(ctx context.Context, index, id string, update map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, index, id, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockElasticsearchProviderMockRecorder) UpdateDocument(ctx, index, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockElasticsearchProvider)(nil).UpdateDocument), ctx, index, id, update)
}
// UseLogger mocks base method.
func (m *MockElasticsearchProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockElasticsearchProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockElasticsearchProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockElasticsearchProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockElasticsearchProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockElasticsearchProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockElasticsearchProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockElasticsearchProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockElasticsearchProvider)(nil).UseTracer), tracer)
}
// MockCouchbase is a mock of Couchbase interface.
type MockCouchbase struct {
ctrl *gomock.Controller
recorder *MockCouchbaseMockRecorder
isgomock struct{}
}
// MockCouchbaseMockRecorder is the mock recorder for MockCouchbase.
type MockCouchbaseMockRecorder struct {
mock *MockCouchbase
}
// NewMockCouchbase creates a new mock instance.
func NewMockCouchbase(ctrl *gomock.Controller) *MockCouchbase {
mock := &MockCouchbase{ctrl: ctrl}
mock.recorder = &MockCouchbaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCouchbase) EXPECT() *MockCouchbaseMockRecorder {
return m.recorder
}
// AnalyticsQuery mocks base method.
func (m *MockCouchbase) AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AnalyticsQuery", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// AnalyticsQuery indicates an expected call of AnalyticsQuery.
func (mr *MockCouchbaseMockRecorder) AnalyticsQuery(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnalyticsQuery", reflect.TypeOf((*MockCouchbase)(nil).AnalyticsQuery), ctx, statement, params, result)
}
// Close mocks base method.
func (m *MockCouchbase) Close(opts any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close", opts)
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockCouchbaseMockRecorder) Close(opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockCouchbase)(nil).Close), opts)
}
// Get mocks base method.
func (m *MockCouchbase) Get(ctx context.Context, key string, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key, result)
ret0, _ := ret[0].(error)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockCouchbaseMockRecorder) Get(ctx, key, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCouchbase)(nil).Get), ctx, key, result)
}
// HealthCheck mocks base method.
func (m *MockCouchbase) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCouchbaseMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCouchbase)(nil).HealthCheck), arg0)
}
// Insert mocks base method.
func (m *MockCouchbase) Insert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockCouchbaseMockRecorder) Insert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockCouchbase)(nil).Insert), ctx, key, document, result)
}
// Query mocks base method.
func (m *MockCouchbase) Query(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCouchbaseMockRecorder) Query(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCouchbase)(nil).Query), ctx, statement, params, result)
}
// Remove mocks base method.
func (m *MockCouchbase) Remove(ctx context.Context, key string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, key)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockCouchbaseMockRecorder) Remove(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCouchbase)(nil).Remove), ctx, key)
}
// RunTransaction mocks base method.
func (m *MockCouchbase) RunTransaction(ctx context.Context, logic func(any) error) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunTransaction", ctx, logic)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunTransaction indicates an expected call of RunTransaction.
func (mr *MockCouchbaseMockRecorder) RunTransaction(ctx, logic any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunTransaction", reflect.TypeOf((*MockCouchbase)(nil).RunTransaction), ctx, logic)
}
// Upsert mocks base method.
func (m *MockCouchbase) Upsert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upsert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Upsert indicates an expected call of Upsert.
func (mr *MockCouchbaseMockRecorder) Upsert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockCouchbase)(nil).Upsert), ctx, key, document, result)
}
// MockCouchbaseProvider is a mock of CouchbaseProvider interface.
type MockCouchbaseProvider struct {
ctrl *gomock.Controller
recorder *MockCouchbaseProviderMockRecorder
isgomock struct{}
}
// MockCouchbaseProviderMockRecorder is the mock recorder for MockCouchbaseProvider.
type MockCouchbaseProviderMockRecorder struct {
mock *MockCouchbaseProvider
}
// NewMockCouchbaseProvider creates a new mock instance.
func NewMockCouchbaseProvider(ctrl *gomock.Controller) *MockCouchbaseProvider {
mock := &MockCouchbaseProvider{ctrl: ctrl}
mock.recorder = &MockCouchbaseProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCouchbaseProvider) EXPECT() *MockCouchbaseProviderMockRecorder {
return m.recorder
}
// AnalyticsQuery mocks base method.
func (m *MockCouchbaseProvider) AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AnalyticsQuery", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// AnalyticsQuery indicates an expected call of AnalyticsQuery.
func (mr *MockCouchbaseProviderMockRecorder) AnalyticsQuery(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnalyticsQuery", reflect.TypeOf((*MockCouchbaseProvider)(nil).AnalyticsQuery), ctx, statement, params, result)
}
// Close mocks base method.
func (m *MockCouchbaseProvider) Close(opts any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close", opts)
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockCouchbaseProviderMockRecorder) Close(opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockCouchbaseProvider)(nil).Close), opts)
}
// Connect mocks base method.
func (m *MockCouchbaseProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockCouchbaseProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockCouchbaseProvider)(nil).Connect))
}
// Get mocks base method.
func (m *MockCouchbaseProvider) Get(ctx context.Context, key string, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key, result)
ret0, _ := ret[0].(error)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockCouchbaseProviderMockRecorder) Get(ctx, key, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCouchbaseProvider)(nil).Get), ctx, key, result)
}
// HealthCheck mocks base method.
func (m *MockCouchbaseProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCouchbaseProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCouchbaseProvider)(nil).HealthCheck), arg0)
}
// Insert mocks base method.
func (m *MockCouchbaseProvider) Insert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockCouchbaseProviderMockRecorder) Insert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockCouchbaseProvider)(nil).Insert), ctx, key, document, result)
}
// Query mocks base method.
func (m *MockCouchbaseProvider) Query(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCouchbaseProviderMockRecorder) Query(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCouchbaseProvider)(nil).Query), ctx, statement, params, result)
}
// Remove mocks base method.
func (m *MockCouchbaseProvider) Remove(ctx context.Context, key string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, key)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockCouchbaseProviderMockRecorder) Remove(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCouchbaseProvider)(nil).Remove), ctx, key)
}
// RunTransaction mocks base method.
func (m *MockCouchbaseProvider) RunTransaction(ctx context.Context, logic func(any) error) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunTransaction", ctx, logic)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunTransaction indicates an expected call of RunTransaction.
func (mr *MockCouchbaseProviderMockRecorder) RunTransaction(ctx, logic any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunTransaction", reflect.TypeOf((*MockCouchbaseProvider)(nil).RunTransaction), ctx, logic)
}
// Upsert mocks base method.
func (m *MockCouchbaseProvider) Upsert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upsert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Upsert indicates an expected call of Upsert.
func (mr *MockCouchbaseProviderMockRecorder) Upsert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockCouchbaseProvider)(nil).Upsert), ctx, key, document, result)
}
// UseLogger mocks base method.
func (m *MockCouchbaseProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockCouchbaseProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockCouchbaseProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockCouchbaseProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockCouchbaseProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockCouchbaseProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockCouchbaseProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockCouchbaseProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockCouchbaseProvider)(nil).UseTracer), tracer)
}
// MockDBResolverProvider is a mock of DBResolverProvider interface.
type MockDBResolverProvider struct {
ctrl *gomock.Controller
recorder *MockDBResolverProviderMockRecorder
isgomock struct{}
}
// MockDBResolverProviderMockRecorder is the mock recorder for MockDBResolverProvider.
type MockDBResolverProviderMockRecorder struct {
mock *MockDBResolverProvider
}
// NewMockDBResolverProvider creates a new mock instance.
func NewMockDBResolverProvider(ctrl *gomock.Controller) *MockDBResolverProvider {
mock := &MockDBResolverProvider{ctrl: ctrl}
mock.recorder = &MockDBResolverProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDBResolverProvider) EXPECT() *MockDBResolverProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockDBResolverProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockDBResolverProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockDBResolverProvider)(nil).Connect))
}
// GetResolver mocks base method.
func (m *MockDBResolverProvider) GetResolver() DB {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetResolver")
ret0, _ := ret[0].(DB)
return ret0
}
// GetResolver indicates an expected call of GetResolver.
func (mr *MockDBResolverProviderMockRecorder) GetResolver() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResolver", reflect.TypeOf((*MockDBResolverProvider)(nil).GetResolver))
}
// UseLogger mocks base method.
func (m *MockDBResolverProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockDBResolverProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockDBResolverProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockDBResolverProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockDBResolverProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockDBResolverProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockDBResolverProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockDBResolverProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockDBResolverProvider)(nil).UseTracer), tracer)
}
// MockInfluxDB is a mock of InfluxDB interface.
type MockInfluxDB struct {
ctrl *gomock.Controller
recorder *MockInfluxDBMockRecorder
isgomock struct{}
}
// MockInfluxDBMockRecorder is the mock recorder for MockInfluxDB.
type MockInfluxDBMockRecorder struct {
mock *MockInfluxDB
}
// NewMockInfluxDB creates a new mock instance.
func NewMockInfluxDB(ctrl *gomock.Controller) *MockInfluxDB {
mock := &MockInfluxDB{ctrl: ctrl}
mock.recorder = &MockInfluxDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInfluxDB) EXPECT() *MockInfluxDBMockRecorder {
return m.recorder
}
// CreateBucket mocks base method.
func (m *MockInfluxDB) CreateBucket(ctx context.Context, org, bucket string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateBucket", ctx, org, bucket)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateBucket indicates an expected call of CreateBucket.
func (mr *MockInfluxDBMockRecorder) CreateBucket(ctx, org, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockInfluxDB)(nil).CreateBucket), ctx, org, bucket)
}
// CreateOrganization mocks base method.
func (m *MockInfluxDB) CreateOrganization(ctx context.Context, org string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrganization", ctx, org)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrganization indicates an expected call of CreateOrganization.
func (mr *MockInfluxDBMockRecorder) CreateOrganization(ctx, org any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrganization", reflect.TypeOf((*MockInfluxDB)(nil).CreateOrganization), ctx, org)
}
// DeleteBucket mocks base method.
func (m *MockInfluxDB) DeleteBucket(ctx context.Context, bucketID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBucket", ctx, bucketID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBucket indicates an expected call of DeleteBucket.
func (mr *MockInfluxDBMockRecorder) DeleteBucket(ctx, bucketID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockInfluxDB)(nil).DeleteBucket), ctx, bucketID)
}
// DeleteOrganization mocks base method.
func (m *MockInfluxDB) DeleteOrganization(ctx context.Context, orgID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOrganization", ctx, orgID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOrganization indicates an expected call of DeleteOrganization.
func (mr *MockInfluxDBMockRecorder) DeleteOrganization(ctx, orgID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganization", reflect.TypeOf((*MockInfluxDB)(nil).DeleteOrganization), ctx, orgID)
}
// HealthCheck mocks base method.
func (m *MockInfluxDB) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockInfluxDBMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockInfluxDB)(nil).HealthCheck), arg0)
}
// ListBuckets mocks base method.
func (m *MockInfluxDB) ListBuckets(ctx context.Context, org string) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListBuckets", ctx, org)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListBuckets indicates an expected call of ListBuckets.
func (mr *MockInfluxDBMockRecorder) ListBuckets(ctx, org any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockInfluxDB)(nil).ListBuckets), ctx, org)
}
// ListOrganization mocks base method.
func (m *MockInfluxDB) ListOrganization(ctx context.Context) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOrganization", ctx)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOrganization indicates an expected call of ListOrganization.
func (mr *MockInfluxDBMockRecorder) ListOrganization(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganization", reflect.TypeOf((*MockInfluxDB)(nil).ListOrganization), ctx)
}
// Ping mocks base method.
func (m *MockInfluxDB) Ping(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ping indicates an expected call of Ping.
func (mr *MockInfluxDBMockRecorder) Ping(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockInfluxDB)(nil).Ping), ctx)
}
// Query mocks base method.
func (m *MockInfluxDB) Query(ctx context.Context, org, fluxQuery string) ([]map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, org, fluxQuery)
ret0, _ := ret[0].([]map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockInfluxDBMockRecorder) Query(ctx, org, fluxQuery any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockInfluxDB)(nil).Query), ctx, org, fluxQuery)
}
// WritePoint mocks base method.
func (m *MockInfluxDB) WritePoint(ctx context.Context, org, bucket, measurement string, tags map[string]string, fields map[string]any, timestamp time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WritePoint", ctx, org, bucket, measurement, tags, fields, timestamp)
ret0, _ := ret[0].(error)
return ret0
}
// WritePoint indicates an expected call of WritePoint.
func (mr *MockInfluxDBMockRecorder) WritePoint(ctx, org, bucket, measurement, tags, fields, timestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WritePoint", reflect.TypeOf((*MockInfluxDB)(nil).WritePoint), ctx, org, bucket, measurement, tags, fields, timestamp)
}
// MockInfluxDBProvider is a mock of InfluxDBProvider interface.
type MockInfluxDBProvider struct {
ctrl *gomock.Controller
recorder *MockInfluxDBProviderMockRecorder
isgomock struct{}
}
// MockInfluxDBProviderMockRecorder is the mock recorder for MockInfluxDBProvider.
type MockInfluxDBProviderMockRecorder struct {
mock *MockInfluxDBProvider
}
// NewMockInfluxDBProvider creates a new mock instance.
func NewMockInfluxDBProvider(ctrl *gomock.Controller) *MockInfluxDBProvider {
mock := &MockInfluxDBProvider{ctrl: ctrl}
mock.recorder = &MockInfluxDBProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockInfluxDBProvider) EXPECT() *MockInfluxDBProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockInfluxDBProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockInfluxDBProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockInfluxDBProvider)(nil).Connect))
}
// CreateBucket mocks base method.
func (m *MockInfluxDBProvider) CreateBucket(ctx context.Context, org, bucket string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateBucket", ctx, org, bucket)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateBucket indicates an expected call of CreateBucket.
func (mr *MockInfluxDBProviderMockRecorder) CreateBucket(ctx, org, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockInfluxDBProvider)(nil).CreateBucket), ctx, org, bucket)
}
// CreateOrganization mocks base method.
func (m *MockInfluxDBProvider) CreateOrganization(ctx context.Context, org string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrganization", ctx, org)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrganization indicates an expected call of CreateOrganization.
func (mr *MockInfluxDBProviderMockRecorder) CreateOrganization(ctx, org any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrganization", reflect.TypeOf((*MockInfluxDBProvider)(nil).CreateOrganization), ctx, org)
}
// DeleteBucket mocks base method.
func (m *MockInfluxDBProvider) DeleteBucket(ctx context.Context, bucketID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBucket", ctx, bucketID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBucket indicates an expected call of DeleteBucket.
func (mr *MockInfluxDBProviderMockRecorder) DeleteBucket(ctx, bucketID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockInfluxDBProvider)(nil).DeleteBucket), ctx, bucketID)
}
// DeleteOrganization mocks base method.
func (m *MockInfluxDBProvider) DeleteOrganization(ctx context.Context, orgID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOrganization", ctx, orgID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOrganization indicates an expected call of DeleteOrganization.
func (mr *MockInfluxDBProviderMockRecorder) DeleteOrganization(ctx, orgID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganization", reflect.TypeOf((*MockInfluxDBProvider)(nil).DeleteOrganization), ctx, orgID)
}
// HealthCheck mocks base method.
func (m *MockInfluxDBProvider) HealthCheck(arg0 context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", arg0)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockInfluxDBProviderMockRecorder) HealthCheck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockInfluxDBProvider)(nil).HealthCheck), arg0)
}
// ListBuckets mocks base method.
func (m *MockInfluxDBProvider) ListBuckets(ctx context.Context, org string) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListBuckets", ctx, org)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListBuckets indicates an expected call of ListBuckets.
func (mr *MockInfluxDBProviderMockRecorder) ListBuckets(ctx, org any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockInfluxDBProvider)(nil).ListBuckets), ctx, org)
}
// ListOrganization mocks base method.
func (m *MockInfluxDBProvider) ListOrganization(ctx context.Context) (map[string]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOrganization", ctx)
ret0, _ := ret[0].(map[string]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOrganization indicates an expected call of ListOrganization.
func (mr *MockInfluxDBProviderMockRecorder) ListOrganization(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOrganization", reflect.TypeOf((*MockInfluxDBProvider)(nil).ListOrganization), ctx)
}
// Ping mocks base method.
func (m *MockInfluxDBProvider) Ping(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ping indicates an expected call of Ping.
func (mr *MockInfluxDBProviderMockRecorder) Ping(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockInfluxDBProvider)(nil).Ping), ctx)
}
// Query mocks base method.
func (m *MockInfluxDBProvider) Query(ctx context.Context, org, fluxQuery string) ([]map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, org, fluxQuery)
ret0, _ := ret[0].([]map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockInfluxDBProviderMockRecorder) Query(ctx, org, fluxQuery any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockInfluxDBProvider)(nil).Query), ctx, org, fluxQuery)
}
// UseLogger mocks base method.
func (m *MockInfluxDBProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockInfluxDBProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockInfluxDBProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockInfluxDBProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockInfluxDBProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockInfluxDBProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockInfluxDBProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockInfluxDBProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockInfluxDBProvider)(nil).UseTracer), tracer)
}
// WritePoint mocks base method.
func (m *MockInfluxDBProvider) WritePoint(ctx context.Context, org, bucket, measurement string, tags map[string]string, fields map[string]any, timestamp time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WritePoint", ctx, org, bucket, measurement, tags, fields, timestamp)
ret0, _ := ret[0].(error)
return ret0
}
// WritePoint indicates an expected call of WritePoint.
func (mr *MockInfluxDBProviderMockRecorder) WritePoint(ctx, org, bucket, measurement, tags, fields, timestamp any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WritePoint", reflect.TypeOf((*MockInfluxDBProvider)(nil).WritePoint), ctx, org, bucket, measurement, tags, fields, timestamp)
}
================================================
FILE: pkg/gofr/container/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=container
//
// Package container is a generated GoMock package.
package container
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
logging "gofr.dev/pkg/gofr/logging"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// ChangeLevel mocks base method.
func (m *MockLogger) ChangeLevel(level logging.Level) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "ChangeLevel", level)
}
// ChangeLevel indicates an expected call of ChangeLevel.
func (mr *MockLoggerMockRecorder) ChangeLevel(level any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChangeLevel", reflect.TypeOf((*MockLogger)(nil).ChangeLevel), level)
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Fatal mocks base method.
func (m *MockLogger) Fatal(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Fatal", varargs...)
}
// Fatal indicates an expected call of Fatal.
func (mr *MockLoggerMockRecorder) Fatal(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockLogger)(nil).Fatal), args...)
}
// Fatalf mocks base method.
func (m *MockLogger) Fatalf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Fatalf", varargs...)
}
// Fatalf indicates an expected call of Fatalf.
func (mr *MockLoggerMockRecorder) Fatalf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatalf", reflect.TypeOf((*MockLogger)(nil).Fatalf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// Notice mocks base method.
func (m *MockLogger) Notice(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Notice", varargs...)
}
// Notice indicates an expected call of Notice.
func (mr *MockLoggerMockRecorder) Notice(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notice", reflect.TypeOf((*MockLogger)(nil).Notice), args...)
}
// Noticef mocks base method.
func (m *MockLogger) Noticef(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Noticef", varargs...)
}
// Noticef indicates an expected call of Noticef.
func (mr *MockLoggerMockRecorder) Noticef(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Noticef", reflect.TypeOf((*MockLogger)(nil).Noticef), varargs...)
}
// Warn mocks base method.
func (m *MockLogger) Warn(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warn", varargs...)
}
// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), args...)
}
// Warnf mocks base method.
func (m *MockLogger) Warnf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warnf", varargs...)
}
// Warnf indicates an expected call of Warnf.
func (mr *MockLoggerMockRecorder) Warnf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...)
}
================================================
FILE: pkg/gofr/container/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=container
//
package container
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// DeltaUpDownCounter mocks base method.
func (m *MockMetrics) DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "DeltaUpDownCounter", varargs...)
}
// DeltaUpDownCounter indicates an expected call of DeltaUpDownCounter.
func (mr *MockMetricsMockRecorder) DeltaUpDownCounter(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeltaUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).DeltaUpDownCounter), varargs...)
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
// NewCounter mocks base method.
func (m *MockMetrics) NewCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewCounter", name, desc)
}
// NewCounter indicates an expected call of NewCounter.
func (mr *MockMetricsMockRecorder) NewCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), name, desc)
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, desc)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, desc)
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// NewUpDownCounter mocks base method.
func (m *MockMetrics) NewUpDownCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewUpDownCounter", name, desc)
}
// NewUpDownCounter indicates an expected call of NewUpDownCounter.
func (mr *MockMetricsMockRecorder) NewUpDownCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).NewUpDownCounter), name, desc)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/container/mockcontainer_test.go
================================================
package container
import (
"bytes"
"context"
"fmt"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/sql"
)
func Test_HttpServiceMock(t *testing.T) {
test := struct {
desc string
path string
statusCode int
expectedRes string
}{
desc: "simple service handler",
path: "/fact",
expectedRes: `{"data":{"fact":"Cats have 3 eyelids.","length":20}}` + "\n",
statusCode: 200,
}
httpservices := []string{"cat-facts", "cat-facts1", "cat-facts2"}
_, mock := NewMockContainer(t, WithMockHTTPService(httpservices...))
res := httptest.NewRecorder()
res.Body = bytes.NewBufferString(`{"fact":"Cats have 3 eyelids.","length":20}` + "\n")
res.Code = test.statusCode
result := res.Result()
// Setting mock expectations
mock.HTTPService.EXPECT().Get(t.Context(), "fact", map[string]any{
"max_length": 20,
}).Return(result, nil)
resp, err := mock.HTTPService.Get(t.Context(), "fact", map[string]any{
"max_length": 20,
})
require.NoError(t, err)
assert.Equal(t, resp, result)
err = result.Body.Close()
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
}
// Test_HttpServiceMockWithServiceName verifies that WithMockHTTPService works correctly.
// when service names are provided, and that mocks.HTTPService matches the service in container.
func Test_HttpServiceMockWithServiceName(t *testing.T) {
serviceName := "test-service"
container, mocks := NewMockContainer(t, WithMockHTTPService(serviceName))
// Verify that the service is registered in the container
serviceFromContainer := container.GetHTTPService(serviceName)
require.NotNil(t, serviceFromContainer, "Service should be registered in container")
// Verify the service is in the HTTPServices map
mock, exists := mocks.HTTPServices[serviceName]
require.True(t, exists, "Service should be in HTTPServices map")
assert.Equal(t, mock, serviceFromContainer, "Service from container should match the mock in HTTPServices map")
// Verify backward compatibility: mocks.HTTPService should be the same as the service mock
assert.Equal(t, mocks.HTTPService, serviceFromContainer,
"mocks.HTTPService (backward compatibility) should be the same instance as container.Services[serviceName]")
assert.Equal(t, mocks.HTTPService, mock,
"mocks.HTTPService should be the same as the mock in HTTPServices map")
// Test that we can set expectations on mocks.HTTPService and they work for the service in container
mockResp := httptest.NewRecorder()
mockResp.Body = bytes.NewBufferString(`{"data":"test"}`)
mockResp.Code = 200
result := mockResp.Result()
// Set expectation on mocks.HTTPService (backward compatibility)
mocks.HTTPService.EXPECT().Get(
gomock.Any(), // Use gomock.Any() for context to avoid context mismatch
"test-path",
gomock.Any(), // Use gomock.Any() for queryParams
).Return(result, nil)
// Call the service from container - should match the expectation
resp, err := serviceFromContainer.Get(context.Background(), "test-path", map[string]any{
"key": "value",
})
require.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, 200, resp.StatusCode)
err = result.Body.Close()
require.NoError(t, err)
err = resp.Body.Close()
require.NoError(t, err)
}
// Test_HttpServiceMockMultipleServices verifies that multiple services have separate mock instances.
func Test_HttpServiceMockMultipleServices(t *testing.T) {
serviceNames := []string{"service1", "service2", "service3"}
container, mocks := NewMockContainer(t, WithMockHTTPService(serviceNames...))
// Verify all services are registered and have separate mock instances
for _, name := range serviceNames {
service := container.GetHTTPService(name)
require.NotNil(t, service, "Service %s should be registered", name)
// Verify the service is in the HTTPServices map
mock, exists := mocks.HTTPServices[name]
require.True(t, exists, "Service %s should be in HTTPServices map", name)
assert.Equal(t, mock, service, "Service %s should match the mock in HTTPServices map", name)
// Verify each service has a different mock instance (use pointer comparison)
for _, otherName := range serviceNames {
if name != otherName {
otherMock := mocks.HTTPServices[otherName]
// Use fmt.Sprintf to compare pointers, as assert.NotEqual might do value comparison
mockPtr := fmt.Sprintf("%p", mock)
otherMockPtr := fmt.Sprintf("%p", otherMock)
assert.NotEqual(t, mockPtr, otherMockPtr, "Service %s and %s should have different mock instances (pointers)", name, otherName)
}
}
}
// Test that different services can have different expectations
mockResp1 := httptest.NewRecorder()
mockResp1.Body = bytes.NewBufferString(`{"data":"service1"}`)
mockResp1.Code = 200
result1 := mockResp1.Result()
mockResp2 := httptest.NewRecorder()
mockResp2.Body = bytes.NewBufferString(`{"data":"service2"}`)
mockResp2.Code = 200
result2 := mockResp2.Result()
mockResp3 := httptest.NewRecorder()
mockResp3.Body = bytes.NewBufferString(`{"data":"service3"}`)
mockResp3.Code = 200
result3 := mockResp3.Result()
// Set different expectations for each service
mocks.HTTPServices["service1"].EXPECT().Get(
gomock.Any(),
"/service1/path",
gomock.Any(),
).Return(result1, nil)
mocks.HTTPServices["service2"].EXPECT().Get(
gomock.Any(),
"/service2/path",
gomock.Any(),
).Return(result2, nil)
mocks.HTTPServices["service3"].EXPECT().Get(
gomock.Any(),
"/service3/path",
gomock.Any(),
).Return(result3, nil)
// Call each service with their specific paths
service1 := container.GetHTTPService("service1")
resp1, err1 := service1.Get(context.Background(), "/service1/path", map[string]any{})
require.NoError(t, err1)
assert.NotNil(t, resp1)
assert.Equal(t, 200, resp1.StatusCode)
resp1.Body.Close()
service2 := container.GetHTTPService("service2")
resp2, err2 := service2.Get(context.Background(), "/service2/path", map[string]any{})
require.NoError(t, err2)
assert.NotNil(t, resp2)
assert.Equal(t, 200, resp2.StatusCode)
resp2.Body.Close()
service3 := container.GetHTTPService("service3")
resp3, err3 := service3.Get(context.Background(), "/service3/path", map[string]any{})
require.NoError(t, err3)
assert.NotNil(t, resp3)
assert.Equal(t, 200, resp3.StatusCode)
resp3.Body.Close()
result1.Body.Close()
result2.Body.Close()
result3.Body.Close()
}
func TestExpectSelect_ValidCases(t *testing.T) {
mockContainer, mock := NewMockContainer(t)
t.Run("Test with string slice", func(t *testing.T) {
var passedResultSlice, actualResultSlice []string
expectedIDs := []string{"1", "2"}
mock.SQL.ExpectSelect(t.Context(), &passedResultSlice, "SELECT id FROM users").ReturnsResponse(expectedIDs)
mockContainer.SQL.Select(t.Context(), &actualResultSlice, "SELECT id FROM users")
require.Equal(t, expectedIDs, actualResultSlice)
})
t.Run("Test with string slice with multiple expectations", func(t *testing.T) {
var passedResultSlice, actualResultSlice, actualResultSlice2 []string
expectedIDs := []string{"1", "2"}
expectedIDs2 := []string{"1", "3"}
mock.SQL.ExpectSelect(t.Context(), &passedResultSlice, "SELECT id FROM users").ReturnsResponse(expectedIDs)
mock.SQL.ExpectSelect(t.Context(), &passedResultSlice, "SELECT id FROM users").ReturnsResponse(expectedIDs2)
mockContainer.SQL.Select(t.Context(), &actualResultSlice, "SELECT id FROM users")
mockContainer.SQL.Select(t.Context(), &actualResultSlice2, "SELECT id FROM users")
require.Equal(t, expectedIDs, actualResultSlice)
require.Equal(t, expectedIDs2, actualResultSlice2)
})
t.Run("Test with struct", func(t *testing.T) {
type User struct {
ID int
Name string
}
var passedUser, actualUser User
expectedUser := User{ID: 1, Name: "John"}
mock.SQL.ExpectSelect(t.Context(), &passedUser, "SELECT * FROM users WHERE id = ?", 1).ReturnsResponse(expectedUser)
mockContainer.SQL.Select(t.Context(), &actualUser, "SELECT * FROM users WHERE id = ?", 1)
require.Equal(t, expectedUser, actualUser)
})
t.Run("Test with map", func(t *testing.T) {
var passedSettings, actualSettings map[string]int
expectedSettings := map[string]int{"a": 1, "b": 2}
mock.SQL.ExpectSelect(t.Context(), &passedSettings, "SELECT * FROM settings").ReturnsResponse(expectedSettings)
mockContainer.SQL.Select(t.Context(), &actualSettings, "SELECT * FROM settings")
require.Equal(t, expectedSettings, actualSettings)
})
}
func TestExpectSelect_ErrorCases(t *testing.T) {
mockDB, sqlMock, _ := sql.NewSQLMocks(t)
ctrl := gomock.NewController(t)
expectation := expectedQuery{}
mockLogger := NewMockLogger(ctrl)
sqlMockWrapper := &mockSQL{sqlMock, &expectation}
sqlDB := &sqlMockDB{mockDB, &expectation, mockLogger}
sqlDB.finish(t)
t.Run("NonPointer_Value_In_ExpectSelect", func(t *testing.T) {
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any())
var uninitializedVal, resultVal int
expectedVal := 123
sqlMockWrapper.ExpectSelect(t.Context(), uninitializedVal, "SELECT * FROM test WHERE id=?", 1).ReturnsResponse(expectedVal)
sqlDB.Select(t.Context(), &resultVal, "SELECT * FROM test WHERE id=?", 1)
assert.Zero(t, resultVal)
})
t.Run("PointerValue_In_ReturnsResponse", func(t *testing.T) {
mockLogger.EXPECT().Errorf("received different expectations: %q", gomock.Any())
var uninitializedVal, resultVal int
expectedVal := 123
sqlMockWrapper.ExpectSelect(t.Context(), &uninitializedVal, "SELECT * FROM test WHERE id=?", 1).ReturnsResponse(&expectedVal)
sqlDB.Select(t.Context(), &resultVal, "SELECT * FROM test WHERE id=?", 1)
assert.Zero(t, resultVal)
})
t.Run("Type_Mismatch_Between_Expect_And_Response", func(t *testing.T) {
mockLogger.EXPECT().Errorf("received different expectations: %q", gomock.Any())
var expectedVal, resultVal []string
sqlMockWrapper.ExpectSelect(t.Context(), &expectedVal, "SELECT * FROM test WHERE id=?", 1).ReturnsResponse(123)
sqlDB.Select(t.Context(), &resultVal, "SELECT * FROM test WHERE id=?", 1)
assert.Empty(t, resultVal)
})
t.Run("Select_Called_Without_Expectations", func(t *testing.T) {
mockLogger.EXPECT().Errorf("did not expect any calls for Select with query: %q", gomock.Any())
var val []string
sqlDB.Select(t.Context(), &val, "SELECT * FROM test WHERE id=?", 1)
assert.Empty(t, val)
})
}
func TestMockSQL_Dialect(t *testing.T) {
mockContainer, mock := NewMockContainer(t)
mock.SQL.ExpectDialect().WillReturnString("abcd")
h := mockContainer.SQL.Dialect()
assert.Equal(t, "abcd", h)
}
func TestMockSQL_HealthCheck(t *testing.T) {
mockContainer, mock := NewMockContainer(t)
expectedHealth := &datasource.Health{
Status: "up",
Details: map[string]any{"uptime": 1234567}}
mock.SQL.ExpectHealthCheck().WillReturnHealthCheck(expectedHealth)
resultHealth := mockContainer.SQL.HealthCheck()
assert.Equal(t, expectedHealth, resultHealth)
}
================================================
FILE: pkg/gofr/container/sql_mock.go
================================================
package container
import (
"context"
"database/sql"
"reflect"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/datasource"
gofrSQL "gofr.dev/pkg/gofr/datasource/sql"
"gofr.dev/pkg/gofr/logging"
)
type health datasource.Health
type dialect string
// expectedQuery stores the mock expectations till the method call.
type expectedQuery struct {
queryWithArgs []queryWithArgs
expectedDialect []dialect
expectedHealthCheck []health
}
type queryWithArgs struct {
queryText string
arguments []any
value any
}
// mockSQL wraps go-mock-sql and expectations.
type mockSQL struct {
sqlmock.Sqlmock
*expectedQuery
}
// sqlMockDB wraps the go-mock-sql DB connection and expectations.
type sqlMockDB struct {
*gofrSQL.DB
*expectedQuery
logger logging.Logger
}
func emptyExpectation(m *sqlMockDB) {
if len(m.queryWithArgs) > 0 {
m.queryWithArgs = m.queryWithArgs[1:]
}
}
func (m sqlMockDB) Select(_ context.Context, value any, query string, args ...any) {
if len(m.queryWithArgs) == 0 {
m.logger.Errorf("did not expect any calls for Select with query: %q", query)
return
}
defer emptyExpectation(&m)
expectedText := m.queryWithArgs[0].queryText
expectedArgs := m.queryWithArgs[0].arguments
valueType := reflect.TypeOf(value)
if valueType.Kind() != reflect.Ptr {
m.logger.Errorf("expected a pointer type: %q", value)
return
}
if m.queryWithArgs[0].value == nil {
m.logger.Errorf("received different expectations: %q", query)
return
}
v := reflect.ValueOf(value)
if v.Kind() == reflect.Ptr && !v.IsNil() {
tobechanged := v.Elem()
tobechanged.Set(reflect.ValueOf(m.queryWithArgs[0].value))
}
if expectedText != query {
m.logger.Errorf("expected query: %q, actual query: %q", query, expectedText)
return
}
if len(args) != len(expectedArgs) {
m.logger.Errorf("expected %d args, actual %d", len(expectedArgs), len(args))
return
}
for i := range args {
if args[i] != expectedArgs[i] {
m.logger.Errorf("expected arg %d, actual arg %d", args[i], expectedArgs[i])
return
}
}
}
func (m sqlMockDB) HealthCheck() *datasource.Health {
if len(m.expectedHealthCheck) == 0 {
m.logger.Error("Did not expect any mock calls for HealthCheck")
return nil
}
expectedString := m.expectedHealthCheck[0]
d := datasource.Health(expectedString)
if len(m.expectedHealthCheck) > 0 {
m.expectedHealthCheck = m.expectedHealthCheck[1:]
}
return &d
}
func (m sqlMockDB) Dialect() string {
if len(m.expectedDialect) == 0 {
m.logger.Error("Did not expect any mock calls for Dialect")
return ""
}
expectedString := m.expectedDialect[0]
if len(m.expectedDialect) > 0 {
m.expectedDialect = m.expectedDialect[1:]
}
return string(expectedString)
}
func (m sqlMockDB) finish(t *testing.T) {
t.Helper()
t.Cleanup(func() {
require.Empty(t, m.queryWithArgs, "Expected mock call to Select")
require.Empty(t, m.expectedDialect, "Expected mock call to Dialect")
require.Empty(t, m.expectedHealthCheck, "Expected mock call to HealthCheck")
})
}
// ExpectSelect is not a direct method for mocking the Select method of SQL in go-mock-sql.
// Hence, it expects the user to already provide the populated data interface field,
// which can then be used within the functions implemented by the user.
func (m *mockSQL) ExpectSelect(_ context.Context, value any, query string, args ...any) *queryWithArgs {
qr := queryWithArgs{queryText: query, arguments: args}
fieldType := reflect.TypeOf(value)
if fieldType.Kind() == reflect.Ptr {
qr.value = value
}
m.queryWithArgs = append(m.queryWithArgs, qr)
return &m.queryWithArgs[len(m.queryWithArgs)-1]
}
func (q *queryWithArgs) ReturnsResponse(value any) {
fieldType := reflect.TypeOf(q.value)
if fieldType == nil {
return
}
valueType := reflect.TypeOf(value)
fieldType = fieldType.Elem()
q.value = nil
if fieldType == valueType {
q.value = value
}
}
func (m *mockSQL) ExpectHealthCheck() *health {
hc := health{}
m.expectedHealthCheck = append(m.expectedHealthCheck, hc)
return &m.expectedHealthCheck[len(m.expectedHealthCheck)-1]
}
func (d *health) WillReturnHealthCheck(dh *datasource.Health) {
*d = health(*dh)
}
func (m *mockSQL) ExpectDialect() *dialect {
d := dialect("")
m.expectedDialect = append(m.expectedDialect, d)
return &m.expectedDialect[len(m.expectedDialect)-1]
}
func (*mockSQL) NewResult(lastInsertID, rowsAffected int64) sql.Result {
return sqlmock.NewResult(lastInsertID, rowsAffected)
}
func (d *dialect) WillReturnString(s string) {
*d = dialect(s)
}
================================================
FILE: pkg/gofr/context.go
================================================
package gofr
import (
"context"
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/websocket"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/cmd/terminal"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/http/middleware"
"gofr.dev/pkg/gofr/logging"
)
type Context struct {
context.Context
// Request needs to be public because handlers need to access request details. Else, we need to provide all
// functionalities of the Request as a method on context. This is not needed because Request here is an interface
// So, internals are not exposed anyway.
Request
// Same logic as above.
*container.Container
// responder is private as Handlers do not need to worry about how to respond. But it is still an abstraction over
// normal response writer as we want to keep the context independent of http. Will help us in writing CMD application
// or gRPC servers etc using the same handler signature.
responder Responder
// Terminal needs to be public as CMD applications need to access various terminal user interface(TUI) features.
Out terminal.Output
logging.ContextLogger
}
type AuthInfo interface {
GetClaims() jwt.MapClaims
GetUsername() string
GetAPIKey() string
}
/*
Trace returns an open telemetry span. We have to always close the span after corresponding work is done. Usages:
span := c.Trace("Some Work")
// Do some work here.
defer span.End()
If an entire function has to traced as span, we can use a simpler format:
defer c.Trace("ExampleHandler").End()
We can write this at the start of function and because of how defer works, trace will start at that line
but End will be called after function ends.
Developer Note: If you chain methods in a defer statement, everything except the last function will be evaluated at call time.
*/
func (c *Context) Trace(name string) trace.Span {
tr := otel.GetTracerProvider().Tracer("gofr-context")
ctx, span := tr.Start(c.Context, name)
// TODO: If we don't close the span using `defer` and run the http-server example by hitting `/trace` endpoint, we are
// getting incomplete redis spans when viewing the trace using correlationID. If we remove assigning the ctx to GoFr
// context then spans are coming correct but then parent-child span relationship is being hindered.
c.Context = ctx
return span
}
func (c *Context) Bind(i any) error {
return c.Request.Bind(i)
}
// WriteMessageToSocket writes a message to the WebSocket connection associated with the context.
// The data parameter can be of type string, []byte, or any struct that can be marshaled to JSON.
// It retrieves the WebSocket connection from the context and sends the message as a TextMessage.
func (c *Context) WriteMessageToSocket(data any) error {
// Retrieve connection from context based on connectionID
conn := c.Container.GetConnectionFromContext(c.Context)
message, err := serializeMessage(data)
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, message)
}
// WriteMessageToService writes a message to the WebSocket connection associated with the given service name.
// The data parameter can be of type string, []byte, or any struct that can be marshaled to JSON.
func (c *Context) WriteMessageToService(serviceName string, data any) error {
// Retrieve connection using serviceName
conn := c.Container.GetWSConnectionByServiceName(serviceName)
if conn == nil {
return fmt.Errorf("%w: %s", ErrConnectionNotFound, serviceName)
}
message, err := serializeMessage(data)
if err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, message)
}
type authInfo struct {
claims jwt.MapClaims
username string
apiKey string
}
// GetAuthInfo is a method on context, to access different methods to retrieve authentication info.
//
// GetAuthInfo().GetClaims() : retrieves the jwt claims.
// GetAuthInfo().GetUsername() : retrieves the username while basic authentication.
// GetAuthInfo().GetAPIKey() : retrieves the APIKey being used for authentication.
func (c *Context) GetAuthInfo() AuthInfo {
claims, _ := c.Request.Context().Value(middleware.JWTClaim).(jwt.MapClaims)
APIKey, _ := c.Request.Context().Value(middleware.APIKey).(string)
username, _ := c.Request.Context().Value(middleware.Username).(string)
return &authInfo{
claims: claims,
username: username,
apiKey: APIKey,
}
}
// GetClaims returns a response of jwt.MapClaims type when OAuth is enabled.
// It returns nil if called, when OAuth is not enabled.
func (a *authInfo) GetClaims() jwt.MapClaims {
return a.claims
}
// GetUsername returns the username when basic auth is enabled.
// It returns an empty string if called, when basic auth is not enabled.
func (a *authInfo) GetUsername() string {
return a.username
}
// GetAPIKey returns the APIKey when APIKey auth is enabled.
// It returns an empty string if called, when APIKey auth is not enabled.
func (a *authInfo) GetAPIKey() string {
return a.apiKey
}
// func (c *Context) reset(w Responder, r Request) {
// c.Request = r
// c.responder = w
// c.Context = nil
// // c.Logger = nil // For now, all loggers are same. So, no need to set nil.
// }
func newContext(w Responder, r Request, c *container.Container) *Context {
return &Context{
Context: r.Context(),
Request: r,
responder: w,
Container: c,
ContextLogger: *logging.NewContextLogger(r.Context(), c.Logger),
}
}
func newCMDContext(w Responder, r Request, c *container.Container, out terminal.Output) *Context {
return &Context{
Context: r.Context(),
responder: w,
Request: r,
Container: c,
Out: out,
ContextLogger: *logging.NewContextLogger(r.Context(), c.Logger),
}
}
func (c *Context) GetCorrelationID() string {
return trace.SpanFromContext(c).SpanContext().TraceID().String()
}
================================================
FILE: pkg/gofr/context_test.go
================================================
package gofr
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/http/middleware"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
"gofr.dev/pkg/gofr/version"
)
func Test_newContextSuccess(t *testing.T) {
httpRequest, err := http.NewRequestWithContext(t.Context(),
http.MethodPost, "/test", bytes.NewBufferString(`{"key":"value"}`))
httpRequest.Header.Set("Content-Type", "application/json")
if err != nil {
t.Fatalf("unable to create request with context %v", err)
}
req := gofrHTTP.NewRequest(httpRequest)
ctx := newContext(nil, req, container.NewContainer(config.NewEnvFile("",
logging.NewMockLogger(logging.DEBUG))))
body := map[string]string{}
err = ctx.Bind(&body)
assert.Equal(t, map[string]string{"key": "value"}, body, "TEST Failed \n unable to read body")
require.NoError(t, err, "TEST Failed \n unable to read body")
}
func TestContext_AddTrace(t *testing.T) {
tp := trace.NewTracerProvider()
otel.SetTracerProvider(tp)
tr := otel.GetTracerProvider().Tracer("gofr-" + version.Framework)
// Creating a dummy request with trace
req := httptest.NewRequest(http.MethodGet, "/dummy", http.NoBody)
originalCtx, span := tr.Start(req.Context(), "start")
traceID := span.SpanContext().TraceID().String()
spanID := span.SpanContext().SpanID().String()
// Creating a new context from original context and adding trace
ctx := Context{
Context: originalCtx,
}
newSpan := ctx.Trace("Some Work")
defer newSpan.End()
newtraceID := newSpan.SpanContext().TraceID().String()
newSpanID := newSpan.SpanContext().SpanID().String()
// both traceIDs must be same as context is same
assert.Equal(t, traceID, newtraceID)
// spanIDs must not be same
assert.NotEqual(t, spanID, newSpanID)
}
func TestContext_WriteMessageToSocket(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
// Use NewServerConfigs to get free ports for both HTTP and metrics
configs := testutil.NewServerConfigs(t)
app := New()
messageChan := make(chan string, 1)
handlerDone := make(chan struct{})
var handlerOnce sync.Once
app.WebSocket("/ws", func(ctx *Context) (any, error) {
defer handlerOnce.Do(func() { close(handlerDone) })
return handleWebSocketMessage(ctx, messageChan)
})
// Start server in goroutine
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
app.Run()
}()
// Give server time to start
time.Sleep(30 * time.Millisecond)
wsURL := fmt.Sprintf("ws://localhost:%d/ws", configs.HTTPPort)
// Test the WebSocket connection
// Note: We don't wait for server to stop as it's designed to run until signal.
// The test completes after testing the WebSocket functionality.
testWebSocketConnection(t, wsURL, messageChan, handlerDone)
}
// handleWebSocketMessage handles the WebSocket message sending logic.
func handleWebSocketMessage(ctx *Context, messageChan chan string) (any, error) {
err := ctx.WriteMessageToSocket("Hello! GoFr")
if err != nil {
// Signal error instead of calling t.Errorf in goroutine
select {
case messageChan <- "ERROR":
default:
}
return nil, err
}
// Signal that message was sent
select {
case messageChan <- "Hello! GoFr":
default:
}
return "Hello! GoFr", nil
}
// testWebSocketConnection tests the WebSocket connection and message reading.
func testWebSocketConnection(t *testing.T, wsURL string, messageChan chan string, handlerDone chan struct{}) {
t.Helper()
// Create WebSocket client with timeout
dialer := &websocket.Dialer{
HandshakeTimeout: 10 * time.Second,
}
ws, resp, err := dialer.Dial(wsURL, nil)
require.NoError(t, err, "WebSocket handshake failed")
defer func() {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
if ws != nil {
ws.Close()
}
}()
// Set read deadline and read message
_ = ws.SetReadDeadline(time.Now().Add(10 * time.Second))
_, message, err := ws.ReadMessage()
require.NoError(t, err, "Failed to read WebSocket message")
assert.Equal(t, "Hello! GoFr", string(message))
// Wait for handler completion
select {
case msg := <-messageChan:
if msg == "ERROR" {
t.Error("WriteMessageToSocket failed in handler")
} else {
assert.Equal(t, "Hello! GoFr", msg)
}
case <-time.After(5 * time.Second):
t.Fatal("Test timed out waiting for handler completion")
}
// Wait for handler to complete before closing connection
select {
case <-handlerDone:
// Handler completed successfully
case <-time.After(2 * time.Second):
t.Error("Handler did not complete within timeout")
}
// Close the websocket connection to trigger cleanup
ws.Close()
// Wait a bit for cleanup to complete
time.Sleep(10 * time.Millisecond)
}
func TestContext_WriteMessageToService(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
// Use NewServerConfigs to get free ports for both HTTP and metrics
configs := testutil.NewServerConfigs(t)
app := New()
// Create a simple echo server for testing
app.WebSocket("/ws", func(ctx *Context) (any, error) {
// This is a simple echo server that reads a message and echoes it back
var message string
// Read the incoming message using ctx.Bind
err := ctx.Bind(&message)
if err != nil {
return nil, err
}
// Echo the message back
return message, nil
})
// Start server in goroutine
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
app.Run()
}()
// Give server time to start
time.Sleep(10 * time.Millisecond)
wsURL := fmt.Sprintf("ws://localhost:%d/ws", configs.HTTPPort)
// Establish a WebSocket connection to the echo server
ws, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
require.NoError(t, err, "Dial should not return an error")
defer func() {
ws.Close()
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}()
// WebSocket service communication should be handled through the Context
// Send a message to the echo server and read the response
err = ws.WriteMessage(websocket.TextMessage, []byte("Hello, WebSocket!"))
require.NoError(t, err, "WriteMessage should not return an error")
// Read the response
_ = ws.SetReadDeadline(time.Now().Add(5 * time.Second))
_, message, err := ws.ReadMessage()
require.NoError(t, err, "ReadMessage should not return an error")
assert.Equal(t, "Hello, WebSocket!", string(message))
// Close the websocket connection to trigger cleanup
ws.Close()
// Wait a bit for cleanup to complete
time.Sleep(10 * time.Millisecond)
}
func TestGetAuthInfo_BasicAuth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
ctx := context.WithValue(req.Context(), middleware.Username, "validUser")
*req = *req.Clone(ctx)
mockContainer, _ := container.NewMockContainer(t)
gofrRq := gofrHTTP.NewRequest(req)
c := &Context{
Context: ctx,
Request: gofrRq,
Container: mockContainer,
}
res := c.GetAuthInfo().GetUsername()
assert.Equal(t, "validUser", res)
}
func TestGetAuthInfo_ApiKey(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
ctx := context.WithValue(req.Context(), middleware.APIKey, "9221e451-451f-4cd6-a23d-2b2d3adea9cf")
*req = *req.Clone(ctx)
gofrRq := gofrHTTP.NewRequest(req)
mockContainer, _ := container.NewMockContainer(t)
c := &Context{
Context: ctx,
Request: gofrRq,
Container: mockContainer,
}
res := c.GetAuthInfo().GetAPIKey()
assert.Equal(t, "9221e451-451f-4cd6-a23d-2b2d3adea9cf", res)
}
func TestGetAuthInfo_JWTClaims(t *testing.T) {
claims := jwt.MapClaims{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
}
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
ctx := context.WithValue(req.Context(), middleware.JWTClaim, claims)
*req = *req.Clone(ctx)
gofrRq := gofrHTTP.NewRequest(req)
mockContainer, _ := container.NewMockContainer(t)
c := &Context{
Context: ctx,
Request: gofrRq,
Container: mockContainer,
}
res := c.GetAuthInfo().GetClaims()
assert.Equal(t, claims, res)
}
func TestContext_GetCorrelationID(t *testing.T) {
// Setup OpenTelemetry tracer
exporter := tracetest.NewInMemoryExporter()
tp := trace.NewTracerProvider(trace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
tracer := tp.Tracer("test")
t.Run("with span", func(t *testing.T) {
ctx, span := tracer.Start(t.Context(), "test-span")
defer span.End()
gofCtx := &Context{Context: ctx}
correlationID := gofCtx.GetCorrelationID()
assert.Len(t, correlationID, 32, "Expected correlation ID length 32, got %d", len(correlationID))
assert.NotEqual(t, "00000000000000000000000000000000", correlationID, "Expected non-empty correlation ID")
})
t.Run("without span", func(t *testing.T) {
gofCtx := &Context{Context: t.Context()}
correlationID := gofCtx.GetCorrelationID()
expected := "00000000000000000000000000000000"
assert.Equal(t, expected, correlationID, "Expected empty TraceID when no span present")
})
}
================================================
FILE: pkg/gofr/cron.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"sync"
"time"
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/version"
)
const (
seconds = 59
minutes = 59
hrs = 23
days = 31
months = 12
dayOfWeek = 6
scheduleParts = 5
schedulePartsWithSecond = 6
)
type CronFunc func(ctx *Context)
// Crontab maintains the job scheduling and runs the jobs at their scheduled time by
// going through them at each tick using a ticker.
type Crontab struct {
// contains unexported fields
ticker *time.Ticker
jobs []*job
container *container.Container
mu sync.RWMutex
}
type job struct {
sec map[int]struct{}
min map[int]struct{}
hour map[int]struct{}
day map[int]struct{}
month map[int]struct{}
dayOfWeek map[int]struct{}
name string
fn CronFunc
}
type tick struct {
sec int
min int
hour int
day int
month int
dayOfWeek int
}
// NewCron initializes and returns new cron tab.
func NewCron(cntnr *container.Container) *Crontab {
c := &Crontab{
ticker: time.NewTicker(time.Second),
container: cntnr,
jobs: make([]*job, 0),
}
c.registerMetrics()
go func() {
for t := range c.ticker.C {
c.runScheduled(t)
}
}()
return c
}
func (c *Crontab) runScheduled(t time.Time) {
c.mu.Lock()
n := len(c.jobs)
jb := make([]*job, n)
copy(jb, c.jobs)
c.mu.Unlock()
for _, j := range jb {
if j.tick(getTick(t)) {
go j.run(c.container)
}
}
}
func (j *job) run(cntnr *container.Container) {
ctx, span := otel.GetTracerProvider().Tracer("gofr-"+version.Framework).
Start(context.Background(), j.name)
defer span.End()
c := newContext(nil, &noopRequest{}, cntnr)
c.Context = ctx
c.Infof("Starting cron job: %s", j.name)
start := time.Now()
defer func() {
duration := time.Since(start).Seconds()
if m := cntnr.Metrics(); m != nil {
m.RecordHistogram(ctx, "app_cron_job_duration", float64(duration), "job", j.name)
if r := recover(); r != nil {
c.Errorf("Panic in cron job %s: %v", j.name, r)
m.IncrementCounter(ctx, "app_cron_job_failures", "job", j.name)
} else {
m.IncrementCounter(ctx, "app_cron_job_success", "job", j.name)
}
} else if r := recover(); r != nil {
c.Errorf("Panic in cron job %s: %v", j.name, r)
}
c.Infof("Finished cron job: %s in %s", j.name, duration)
}()
if m := cntnr.Metrics(); m != nil {
m.IncrementCounter(ctx, "app_cron_job_total", "job", j.name)
}
j.fn(c)
}
func (c *Crontab) registerMetrics() {
m := c.container.Metrics()
if m == nil {
return
}
cronJobHistogramBuckets := []float64{.05, .1, .5, 1, 5, 10, 30, 60, 120, 300, 600, 1800, 3600}
m.NewHistogram(
"app_cron_job_duration",
"Duration of cron job execution in seconds",
cronJobHistogramBuckets...,
)
m.NewCounter("app_cron_job_total", "Total number of cron job executions")
m.NewCounter("app_cron_job_success", "Number of successful cron job executions")
m.NewCounter("app_cron_job_failures", "Number of failed cron job executions")
}
// AddJob to cron tab, returns error if the cron syntax can't be parsed or is out of bounds.
func (c *Crontab) AddJob(schedule, jobName string, fn CronFunc) error {
j, err := parseSchedule(schedule)
if err != nil {
return err
}
j.name = jobName
j.fn = fn
c.mu.Lock()
c.jobs = append(c.jobs, j)
c.mu.Unlock()
return nil
}
var errBadScheduleFormat = errors.New("schedule string must have five components like * * * * *")
// errOutOfRange denotes the errors that occur when a range in schedule is out of scope for the particular time unit.
type errOutOfRange struct {
rangeVal any
input string
min, max int
}
func (e errOutOfRange) Error() string {
return fmt.Sprintf("out of range for %s in %s. %s must be in "+
"range %d-%d", e.rangeVal, e.input, e.rangeVal, e.min, e.max)
}
type errParsing struct {
invalidPart string
base string
}
func (e errParsing) Error() string {
if e.base != "" {
return fmt.Sprintf("unable to parse %s part in %s", e.invalidPart, e.base)
}
return fmt.Sprintf("unable to parse %s", e.invalidPart)
}
// noopRequest is a non-operating implementation of Request interface
// this is required to prevent panics while executing cron jobs.
type noopRequest struct {
}
func (noopRequest) Context() context.Context {
return context.Background()
}
func (noopRequest) Param(string) string {
return ""
}
func (noopRequest) PathParam(string) string {
return ""
}
func (noopRequest) HostName() string {
return "gofr"
}
func (noopRequest) Bind(any) error {
return nil
}
func (noopRequest) Params(string) []string {
return nil
}
================================================
FILE: pkg/gofr/cron_scheduler.go
================================================
package gofr
import (
"regexp"
"strconv"
"strings"
"time"
)
// this will compile the regex once instead of compiling it each time when it is being called.
var (
matchSpaces = regexp.MustCompile(`\s+`)
matchN = regexp.MustCompile(`(.*)/(\d+)`)
matchRange = regexp.MustCompile(`^(\d+)-(\d+)$`)
)
// parseSchedule parses schedule string and create job struct with filled times to launch,
// or error if syntax is wrong.
func parseSchedule(s string) (*job, error) {
var err error
j := &job{}
s = matchSpaces.ReplaceAllLiteralString(s, " ")
s = strings.Trim(s, " ")
parts := strings.Split(s, " ")
var partsItr int
switch len(parts) {
case schedulePartsWithSecond:
j.sec, err = parsePart(parts[partsItr], 0, seconds)
if err != nil {
return nil, err
}
partsItr++
case scheduleParts:
partsItr = 0
default:
return nil, errBadScheduleFormat
}
j.min, err = parsePart(parts[partsItr], 0, minutes)
if err != nil {
return nil, err
}
j.hour, err = parsePart(parts[partsItr+1], 0, hrs)
if err != nil {
return nil, err
}
j.day, err = parsePart(parts[partsItr+2], 1, days)
if err != nil {
return nil, err
}
j.month, err = parsePart(parts[partsItr+3], 1, months)
if err != nil {
return nil, err
}
j.dayOfWeek, err = parsePart(parts[partsItr+4], 0, dayOfWeek)
if err != nil {
return nil, err
}
// day/dayOfWeek combination
mergeDays(j)
return j, nil
}
func mergeDays(j *job) {
switch {
case len(j.day) < 31 && len(j.dayOfWeek) == 7: // day set, but not dayOfWeek, clear dayOfWeek
j.dayOfWeek = make(map[int]struct{})
case len(j.dayOfWeek) < 7 && len(j.day) == 31: // dayOfWeek set, but not day, clear day
j.day = make(map[int]struct{})
}
}
// parsePart parse individual schedule part from schedule string.
func parsePart(s string, minValue, maxValue int) (map[int]struct{}, error) {
// wildcard pattern
if s == "*" {
return getDefaultJobField(minValue, maxValue, 1), nil
}
// */2 1-59/5 pattern
if matches := matchN.FindStringSubmatch(s); matches != nil {
return parseSteps(s, matches[1], matches[2], minValue, maxValue)
}
// 1,2,4 or 1,2,10-15,20,30-45 pattern
return parseRange(s, minValue, maxValue)
}
func parseSteps(s, match1, match2 string, minValue, maxValue int) (map[int]struct{}, error) {
localMin := minValue
localMax := maxValue
if match1 != "" && match1 != "*" {
rng := matchRange.FindStringSubmatch(match1)
if rng == nil {
return nil, errParsing{match1, s}
}
localMin, _ = strconv.Atoi(rng[1])
localMax, _ = strconv.Atoi(rng[2])
if localMin < minValue || localMax > maxValue {
return nil, errOutOfRange{rng[1], s, minValue, maxValue}
}
}
n, _ := strconv.Atoi(match2)
return getDefaultJobField(localMin, localMax, n), nil
}
func parseRange(s string, minValue, maxValue int) (map[int]struct{}, error) {
r := make(map[int]struct{})
parts := strings.Split(s, ",")
for _, part := range parts {
if err := parseSingleOrRange(part, minValue, maxValue, r); err != nil {
return nil, err
}
}
if len(r) == 0 {
return nil, errParsing{invalidPart: s}
}
return r, nil
}
func parseSingleOrRange(part string, minValue, maxValue int, r map[int]struct{}) error {
if rng := matchRange.FindStringSubmatch(part); rng != nil {
localMin, _ := strconv.Atoi(rng[1])
localMax, _ := strconv.Atoi(rng[2])
if localMin < minValue || localMax > maxValue {
return errOutOfRange{part, part, minValue, maxValue}
}
for i := localMin; i <= localMax; i++ {
r[i] = struct{}{}
}
} else {
i, err := strconv.Atoi(part)
if err != nil {
return errParsing{part, part}
}
if i < minValue || i > maxValue {
return errOutOfRange{part, part, minValue, maxValue}
}
r[i] = struct{}{}
}
return nil
}
func getDefaultJobField(minValue, maxValue, incr int) map[int]struct{} {
r := make(map[int]struct{})
for i := minValue; i <= maxValue; i += incr {
r[i] = struct{}{}
}
return r
}
func getTick(t time.Time) *tick {
return &tick{
sec: t.Second(),
min: t.Minute(),
hour: t.Hour(),
day: t.Day(),
month: int(t.Month()),
dayOfWeek: int(t.Weekday()),
}
}
func (j *job) tick(t *tick) bool {
if _, ok := j.min[t.min]; !ok {
return false
}
if _, ok := j.hour[t.hour]; !ok {
return false
}
// cumulative day and dayOfWeek, as it should be
_, day := j.day[t.day]
_, dayOfWeek := j.dayOfWeek[t.dayOfWeek]
if !day && !dayOfWeek {
return false
}
if _, ok := j.month[t.month]; !ok {
return false
}
if j.sec != nil {
if _, ok := j.sec[t.sec]; !ok {
return false
}
} else {
if t.sec != 0 {
return false
}
}
return true
}
================================================
FILE: pkg/gofr/cron_test.go
================================================
package gofr
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestCron_parseSchedule_Success(t *testing.T) {
testCases := []struct {
desc string
schedule string
expJob *job
}{
{
desc: "success case: all wildcard",
schedule: "* * * * * *",
expJob: &job{
sec: getDefaultJobField(0, 59, 1),
min: getDefaultJobField(0, 59, 1),
hour: getDefaultJobField(0, 23, 1),
day: getDefaultJobField(1, 31, 1),
month: getDefaultJobField(1, 12, 1),
dayOfWeek: getDefaultJobField(0, 6, 1),
},
},
{
schedule: "*/3 * * * *",
expJob: &job{
min: getDefaultJobField(0, 59, 3),
hour: getDefaultJobField(0, 23, 1),
day: getDefaultJobField(1, 31, 1),
month: getDefaultJobField(1, 12, 1),
dayOfWeek: getDefaultJobField(0, 6, 1),
},
},
{
schedule: "1,5,10 * * * *",
expJob: &job{
min: map[int]struct{}{1: {}, 5: {}, 10: {}},
hour: getDefaultJobField(0, 23, 1),
day: getDefaultJobField(1, 31, 1),
month: getDefaultJobField(1, 12, 1),
dayOfWeek: getDefaultJobField(0, 6, 1),
},
},
{
schedule: "*/20 3-5 * * *",
expJob: &job{
min: getDefaultJobField(0, 59, 20),
hour: getDefaultJobField(3, 5, 1),
day: getDefaultJobField(1, 31, 1),
month: getDefaultJobField(1, 12, 1),
dayOfWeek: getDefaultJobField(0, 6, 1),
},
},
{
schedule: "*/20 3-5/2 * * *",
expJob: &job{
min: getDefaultJobField(0, 59, 20),
hour: getDefaultJobField(3, 5, 2),
day: getDefaultJobField(1, 31, 1),
month: getDefaultJobField(1, 12, 1),
dayOfWeek: getDefaultJobField(0, 6, 1),
},
},
{
schedule: "*/20 3-5/2 22 * *",
expJob: &job{
min: getDefaultJobField(0, 59, 20),
hour: getDefaultJobField(3, 5, 2),
day: map[int]struct{}{22: {}},
month: getDefaultJobField(1, 12, 1),
dayOfWeek: map[int]struct{}{},
},
},
{
schedule: "*/20 3-5/2 22 */5 *",
expJob: &job{
min: getDefaultJobField(0, 59, 20),
hour: getDefaultJobField(3, 5, 2),
day: map[int]struct{}{22: {}},
month: getDefaultJobField(1, 12, 5),
dayOfWeek: map[int]struct{}{},
},
},
{
schedule: "*/20 3-5/2 * */5 4",
expJob: &job{
min: getDefaultJobField(0, 59, 20),
hour: getDefaultJobField(3, 5, 2),
day: map[int]struct{}{},
month: getDefaultJobField(1, 12, 5),
dayOfWeek: map[int]struct{}{4: {}},
},
},
}
for _, tc := range testCases {
j, err := parseSchedule(tc.schedule)
require.NoError(t, err)
assert.Equal(t, *tc.expJob, *j)
}
}
func TestCron_parseSchedule_Error(t *testing.T) {
testCases := []struct {
desc string
schedules []string
expErrString string
}{
{
desc: "incorrect number of schedule parts: less",
schedules: []string{"* * * * ", "* * * * * * *"},
expErrString: "schedule string must have five components like * * * * *",
},
{
desc: "incorrect range",
schedules: []string{
"1-100 * * * * *",
"1-200 * * * *",
"* 0-30 * * *",
"* * 0-10 * *",
"* * 1-33 * *",
"* * * 0-22 *",
"* * * * 0-7",
"* * 1-40/2 * *",
"60 * * * *",
},
expErrString: "out of range",
},
{
desc: "unparsable schedule parts",
schedules: []string{
"* * ab/2 * *",
"* 1,2/10 * * *",
"* * 1,2,3,1-15/10 * *",
"a b c d e",
},
expErrString: "unable to parse",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
for _, s := range tc.schedules {
j, err := parseSchedule(s)
assert.Nil(t, j)
require.ErrorContains(t, err, tc.expErrString)
}
})
}
}
func TestCron_getDefaultJobField(t *testing.T) {
testCases := []struct {
min int
max int
incr int
expOutCount int
}{
{1, 10, 1, 10},
{1, 10, 2, 5},
}
for _, tc := range testCases {
out := getDefaultJobField(tc.min, tc.max, tc.incr)
assert.Len(t, out, tc.expOutCount)
}
}
func TestCron_getTick(t *testing.T) {
expTick := &tick{10, 20, 13, 10, 5, 5}
tM := time.Date(2024, 5, 10, 13, 20, 10, 1, time.Local)
tck := getTick(tM)
assert.Equal(t, expTick, tck)
}
func TestCronTab_AddJob(t *testing.T) {
fn := func(*Context) {}
testCases := []struct {
schedule string
expErr error
}{
{
schedule: "* * * * *",
},
{
schedule: "* * * *",
expErr: errBadScheduleFormat,
},
}
// We need a mock container because NewCron now registers metrics
mockContainer, mocks := container.NewMockContainer(t)
// Expect metrics registration
mocks.Metrics.EXPECT().NewHistogram("app_cron_job_duration", gomock.Any(), gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_total", gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_success", gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_failures", gomock.Any()).AnyTimes()
c := NewCron(mockContainer)
for _, tc := range testCases {
err := c.AddJob(tc.schedule, "test-job", fn)
assert.Equal(t, tc.expErr, err)
}
}
func TestCronTab_runScheduled(t *testing.T) {
j := &job{
sec: map[int]struct{}{1: {}},
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
month: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{1: {}},
name: "test-job",
fn: func(*Context) { fmt.Println("hello from cron") },
}
// can make container nil as we are not testing the internal working of
// dependency function as it is user defined
// can make container nil as we are not testing the internal working of
// dependency function as it is user defined
mockContainer, mocks := container.NewMockContainer(t)
// Expect metrics registration
mocks.Metrics.EXPECT().NewHistogram("app_cron_job_duration", gomock.Any(), gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_total", gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_success", gomock.Any()).AnyTimes()
mocks.Metrics.EXPECT().NewCounter("app_cron_job_failures", gomock.Any()).AnyTimes()
// Expect metrics recording during run
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "app_cron_job_total", "job", "test-job").Times(1)
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "app_cron_job_success", "job", "test-job").Times(1)
mocks.Metrics.EXPECT().RecordHistogram(gomock.Any(), "app_cron_job_duration", gomock.Any(), "job", "test-job").Times(1)
c := NewCron(mockContainer)
// Populate the job array for cron table
c.jobs = []*job{j}
out := testutil.StdoutOutputForFunc(func() {
c.runScheduled(time.Date(2024, 1, 1, 1, 1, 1, 1, time.Local))
// block the main go routine to let the cron run
time.Sleep(100 * time.Millisecond)
})
assert.Contains(t, out, "hello from cron")
}
func TestJob_tick(t *testing.T) {
tck := &tick{1, 1, 1, 1, 1, 1}
testCases := []struct {
desc string
job *job
exp bool
}{
{
desc: "min not matching",
job: &job{
sec: map[int]struct{}{1: {}},
min: map[int]struct{}{2: {}},
},
},
{
desc: "hour not matching",
job: &job{
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{2: {}},
},
},
{
desc: "day not matching",
job: &job{
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{2: {}},
},
},
{
desc: "month not matching",
job: &job{
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{1: {}},
month: map[int]struct{}{2: {}},
},
},
{
desc: "weekday not matching",
job: &job{
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{2: {}},
},
},
{
desc: "sec not matching",
job: &job{
sec: map[int]struct{}{2: {}},
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
month: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{1: {}},
},
},
{
desc: "job scheduled on the tick",
job: &job{
sec: map[int]struct{}{1: {}},
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
month: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{1: {}},
},
exp: true,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
out := tc.job.tick(tck)
assert.Equal(t, tc.exp, out)
})
}
}
func Test_noopRequest(t *testing.T) {
noop := noopRequest{}
assert.NotNil(t, noop.Context())
assert.Empty(t, noop.Param(""))
assert.Empty(t, noop.PathParam(""))
assert.Equal(t, "gofr", noop.HostName())
require.NoError(t, noop.Bind(nil))
assert.Nil(t, noop.Params("test"))
}
func TestCron_parseRange(t *testing.T) {
tests := []struct {
name string
input string
expected map[int]struct{}
min int
max int
hasError bool
}{
{
name: "Valid Range",
input: "1-5",
expected: map[int]struct{}{
1: {}, 2: {}, 3: {}, 4: {}, 5: {},
},
min: 1, max: 10,
hasError: false,
},
{
name: "Out of Range",
input: "1-12",
expected: nil,
min: 1, max: 10,
hasError: true,
},
{
name: "Invalid Input",
input: "a-b",
expected: nil,
min: 1, max: 10,
hasError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := parseRange(test.input, test.min, test.max)
if test.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Len(t, output, len(test.expected))
assert.Equal(t, test.expected, output)
})
}
}
func TestCron_parseRange_BoundaryValues(t *testing.T) {
tests := []struct {
name string
input string
expected map[int]struct{}
min int
max int
hasError bool
}{
{
name: "Lower Boundary",
input: "1-1",
expected: map[int]struct{}{
1: {},
},
min: 1,
max: 10,
hasError: false,
},
{
name: "Upper Boundary",
input: "10-10",
expected: map[int]struct{}{
10: {},
},
min: 1,
max: 10,
hasError: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := parseRange(test.input, test.min, test.max)
if test.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, test.expected, output, "Expected: %v, got: %v", test.expected, output)
})
}
}
func TestCron_parsePart_InputFormats(t *testing.T) {
tests := []struct {
name string
input string
expected map[int]struct{}
min int
max int
hasError bool
}{
{
name: "Valid Input with Multiple Values",
input: "1,5,7",
expected: map[int]struct{}{
1: {}, 5: {}, 7: {},
},
min: 1,
max: 10,
hasError: false,
},
{
name: "Invalid Input Format",
input: "1,a,3",
expected: nil,
min: 1,
max: 10,
hasError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := parsePart(test.input, test.min, test.max)
if test.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
assert.Equal(t, test.expected, output)
})
}
}
func TestCron_parseRange_ErrorHandling(t *testing.T) {
tests := []struct {
name string
input string
min int
max int
hasError bool
}{
{
name: "Empty String Input",
input: "",
min: 1,
max: 10,
hasError: true,
},
{
name: "Out of Range Input",
input: "15-20",
min: 1,
max: 10,
hasError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := parseRange(test.input, test.min, test.max)
if test.hasError {
require.Error(t, err, "Expected an error for input: %s", test.input)
} else {
require.NoError(t, err)
}
})
}
}
func TestCron_parseRange_SuccessCases(t *testing.T) {
tests := []struct {
name string
input string
expected map[int]struct{}
min int
max int
}{
{
name: "Full Range",
input: "1-10",
expected: map[int]struct{}{
1: {}, 2: {}, 3: {}, 4: {}, 5: {}, 6: {}, 7: {}, 8: {}, 9: {}, 10: {},
},
min: 1, max: 10,
},
{
name: "Partial Range",
input: "5-7",
expected: map[int]struct{}{
5: {}, 6: {}, 7: {},
},
min: 1, max: 10,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := parseRange(test.input, test.min, test.max)
require.NoError(t, err)
assert.Equal(t, test.expected, output)
})
}
}
func TestCron_parsePart(t *testing.T) {
tests := []struct {
name string
input string
expected map[int]struct{}
min int
max int
hasError bool
}{
{
name: "Single Value",
input: "5",
expected: map[int]struct{}{
5: {},
},
min: 1,
max: 10,
hasError: false,
},
{
name: "Valid Multiple Values",
input: "1,3,5",
expected: map[int]struct{}{
1: {}, 3: {}, 5: {},
},
min: 1,
max: 10,
hasError: false,
},
{
name: "Invalid Value",
input: "15",
expected: nil,
min: 1,
max: 10,
hasError: true,
},
{
name: "Invalid Format",
input: "1,2,a",
expected: nil,
min: 1,
max: 10,
hasError: true,
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
output, err := parsePart(test.input, test.min, test.max)
if test.hasError {
require.Error(t, err, "TEST[%d] - Expected error but got none", i)
} else {
require.NoError(t, err, "TEST[%d] - Expected no error but got: %v", i, err)
}
assert.Len(t, output, len(test.expected), "TEST[%d] - Expected length: %v, got: %v", i, len(test.expected), len(output))
assert.Equal(t, test.expected, output, "TEST[%d] - Expected: %v, got: %v", i, test.expected, output)
})
}
}
func TestCronTab_runScheduled_Panic(t *testing.T) {
testCases := []struct {
desc string
setupContainer func(t *testing.T) *container.Container
jobName string
panicMessage string
expectMetricsCalls bool
}{
{
desc: "panic with metrics",
setupContainer: func(t *testing.T) *container.Container {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
// Expect metrics registration
mocks.Metrics.EXPECT().NewHistogram("app_cron_job_duration", gomock.Any(), gomock.Any()).Times(1)
mocks.Metrics.EXPECT().NewCounter("app_cron_job_total", gomock.Any()).Times(1)
mocks.Metrics.EXPECT().NewCounter("app_cron_job_success", gomock.Any()).Times(1)
mocks.Metrics.EXPECT().NewCounter("app_cron_job_failures", gomock.Any()).Times(1)
// Expect metrics recording during panic
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "app_cron_job_total", "job", "panic-job").Times(1)
mocks.Metrics.EXPECT().RecordHistogram(gomock.Any(), "app_cron_job_duration", gomock.Any(), "job", "panic-job").Times(1)
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "app_cron_job_failures", "job", "panic-job").Times(1)
return mockContainer
},
jobName: "panic-job",
panicMessage: "simulated panic with metrics",
expectMetricsCalls: true,
},
{
desc: "panic without metrics",
setupContainer: func(*testing.T) *container.Container {
return &container.Container{
Logger: logging.NewMockLogger(logging.INFO),
}
},
jobName: "panic-job-no-metrics",
panicMessage: "simulated panic without metrics",
expectMetricsCalls: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
cntnr := tc.setupContainer(t)
j := &job{
sec: map[int]struct{}{1: {}},
min: map[int]struct{}{1: {}},
hour: map[int]struct{}{1: {}},
day: map[int]struct{}{1: {}},
month: map[int]struct{}{1: {}},
dayOfWeek: map[int]struct{}{1: {}},
name: tc.jobName,
fn: func(*Context) {
panic(tc.panicMessage)
},
}
var c *Crontab
if tc.expectMetricsCalls {
c = NewCron(cntnr)
c.jobs = []*job{j}
} else {
c = &Crontab{
ticker: time.NewTicker(time.Second),
container: cntnr,
jobs: []*job{j},
}
}
c.runScheduled(time.Date(2024, 1, 1, 1, 1, 1, 1, time.Local))
time.Sleep(200 * time.Millisecond)
})
assert.Contains(t, logs, fmt.Sprintf("Panic in cron job %s", tc.jobName))
assert.Contains(t, logs, tc.panicMessage)
})
}
}
================================================
FILE: pkg/gofr/crud_handlers.go
================================================
package gofr
import (
"errors"
"fmt"
"reflect"
"gofr.dev/pkg/gofr/datasource/sql"
)
var (
errInvalidObject = errors.New("unexpected object given for AddRESTHandlers")
errEntityNotFound = errors.New("entity not found")
errObjectIsNil = errors.New("object given for AddRESTHandlers is nil")
errNonPointerObject = errors.New("passed object is not pointer")
errFieldCannotBeNull = errors.New("field cannot be null")
errInvalidSQLTag = errors.New("invalid sql tag")
)
type Create interface {
Create(c *Context) (any, error)
}
type GetAll interface {
GetAll(c *Context) (any, error)
}
type Get interface {
Get(c *Context) (any, error)
}
type Update interface {
Update(c *Context) (any, error)
}
type Delete interface {
Delete(c *Context) (any, error)
}
type TableNameOverrider interface {
TableName() string
}
type RestPathOverrider interface {
RestPath() string
}
type CRUD interface {
Create
GetAll
Get
Update
Delete
}
// entity stores information about an entity.
type entity struct {
name string
entityType reflect.Type
primaryKey string
tableName string
restPath string
constraints map[string]sql.FieldConstraints
}
// scanEntity extracts entity information for CRUD operations.
func scanEntity(object any) (*entity, error) {
if object == nil {
return nil, errObjectIsNil
}
objType := reflect.TypeOf(object)
if objType.Kind() != reflect.Ptr {
return nil, fmt.Errorf("failed to register routes for '%s' struct, %w", objType.Name(), errNonPointerObject)
}
entityType := objType.Elem()
if entityType.Kind() != reflect.Struct {
return nil, errInvalidObject
}
structName := entityType.Name()
entityValue := reflect.ValueOf(object).Elem().Type()
primaryKeyField := entityValue.Field(0) // Assume the first field is the primary key
primaryKeyFieldName := toSnakeCase(primaryKeyField.Name)
tableName := getTableName(object, structName)
restPath := getRestPath(object, structName)
e := &entity{
name: structName,
entityType: entityType,
primaryKey: primaryKeyFieldName,
tableName: tableName,
restPath: restPath,
constraints: make(map[string]sql.FieldConstraints),
}
for i := 0; i < entityType.NumField(); i++ {
field := entityType.Field(i)
fieldName := toSnakeCase(field.Name)
constraints, err := parseSQLTag(field.Tag)
if err != nil {
return nil, err
}
e.constraints[fieldName] = constraints
}
return e, nil
}
// registerCRUDHandlers registers CRUD handlers for an entity.
func (a *App) registerCRUDHandlers(e *entity, object any) {
basePath := fmt.Sprintf("/%s", e.restPath)
idPath := fmt.Sprintf("/%s/{%s}", e.restPath, e.primaryKey)
if fn, ok := object.(Create); ok {
a.POST(basePath, fn.Create)
} else {
a.POST(basePath, e.Create)
}
if fn, ok := object.(GetAll); ok {
a.GET(basePath, fn.GetAll)
} else {
a.GET(basePath, e.GetAll)
}
if fn, ok := object.(Get); ok {
a.GET(idPath, fn.Get)
} else {
a.GET(idPath, e.Get)
}
if fn, ok := object.(Update); ok {
a.PUT(idPath, fn.Update)
} else {
a.PUT(idPath, e.Update)
}
if fn, ok := object.(Delete); ok {
a.DELETE(idPath, fn.Delete)
} else {
a.DELETE(idPath, e.Delete)
}
}
func (e *entity) Create(c *Context) (any, error) {
newEntity, err := e.bindAndValidateEntity(c)
if err != nil {
return nil, err
}
fieldNames, fieldValues := e.extractFields(newEntity)
stmt, err := sql.InsertQuery(c.SQL.Dialect(), e.tableName, fieldNames, fieldValues, e.constraints)
if err != nil {
return nil, err
}
result, err := c.SQL.ExecContext(c, stmt, fieldValues...)
if err != nil {
return nil, err
}
var lastID any
if hasAutoIncrementID(e.constraints) { // Check for auto-increment ID
lastID, err = result.LastInsertId()
if err != nil {
return nil, err
}
} else {
lastID = fieldValues[0]
}
return fmt.Sprintf("%s successfully created with id: %v", e.name, lastID), nil
}
func (e *entity) bindAndValidateEntity(c *Context) (any, error) {
newEntity := reflect.New(e.entityType).Interface()
err := c.Bind(newEntity)
if err != nil {
return nil, err
}
for i := 0; i < e.entityType.NumField(); i++ {
field := e.entityType.Field(i)
fieldName := toSnakeCase(field.Name)
if e.constraints[fieldName].NotNull && reflect.ValueOf(newEntity).Elem().Field(i).Interface() == nil {
return nil, fmt.Errorf("%w: %s", errFieldCannotBeNull, fieldName)
}
}
return newEntity, nil
}
func (e *entity) extractFields(newEntity any) (fieldNames []string, fieldValues []any) {
fieldNames = make([]string, 0, e.entityType.NumField())
fieldValues = make([]any, 0, e.entityType.NumField())
for i := 0; i < e.entityType.NumField(); i++ {
field := e.entityType.Field(i)
fieldName := toSnakeCase(field.Name)
if e.constraints[fieldName].AutoIncrement {
continue // Skip auto-increment fields for insertion
}
fieldNames = append(fieldNames, fieldName)
fieldValues = append(fieldValues, reflect.ValueOf(newEntity).Elem().Field(i).Interface())
}
return fieldNames, fieldValues
}
func (e *entity) GetAll(c *Context) (any, error) {
query := sql.SelectQuery(c.SQL.Dialect(), e.tableName)
rows, err := c.SQL.QueryContext(c, query)
if err != nil || rows.Err() != nil {
return nil, err
}
defer rows.Close()
dest := make([]any, e.entityType.NumField())
val := reflect.New(e.entityType).Elem()
for i := 0; i < e.entityType.NumField(); i++ {
dest[i] = val.Field(i).Addr().Interface()
}
var entities []any
for rows.Next() {
newEntity := reflect.New(e.entityType).Interface()
newVal := reflect.ValueOf(newEntity).Elem()
err = rows.Scan(dest...)
if err != nil {
return nil, err
}
for i := 0; i < e.entityType.NumField(); i++ {
scanVal := reflect.ValueOf(dest[i]).Elem().Interface()
newVal.Field(i).Set(reflect.ValueOf(scanVal))
}
entities = append(entities, newEntity)
}
return entities, nil
}
func (e *entity) Get(c *Context) (any, error) {
newEntity := reflect.New(e.entityType).Interface()
id := c.Request.PathParam("id")
query := sql.SelectByQuery(c.SQL.Dialect(), e.tableName, e.primaryKey)
row := c.SQL.QueryRowContext(c, query, id)
dest := make([]any, e.entityType.NumField())
val := reflect.ValueOf(newEntity).Elem()
for i := 0; i < val.NumField(); i++ {
dest[i] = val.Field(i).Addr().Interface()
}
err := row.Scan(dest...)
if err != nil {
return nil, err
}
return newEntity, nil
}
func (e *entity) Update(c *Context) (any, error) {
newEntity := reflect.New(e.entityType).Interface()
id := c.PathParam(e.primaryKey)
err := c.Bind(newEntity)
if err != nil {
return nil, err
}
fieldNames := make([]string, 0, e.entityType.NumField())
fieldValues := make([]any, 0, e.entityType.NumField())
for i := 0; i < e.entityType.NumField(); i++ {
field := e.entityType.Field(i)
fieldNames = append(fieldNames, toSnakeCase(field.Name))
fieldValues = append(fieldValues, reflect.ValueOf(newEntity).Elem().Field(i).Interface())
}
stmt := sql.UpdateByQuery(c.SQL.Dialect(), e.tableName, fieldNames[1:], e.primaryKey)
_, err = c.SQL.ExecContext(c, stmt, append(fieldValues[1:], id)...)
if err != nil {
return nil, err
}
return fmt.Sprintf("%s successfully updated with id: %s", e.name, id), nil
}
func (e *entity) Delete(c *Context) (any, error) {
id := c.PathParam("id")
query := sql.DeleteByQuery(c.SQL.Dialect(), e.tableName, e.primaryKey)
result, err := c.SQL.ExecContext(c, query, id)
if err != nil {
return nil, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, errEntityNotFound
}
return fmt.Sprintf("%s successfully deleted with id: %v", e.name, id), nil
}
================================================
FILE: pkg/gofr/crud_handlers_test.go
================================================
package gofr
import (
"bytes"
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
gofrSql "gofr.dev/pkg/gofr/datasource/sql"
gofrHTTP "gofr.dev/pkg/gofr/http"
)
var (
errSQLScan = errors.New("sql: Scan error on column index 0, name \"id\": converting driver.Value type string " +
"(\"as\") to a int: invalid syntax")
errMock = errors.New("mock error")
)
func createTestContext(method, path, id string, body []byte, cont *container.Container) *Context {
testReq := httptest.NewRequest(method, path+"/"+id, bytes.NewBuffer(body))
testReq = mux.SetURLVars(testReq, map[string]string{"id": id})
testReq.Header.Set("Content-Type", "application/json")
gofrReq := gofrHTTP.NewRequest(testReq)
return newContext(gofrHTTP.NewResponder(httptest.NewRecorder(), method), gofrReq, cont)
}
type userEntity struct {
ID int `json:"id"`
Name string `json:"name"`
IsEmployed bool `json:"isEmployed"`
}
func (*userEntity) TableName() string {
return "user"
}
func (*userEntity) RestPath() string {
return "users"
}
func Test_scanEntity(t *testing.T) {
var invalidObject int
type userTestEntity struct {
ID int `sql:"auto_increment"`
Name string `sql:"not_null"`
}
tests := []struct {
desc string
input any
resp *entity
err error
}{
{
desc: "success case (default)",
input: &userTestEntity{},
resp: &entity{
name: "userTestEntity",
entityType: reflect.TypeOf(userTestEntity{}),
primaryKey: "id",
tableName: "user_test_entity",
restPath: "usertestentity",
constraints: map[string]gofrSql.FieldConstraints{"id": {AutoIncrement: true, NotNull: false},
"name": {AutoIncrement: false, NotNull: true},
},
},
err: nil,
},
{
desc: "success case (custom)",
input: &userEntity{},
resp: &entity{
name: "userEntity",
entityType: reflect.TypeOf(userEntity{}),
primaryKey: "id",
tableName: "user",
restPath: "users",
constraints: map[string]gofrSql.FieldConstraints{"id": {AutoIncrement: false, NotNull: false},
"is_employed": {AutoIncrement: false, NotNull: false}, "name": {AutoIncrement: false, NotNull: false}},
},
err: nil,
},
{
desc: "invalid object",
input: &invalidObject,
resp: nil,
err: errInvalidObject,
},
{
desc: "invalid object type",
input: userEntity{},
resp: nil,
err: fmt.Errorf("failed to register routes for 'userEntity' struct, %w", errNonPointerObject),
},
}
for i, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
resp, err := scanEntity(tc.input)
assert.Equal(t, tc.resp, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.err, err, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
type mockTableName struct {
tableName string
}
func (m *mockTableName) TableName() string {
return m.tableName
}
func Test_getTableName(t *testing.T) {
tests := []struct {
name string
object any
structName string
want string
}{
{
name: "Test with TableNameOverrider interface",
object: &mockTableName{tableName: "custom_table"},
structName: "mockTableName",
want: "custom_table",
},
{
name: "Test without TableNameOverrider interface",
object: &struct{}{},
structName: "TestStruct",
want: "test_struct",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getTableName(tt.object, tt.structName)
assert.Equal(t, tt.want, got)
})
}
}
type mockRestPath struct {
restPath string
}
func (m *mockRestPath) RestPath() string {
return m.restPath
}
func Test_getRestPath(t *testing.T) {
tests := []struct {
name string
object any
structName string
want string
}{
{
name: "Test with RestPathOverrider interface",
object: &mockRestPath{restPath: "custom_path"},
structName: "mockRestPath",
want: "custom_path",
},
{
name: "Test without RestPathOverrider interface - with lower case transformation",
object: &struct{}{},
structName: "TestStruct",
want: "teststruct",
},
{
name: "Test without RestPathOverrider interface",
object: &struct{}{},
structName: "test_struct",
want: "test_struct",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := getRestPath(tt.object, tt.structName)
assert.Equal(t, tt.want, got)
})
}
}
func Test_CreateHandler(t *testing.T) {
e := entity{
name: "userEntity",
entityType: reflect.TypeOf(userEntity{}),
primaryKey: "id",
tableName: "user_entity",
}
tests := []struct {
desc string
dialect string
reqBody []byte
id int
mockErr error
expectedQuery string
expectedResp any
expectedErr error
}{
{
desc: "success case",
dialect: "mysql",
reqBody: []byte(`{"id":1,"name":"goFr","isEmployed":true}`),
id: 1,
mockErr: nil,
expectedQuery: "INSERT INTO `user_entity` (`id`, `name`, `is_employed`) VALUES (?, ?, ?)",
expectedResp: "userEntity successfully created with id: 1",
expectedErr: nil,
},
{
desc: "success case",
dialect: "postgres",
reqBody: []byte(`{"id":1,"name":"goFr","isEmployed":true}`),
id: 1,
mockErr: nil,
expectedQuery: `INSERT INTO "user_entity" ("id", "name", "is_employed") VALUES ($1, $2, $3)`,
expectedResp: "userEntity successfully created with id: 1",
expectedErr: nil,
},
{
desc: "bind error",
dialect: "any-other-dialect",
reqBody: []byte(`{"id":"2"}`),
id: 2,
mockErr: nil,
expectedQuery: "",
expectedResp: nil,
expectedErr: &json.UnmarshalTypeError{Value: "string", Offset: 9, Struct: "userEntity", Field: "id"},
},
}
for i, tc := range tests {
t.Run(tc.dialect+" "+tc.desc, func(t *testing.T) {
c, mocks := container.NewMockContainer(t)
ctrl := gomock.NewController(t)
mockMetrics := gofrSql.NewMockMetrics(ctrl)
ctx := createTestContext(http.MethodPost, "/users", "", tc.reqBody, c)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(),
"hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT").MaxTimes(2)
if tc.expectedErr == nil {
mocks.SQL.ExpectDialect().WillReturnString(tc.dialect)
}
if tc.expectedQuery != "" {
mocks.SQL.ExpectExec(tc.expectedQuery).WithArgs(tc.id, "goFr", true).WillReturnResult(mocks.SQL.NewResult(10, 1))
}
resp, err := e.Create(ctx)
assert.Equal(t, tc.expectedResp, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.IsType(t, tc.expectedErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func Test_GetAllHandler(t *testing.T) {
e := entity{
name: "userEntity",
entityType: reflect.TypeOf(userEntity{}),
primaryKey: "id",
tableName: "user_entity",
}
dialectCases := []struct {
dialect string
expectedQuery string
}{
{
dialect: "mysql",
expectedQuery: "SELECT * FROM `user_entity`",
},
{
dialect: "postgres",
expectedQuery: `SELECT * FROM "user_entity"`,
},
}
type testCase struct {
desc string
mockResp *sqlmock.Rows
mockErr error
expectedResp any
expectedErr error
}
for _, dc := range dialectCases {
tests := []testCase{
{
desc: "success case",
mockResp: sqlmock.NewRows([]string{"id", "name", "is_employed"}).AddRow(1, "John Doe", true).AddRow(2, "Jane Doe", false),
mockErr: nil,
expectedResp: []any{&userEntity{ID: 1, Name: "John Doe", IsEmployed: true},
&userEntity{ID: 2, Name: "Jane Doe", IsEmployed: false}},
expectedErr: nil,
},
{
desc: "error retrieving rows",
mockResp: sqlmock.NewRows([]string{"id", "name", "is_employed"}),
mockErr: errMock,
expectedResp: nil,
expectedErr: errMock,
},
{
desc: "error scanning rows",
mockResp: sqlmock.NewRows([]string{"id", "name", "is_employed"}).AddRow("as", "", false),
mockErr: nil,
expectedResp: nil,
expectedErr: errSQLScan,
},
{
desc: "error retrieving rows",
mockResp: sqlmock.NewRows([]string{"id", "name", "is_employed"}),
mockErr: errTest,
expectedResp: nil,
expectedErr: errTest,
},
}
for i, tc := range tests {
t.Run(dc.dialect+" "+tc.desc, func(t *testing.T) {
c := container.NewContainer(nil)
db, mock, _ := gofrSql.NewSQLMocksWithConfig(t, &gofrSql.DBConfig{Dialect: dc.dialect})
c.SQL = db
defer db.Close()
ctx := createTestContext(http.MethodGet, "/users", "", nil, c)
mock.ExpectQuery(dc.expectedQuery).WillReturnRows(tc.mockResp).WillReturnError(tc.mockErr)
resp, err := e.GetAll(ctx)
assert.Equal(t, tc.expectedResp, resp, "Failed.\n%s", tc.desc)
if tc.expectedErr != nil {
require.ErrorContainsf(t, err, tc.expectedErr.Error(), "TEST[%d], Failed.\n%s", i, tc.desc)
} else {
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
})
}
}
}
func Test_GetHandler(t *testing.T) {
e := entity{
name: "userEntity",
entityType: reflect.TypeOf(userEntity{}),
primaryKey: "id",
tableName: "user_entity",
}
dialectCases := []struct {
dialect string
expectedQuery string
}{
{
dialect: "mysql",
expectedQuery: "SELECT * FROM `user_entity` WHERE `id`=?",
},
{
dialect: "postgres",
expectedQuery: `SELECT * FROM "user_entity" WHERE "id"=$1`,
},
}
type testCase struct {
desc string
id string
mockRow *sqlmock.Rows
mockErr error
expectedResp any
expectedErr error
}
for _, dc := range dialectCases {
testCases := []testCase{
{
desc: "success case",
id: "1",
mockRow: sqlmock.NewRows([]string{"id", "name", "is_employed"}).AddRow(1, "John Doe", true),
mockErr: nil,
expectedResp: &userEntity{ID: 1, Name: "John Doe", IsEmployed: true},
expectedErr: nil,
},
{
desc: "no rows found",
id: "2",
mockRow: sqlmock.NewRows(nil),
mockErr: nil,
expectedResp: nil,
expectedErr: sql.ErrNoRows,
},
{
desc: "error scanning rows",
id: "3",
mockRow: sqlmock.NewRows([]string{"id", "name", "is_employed"}).AddRow("as", "", false),
mockErr: nil,
expectedResp: nil,
expectedErr: errSQLScan,
},
}
for _, tc := range testCases {
t.Run(dc.dialect+" "+tc.desc, func(t *testing.T) {
c := container.NewContainer(nil)
db, mock, _ := gofrSql.NewSQLMocksWithConfig(t, &gofrSql.DBConfig{Dialect: dc.dialect})
c.SQL = db
defer db.Close()
ctx := createTestContext(http.MethodGet, "/user", tc.id, nil, c)
mock.ExpectQuery(dc.expectedQuery).WithArgs(tc.id).WillReturnRows(tc.mockRow).WillReturnError(tc.mockErr)
resp, err := e.Get(ctx)
assert.Equal(t, tc.expectedResp, resp, "Failed.\n%s", tc.desc)
if tc.expectedErr != nil {
require.ErrorContainsf(t, err, tc.expectedErr.Error(), "Failed.\n%s", tc.desc)
} else {
require.NoError(t, err, "Failed.\n%s", tc.desc)
}
})
}
}
}
func Test_UpdateHandler(t *testing.T) {
c := container.NewContainer(nil)
e := entity{
name: "userEntity",
entityType: reflect.TypeOf(userEntity{}),
primaryKey: "id",
tableName: "user_entity",
}
dialectCases := []struct {
dialect string
expectedQuery string
}{
{
dialect: "mysql",
expectedQuery: "UPDATE `user_entity` SET `name`=?, `is_employed`=? WHERE `id`=?",
},
{
dialect: "postgres",
expectedQuery: `UPDATE "user_entity" SET "name"=$1, "is_employed"=$2 WHERE "id"=$3`,
},
}
type testCase struct {
desc string
id string
reqBody []byte
mockErr error
expectedResp any
expectedErr error
}
for _, dc := range dialectCases {
tests := []testCase{
{
desc: "success case",
id: "1",
reqBody: []byte(`{"id":1,"name":"goFr","isEmployed":true}`),
mockErr: nil,
expectedResp: "userEntity successfully updated with id: 1",
expectedErr: nil,
},
{
desc: "bind error",
id: "2",
reqBody: []byte(`{"id":"2"}`),
mockErr: nil,
expectedResp: nil,
expectedErr: &json.UnmarshalTypeError{Value: "string", Offset: 9, Struct: "user", Field: "id"},
},
{
desc: "error From DB",
id: "3",
reqBody: []byte(`{"id":3,"name":"goFr","isEmployed":false}`),
mockErr: sqlmock.ErrCancelled,
expectedResp: nil,
expectedErr: sqlmock.ErrCancelled,
},
}
db, mock, _ := gofrSql.NewSQLMocksWithConfig(t, &gofrSql.DBConfig{Dialect: dc.dialect})
c.SQL = db
for i, tc := range tests {
t.Run(dc.dialect+" "+tc.desc, func(t *testing.T) {
ctx := createTestContext(http.MethodPut, "/user", tc.id, tc.reqBody, c)
mock.ExpectExec(dc.expectedQuery).WithArgs("goFr", true, tc.id).
WillReturnResult(sqlmock.NewResult(1, 1)).WillReturnError(nil)
resp, err := e.Update(ctx)
assert.Equal(t, tc.expectedResp, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.IsType(t, tc.expectedErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
t.Cleanup(func() {
db.Close()
})
}
}
func Test_DeleteHandler(t *testing.T) {
c, mocks := container.NewMockContainer(t)
e := entity{
name: "userEntity",
entityType: nil,
primaryKey: "id",
tableName: "user_entity",
}
dialectCases := []struct {
dialect string
expectedQuery string
}{
{
dialect: "mysql",
expectedQuery: "DELETE FROM `user_entity` WHERE `id`=?",
},
{
dialect: "postgres",
expectedQuery: `DELETE FROM "user_entity" WHERE "id"=$1`,
},
}
type testCase struct {
desc string
id string
mockResp driver.Result
mockErr error
expectedErr error
expectedResp any
}
for _, dc := range dialectCases {
tests := []testCase{
{
desc: "success case",
id: "1",
mockResp: sqlmock.NewResult(1, 1),
mockErr: nil,
expectedErr: nil,
expectedResp: "userEntity successfully deleted with id: 1",
},
{
desc: "SQL error case",
id: "2",
mockResp: nil,
mockErr: errTest,
expectedErr: errTest,
expectedResp: nil,
},
{
desc: "no rows affected",
id: "3",
mockResp: sqlmock.NewResult(0, 0),
mockErr: nil,
expectedErr: errEntityNotFound,
expectedResp: nil,
},
}
for i, tc := range tests {
t.Run(dc.dialect+" "+tc.desc, func(t *testing.T) {
ctx := createTestContext(http.MethodDelete, "/user", tc.id, nil, c)
mocks.SQL.ExpectDialect().WillReturnString(dc.dialect)
mocks.SQL.ExpectExec(dc.expectedQuery).WithArgs(tc.id).WillReturnResult(tc.mockResp).WillReturnError(tc.mockErr)
resp, err := e.Delete(ctx)
assert.Equal(t, tc.expectedResp, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expectedErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
}
================================================
FILE: pkg/gofr/crud_helpers.go
================================================
package gofr
import (
"fmt"
"reflect"
"strings"
"gofr.dev/pkg/gofr/datasource/sql"
)
func getTableName(object any, structName string) string {
if v, ok := object.(TableNameOverrider); ok {
return v.TableName()
}
return toSnakeCase(structName)
}
func getRestPath(object any, structName string) string {
if v, ok := object.(RestPathOverrider); ok {
return v.RestPath()
}
return strings.ToLower(structName)
}
func hasAutoIncrementID(constraints map[string]sql.FieldConstraints) bool {
for _, constraint := range constraints {
if constraint.AutoIncrement {
return true
}
}
return false
}
func parseSQLTag(inputTags reflect.StructTag) (sql.FieldConstraints, error) {
var constraints sql.FieldConstraints
sqlTag := inputTags.Get("sql")
if sqlTag == "" {
return constraints, nil
}
tags := strings.Split(sqlTag, ",")
for _, tag := range tags {
tag = strings.ToLower(tag) // Convert to lowercase for case-insensitivity
switch tag {
case "auto_increment":
constraints.AutoIncrement = true
case "not_null":
constraints.NotNull = true
default:
return constraints, fmt.Errorf("%w: %s", errInvalidSQLTag, tag)
}
}
return constraints, nil
}
func toSnakeCase(str string) string {
diff := 'a' - 'A'
length := len(str)
var builder strings.Builder
for i, char := range str {
if char >= 'a' {
builder.WriteRune(char)
continue
}
if (i != 0 || i == length-1) && ((i > 0 && rune(str[i-1]) >= 'a') || (i < length-1 && rune(str[i+1]) >= 'a')) {
builder.WriteRune('_')
}
builder.WriteRune(char + diff)
}
return builder.String()
}
================================================
FILE: pkg/gofr/datasource/README.md
================================================
# Datasource
GoFr provides following features to ensure robust and observable interactions with various data sources:
1. Health Checks
A mechanism for a datasource to self-report its operational status.
New datasources require implementing the HealthCheck() method with the signature:
```go
HealthCheck() datasource.Health
```
This method should return the current health status of the datasource.
2. Retry Mechanism
GoFr attempts to re-establish connections if lost during application runtime.
New datasources should be verified for built-in retry mechanisms. If absent, implement a mechanism for automatic reconnection.
3. Metrics
Datasources should expose relevant metrics for performance monitoring.
The specific metrics to be implemented depend on the datasource type. Discussions are required to determine the appropriate metrics for each new datasource.
4. Logging
GoFr supports level-based logging with the PrettyPrint interface.
New datasources should implement logging with the following levels:
- DEBUG: Logs connection attempts with critical details.
- INFO: Logs successful connection establishment.
- WARN: Logs connection retrying
> Additional logs can be added to enhance debugging and improve user experience.
5. Tracing
GoFr supports tracing for all the datasources, for example for SQL it traces the request using https://github.com/XSAM/otelsql.
If any official package or any widely used package is not available we have to implement our own, but in the scope of a different ISSUE.
All logs should include:
- Timestamp
- Request ID (Correlation ID)
- Time taken to execute the query
- Datasource name (consistent with other logs)
## Implementing New Datasources
GoFr offers built-in support for popular datasources like SQL (MySQL, PostgreSQL, SQLite), Redis, and Pub/Sub (MQTT, Kafka, Google as a backend). Including additional functionalities within the core GoFr binary would increase the application size unnecessarily.
Therefore, GoFr utilizes a pluggable approach for new datasources by separating implementation in the following way:
- Interface Definition:
Create an interface with required methods within the datasource package.
Register the interface with the container (similar to MongoDB in https://github.com/tfogo/mongodb-go-tutorial).
- Method Registration:
Create a method in gofr.go (similar to the existing one) that accepts the newly defined interface.
- Separate Repository:
Develop a separate repository to implement the interface for the new datasource.
This approach ensures that the new datasource dependency is only loaded when utilized, minimizing binary size for GoFr applications. It also empowers users to create custom implementations beyond the defaults provided by GoFr.
## Supported Datasources
| Datasource | Health-Check | Logs | Metrics | Traces | As Driver |
|------------------|:------------:|:----:|:-------:|:------:|:---------:|
| MySQL | ✅ | ✅ | ✅ | ✅ | |
| REDIS | ✅ | ✅ | ✅ | ✅ | |
| PostgreSQL | ✅ | ✅ | ✅ | ✅ | |
| MongoDB | ✅ | ✅ | ✅ | ✅ | ✅ |
| SQLite | ✅ | ✅ | ✅ | ✅ | |
| BadgerDB | ✅ | ✅ | ✅ | ✅ | ✅ |
| Cassandra | ✅ | ✅ | ✅ | ✅ | ✅ |
| ClickHouse | | ✅ | ✅ | ✅ | ✅ |
| FTP | | ✅ | | | ✅ |
| SFTP | | ✅ | | | ✅ |
| Solr | | ✅ | ✅ | ✅ | ✅ |
| DGraph | ✅ | ✅ | ✅ | ✅ | |
| Azure Event Hubs | | ✅ | ✅ | | ✅ |
| OpenTSDB | ✅ | ✅ | | ✅ | ✅ |
| SurrealDB | ✅ | ✅ | ✅ | ✅ | ✅ |
| ArangoDB | ✅ | ✅ | ✅ | ✅ | ✅ |
| NATS-KV | ✅ | ✅ | ✅ | ✅ | ✅ |
| ScyllaDB | ✅ | ✅ | ✅ | ✅ | |
| Elasticsearch | ✅ | ✅ | ✅ | ✅ | ✅ |
| Couchbase | ✅ | ✅ | ✅ | ✅ | ✅ |
================================================
FILE: pkg/gofr/datasource/arangodb/arango.go
================================================
package arangodb
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/arangodb/go-driver/v2/arangodb"
arangoShared "github.com/arangodb/go-driver/v2/arangodb/shared"
"github.com/arangodb/go-driver/v2/connection"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const (
defaultTimeout = 5 * time.Second
arangoEdgeCollectionType = 3
)
// Client represents an ArangoDB client.
type Client struct {
client arangodb.Client
logger Logger
metrics Metrics
tracer trace.Tracer
config *Config
endpoint string
*DB
*Document
*Graph
}
type EdgeDefinition []arangodb.EdgeDefinition
type UserOptions struct {
Password string `json:"passwd,omitempty"`
Active *bool `json:"active,omitempty"`
Extra any `json:"extra,omitempty"`
}
// Config holds the configuration for ArangoDB connection.
type Config struct {
Host string
User string
Password string
Port int
}
var (
errStatusDown = errors.New("status down")
errMissingField = errors.New("missing required field in config")
errInvalidResultType = errors.New("result must be a pointer to a slice of maps")
errInvalidUserOptionsType = errors.New("userOptions must be a *UserOptions type")
ErrDatabaseExists = errors.New("database already exists")
ErrCollectionExists = errors.New("collection already exists")
ErrGraphExists = errors.New("graph already exists")
)
// New creates a new ArangoDB client with the provided configuration.
func New(c Config) *Client {
client := &Client{
config: &c,
}
client.DB = &DB{client: client}
client.Document = &Document{client: client}
client.Graph = &Graph{client: client}
return client
}
// UseLogger sets the logger for the ArangoDB client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the ArangoDB client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the ArangoDB client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Connect establishes a connection to the ArangoDB server.
func (c *Client) Connect() {
if err := c.validateConfig(); err != nil {
c.logger.Errorf("config validation error: %v", err)
return
}
c.endpoint = fmt.Sprintf("http://%s:%d", c.config.Host, c.config.Port)
c.logger.Debugf("connecting to ArangoDB at %s", c.endpoint)
// Use HTTP connection instead of HTTP2
endpoint := connection.NewRoundRobinEndpoints([]string{c.endpoint})
conn := connection.NewHttpConnection(connection.HttpConfiguration{Endpoint: endpoint})
// Set authentication
auth := connection.NewBasicAuth(c.config.User, c.config.Password)
if err := conn.SetAuthentication(auth); err != nil {
c.logger.Errorf("authentication setup failed: %v", err)
return
}
// Create ArangoDB client
client := arangodb.NewClient(conn)
c.client = client
// Test connection by fetching server version
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
_, err := c.client.Version(ctx)
if err != nil {
c.logger.Errorf("failed to verify connection: %v", err)
return
}
// Initialize metrics
arangoBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_arango_stats", "Response time of ArangoDB operations in milliseconds.", arangoBuckets...)
c.logger.Logf("Connected to ArangoDB successfully at %s", c.endpoint)
}
func (c *Client) validateConfig() error {
if c.config.Host == "" {
return fmt.Errorf("%w: host is empty", errMissingField)
}
if c.config.Port == 0 {
return fmt.Errorf("%w: port is empty", errMissingField)
}
if c.config.User == "" {
return fmt.Errorf("%w: user is empty", errMissingField)
}
if c.config.Password == "" {
return fmt.Errorf("%w: password is empty", errMissingField)
}
return nil
}
// Query executes an AQL (ArangoDB Query Language) query on the specified database and stores the result.
//
// Parameters:
// - ctx: Context for request-scoped values, cancellation, and tracing.
// - dbName: Name of the ArangoDB database where the query will be executed.
// - query: The AQL query string to execute.
// - bindVars: Map of bind parameters used in the AQL query.
// - result: Pointer to a slice of maps where the query results will be unmarshaled.
// Must be a valid pointer to avoid runtime errors.
// - options: A flexible map[string]any to customize query behavior. Keys should be in camelCase
// and correspond to fields in ArangoDB’s QueryOptions and QuerySubOptions structs.
//
// Available option keys include (but are not limited to):
//
// QueryOptions:
// - count (bool): Include the total number of results in the result set.
// - batchSize (int): Number of results to return per batch.
// - cache (bool): Whether to cache the query results.
// - memoryLimit (int64): Maximum memory in bytes for query execution.
// - ttl (float64): Time-to-live for the cursor in seconds.
// - options (map[string]any): Nested options from QuerySubOptions.
//
// QuerySubOptions:
// - allowDirtyReads (bool)
// - allowRetry (bool)
// - failOnWarning (*bool)
// - fullCount (bool): Return full count ignoring LIMIT clause.
// - optimizer (map[string]any): Optimizer-specific directives.
// - maxRuntime (float64): Maximum query runtime in seconds.
// - stream (bool): Enable streaming cursor.
// - profile (uint): Enable query profiling (0-2).
// - skipInaccessibleCollections (*bool)
// - intermediateCommitCount (*int)
// - intermediateCommitSize (*int)
// - maxDNFConditionMembers (*int)
// - maxNodesPerCallstack (*int)
// - maxNumberOfPlans (*int)
// - maxTransactionSize (*int)
// - maxWarningCount (*int)
// - satelliteSyncWait (float64)
// - spillOverThresholdMemoryUsage (*int)
// - spillOverThresholdNumRows (*int)
// - maxPlans (int)
// - shardIds ([]string)
// - forceOneShardAttributeValue (*string)
//
// Returns an error if:
// - The database connection fails.
// - The query execution fails.
// - The result parameter is not a valid pointer to a slice of maps.
//
// Example:
//
// var results []map[string]interface{}
//
// query := `FOR u IN users FILTER u.age > @minAge RETURN u`
//
// bindVars := map[string]interface{}{
// "minAge": 21,
// }
//
// options := map[string]any{
// "count": true,
// "batchSize": 100,
// "options": map[string]any{
// "fullCount": true,
// "profile": 2,
// },
// }
//
// err := Query(ctx, "myDatabase", query, bindVars, &results, options)
// if err != nil {
// log.Fatalf("Query failed: %v", err)
// }
//
// for _, doc := range results {
// fmt.Printf("User: %+v\n", doc)
// }
func (c *Client) Query(ctx context.Context, dbName, query string, bindVars map[string]any, result any, options ...map[string]any) error {
tracerCtx, span := c.addTrace(ctx, "query", map[string]string{"DB": dbName})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{Operation: "query",
Database: dbName, Query: query}, startTime, "query", span)
db, err := c.client.GetDatabase(tracerCtx, dbName, nil)
if err != nil {
return err
}
var queryOptions arangodb.QueryOptions
err = bindQueryOptions(&queryOptions, options)
if err != nil {
return err
}
queryOptions.BindVars = bindVars
cursor, err := db.Query(tracerCtx, query, &queryOptions)
if err != nil {
return err
}
defer cursor.Close()
resultSlice, ok := result.(*[]map[string]any)
if !ok {
return errInvalidResultType
}
for {
var doc map[string]any
_, err = cursor.ReadDocument(tracerCtx, &doc)
if arangoShared.IsNoMoreDocuments(err) {
break
}
if err != nil {
return err
}
*resultSlice = append(*resultSlice, doc)
}
return nil
}
func bindQueryOptions(queryOptions *arangodb.QueryOptions, options []map[string]any) error {
if len(options) > 0 {
// Merge all options into a single map
mergedOpts := make(map[string]any)
for _, opts := range options {
for k, v := range opts {
mergedOpts[k] = v
}
}
bytes, err := json.Marshal(mergedOpts)
if err != nil {
return err
}
if err := json.Unmarshal(bytes, &queryOptions); err != nil {
return err
}
}
return nil
}
// addTrace adds tracing to context if tracer is configured.
func (c *Client) addTrace(ctx context.Context, operation string, attributes map[string]string) (context.Context, trace.Span) {
if c.tracer != nil {
contextWithTrace, span := c.tracer.Start(ctx, fmt.Sprintf("arangodb-%v", operation))
// Add default attributes
span.SetAttributes(attribute.String("arangodb.operation", operation))
// Add custom attributes if provided
for key, value := range attributes {
span.SetAttributes(attribute.String(fmt.Sprintf("arangodb.%s", key), value))
}
return contextWithTrace, span
}
return ctx, nil
}
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
c.metrics.RecordHistogram(context.Background(), "app_arango_stats", float64(duration),
"endpoint", c.endpoint,
"type", ql.Query,
)
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("arangodb.%v.duration", method), duration))
}
}
// Health represents the health status of ArangoDB.
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// HealthCheck performs a health check.
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
h := Health{
Details: map[string]any{
"endpoint": c.endpoint,
},
}
version, err := c.client.Version(ctx)
if err != nil {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
h.Details["version"] = version.Version
h.Details["server"] = version.Server
return &h, nil
}
func (uo *UserOptions) toArangoUserOptions() *arangodb.UserOptions {
return &arangodb.UserOptions{
Password: uo.Password,
Active: uo.Active,
Extra: uo.Extra,
}
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_db.go
================================================
package arangodb
import (
"context"
"time"
"github.com/arangodb/go-driver/v2/arangodb"
)
type DB struct {
client *Client
}
// CreateDB creates a new database in ArangoDB.
// It first checks if the database already exists before attempting to create it.
// Returns ErrDatabaseExists if the database already exists.
func (d *DB) CreateDB(ctx context.Context, database string) error {
tracerCtx, span := d.client.addTrace(ctx, "createDB", map[string]string{"DB": database})
startTime := time.Now()
defer d.client.sendOperationStats(&QueryLog{Operation: "createDB", Database: database}, startTime, "createDB", span)
// Check if the database already exists
exists, err := d.client.client.DatabaseExists(tracerCtx, database)
if err != nil {
return err
}
if exists {
d.client.logger.Debugf("database %s already exists", database)
return ErrDatabaseExists
}
_, err = d.client.client.CreateDatabase(tracerCtx, database, nil)
return err
}
// DropDB deletes a database from ArangoDB.
func (d *DB) DropDB(ctx context.Context, database string) error {
tracerCtx, span := d.client.addTrace(ctx, "dropDB", map[string]string{"DB": database})
startTime := time.Now()
defer d.client.sendOperationStats(&QueryLog{Operation: "dropDB", Database: database}, startTime, "dropDB", span)
db, err := d.client.client.GetDatabase(tracerCtx, database, &arangodb.GetDatabaseOptions{})
if err != nil {
return err
}
err = db.Remove(tracerCtx)
if err != nil {
return err
}
return err
}
// CreateCollection creates a new collection in a database with specified type.
// It first checks if the collection already exists before attempting to create it.
// Returns ErrCollectionExists if the collection already exists.
func (d *DB) CreateCollection(ctx context.Context, database, collection string, isEdge bool) error {
tracerCtx, span := d.client.addTrace(ctx, "createCollection", map[string]string{"collection": collection})
startTime := time.Now()
defer d.client.sendOperationStats(&QueryLog{Operation: "createCollection", Database: database,
Collection: collection, Filter: isEdge}, startTime, "createCollection", span)
db, err := d.client.client.GetDatabase(tracerCtx, database, nil)
if err != nil {
return err
}
// Check if the collection already exists
exists, err := db.CollectionExists(tracerCtx, collection)
if err != nil {
return err
}
if exists {
d.client.logger.Debugf("collection %s already exists in database %s", collection, database)
return ErrCollectionExists
}
collectionType := arangodb.CollectionTypeDocument
if isEdge {
collectionType = arangodb.CollectionTypeEdge
}
options := arangodb.CreateCollectionPropertiesV2{Type: &collectionType}
_, err = db.CreateCollectionV2(tracerCtx, collection, &options)
return err
}
// DropCollection deletes an existing collection from a database.
func (d *DB) DropCollection(ctx context.Context, database, collectionName string) error {
return d.handleCollectionOperation(ctx, "dropCollection", database, collectionName, func(collection arangodb.Collection) error {
return collection.Remove(ctx)
})
}
func (d *DB) getCollection(ctx context.Context, dbName, collectionName string) (arangodb.Collection, error) {
db, err := d.client.client.GetDatabase(ctx, dbName, nil)
if err != nil {
return nil, err
}
collection, err := db.GetCollection(ctx, collectionName, nil)
if err != nil {
return nil, err
}
return collection, nil
}
// handleCollectionOperation handles common logic for collection operations.
func (d *DB) handleCollectionOperation(ctx context.Context, operation, database, collectionName string,
action func(arangodb.Collection) error) error {
tracerCtx, span := d.client.addTrace(ctx, operation, map[string]string{"collection": collectionName})
startTime := time.Now()
defer d.client.sendOperationStats(&QueryLog{Operation: operation, Database: database,
Collection: collectionName}, startTime, operation, span)
collection, err := d.getCollection(tracerCtx, database, collectionName)
if err != nil {
return err
}
return action(collection)
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_db_test.go
================================================
package arangodb
import (
"context"
"errors"
"testing"
"github.com/arangodb/go-driver/v2/arangodb"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
var errCollectionNotFound = errors.New("collection not found")
func Test_Client_CreateDB(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
database := "testDB"
mockArango.EXPECT().DatabaseExists(gomock.Any(), gomock.Any()).Return(false, nil)
mockArango.EXPECT().CreateDatabase(gomock.Any(), database, nil).Return(nil, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateDB(ctx, database)
require.NoError(t, err, "Expected no error while creating the database")
}
func Test_Client_CreateDB_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
database := "errorDB"
mockArango.EXPECT().DatabaseExists(gomock.Any(), gomock.Any()).Return(false, errDBNotFound)
mockArango.EXPECT().CreateDatabase(gomock.Any(), database, nil).Return(nil, errDBNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateDB(ctx, database)
require.Error(t, err, "Expected an error while creating the database")
require.Equal(t, "database not found", err.Error())
}
func Test_Client_CreateDB_AlreadyExists(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
database := "dbExists"
mockArango.EXPECT().DatabaseExists(gomock.Any(), gomock.Any()).Return(true, nil)
mockLogger.EXPECT().Debugf("database %s already exists", database)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateDB(ctx, database)
require.Equal(t, ErrDatabaseExists, err, "Expected error when database already exists")
}
func Test_Client_DropDB(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
database := "testDB"
ctrl := gomock.NewController(t)
mockDB := NewMockDatabase(ctrl)
// Mock the database method to return a mock database instance
mockArango.EXPECT().GetDatabase(gomock.Any(), database, &arangodb.GetDatabaseOptions{}).
Return(arangodb.Database(mockDB), nil).Times(1)
mockDB.EXPECT().Remove(gomock.Any()).Return(nil).Times(1)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DropDB(ctx, database)
require.NoError(t, err, "Expected no error while dropping the database")
}
func Test_Client_DropDB_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
database := "testDB"
mockArango.EXPECT().GetDatabase(gomock.Any(), database, &arangodb.GetDatabaseOptions{}).
Return(nil, errDBNotFound).Times(1)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DropDB(ctx, database)
require.Error(t, err, "Expected error when trying to drop a non-existent database")
require.Equal(t, "database not found", err.Error())
}
func Test_Client_DropDB_RemoveError(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", &arangodb.GetDatabaseOptions{}).
Return(mockDB, nil).Times(1)
mockDB.EXPECT().Remove(gomock.Any()).Return(errDBNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DropDB(context.Background(), "testDB")
require.Error(t, err, "Expected error when removing the database")
require.Equal(t, "database not found", err.Error())
}
func Test_Client_CreateCollection(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil)
mockDB.EXPECT().CollectionExists(gomock.Any(), "testCollection").Return(false, nil)
mockDB.EXPECT().CreateCollectionV2(gomock.Any(), "testCollection", gomock.Any()).Return(nil, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateCollection(context.Background(), "testDB", "testCollection", true)
require.NoError(t, err, "Expected no error while creating the collection")
}
func Test_Client_CreateCollection_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil)
mockDB.EXPECT().CollectionExists(gomock.Any(), "testCollection").Return(false, nil)
mockDB.EXPECT().CreateCollectionV2(gomock.Any(), "testCollection", gomock.Any()).Return(nil, errCollectionNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateCollection(context.Background(), "testDB", "testCollection", false)
require.Error(t, err, "Expected an error while creating the collection")
require.Equal(t, "collection not found", err.Error())
}
func Test_Client_CreateCollection_AlreadyExists(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "dbExists", nil).
Return(mockDB, nil)
mockDB.EXPECT().CollectionExists(gomock.Any(), "testCollection").Return(true, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf("collection %s already exists in database %s",
"testCollection", "dbExists")
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.CreateCollection(context.Background(), "dbExists", "testCollection", true)
require.Equal(t, ErrCollectionExists, err, "Expected error when collection already exists")
}
func Test_Client_DropCollection(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctrl := gomock.NewController(t)
mockDB := NewMockDatabase(ctrl)
mockCollection := NewMockCollection(ctrl)
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil)
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil)
mockCollection.EXPECT().Remove(gomock.Any()).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats",
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Execute
err := client.DropCollection(context.Background(), "testDB", "testCollection")
require.NoError(t, err, "Expected no error while dropping the collection")
}
func Test_Client_DropCollection_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil)
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(nil, errCollectionNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DropCollection(context.Background(), "testDB", "testCollection")
require.Error(t, err, "Expected error when trying to drop a non-existent collection")
require.Equal(t, "collection not found", err.Error())
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_document.go
================================================
package arangodb
import (
"context"
"errors"
"time"
"github.com/arangodb/go-driver/v2/arangodb"
)
var (
errInvalidEdgeDocumentType = errors.New("document must be a map when creating an edge")
errMissingEdgeFields = errors.New("missing '_from' or '_to' field for edge document")
errInvalidFromField = errors.New("'_from' field must be a string")
errInvalidToField = errors.New("'_to' field must be a string")
)
type Document struct {
client *Client
}
// CreateDocument creates a new document in the specified collection.
// If the collection is an edge collection, the document must include `_from` and `_to`.
//
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - dbName: Name of the database where the document will be created.
// - collectionName: Name of the collection where the document will be created.
// - document: The document to be created. For edge collections, it must include `_from` and `_to` fields.
//
// Returns the ID of the created document and an error if the document creation fails.
//
// Example for creating a regular document:
//
// doc := map[string]any{
// "name": "Alice",
// "age": 30,
// }
//
// id, err := client.CreateDocument(ctx, "myDB", "users", doc)
//
// Example for creating an edge document:
//
// edgeDoc := map[string]any{
// "_from": "users/123",
// "_to": "orders/456",
// "relation": "purchased",
// }
//
// id, err := client.CreateDocument(ctx, "myDB", "edges", edgeDoc).
func (d *Document) CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error) {
collection, tracerCtx, err := executeCollectionOperation(ctx, *d, dbName, collectionName,
"createDocument", "")
if err != nil {
return "", err
}
var isEdge bool
// Check if the collection is an edge collection
isEdge, err = d.isEdgeCollection(ctx, dbName, collectionName)
if err != nil {
return "", err
}
// Validate edge document if needed
if isEdge {
err = validateEdgeDocument(document)
if err != nil {
return "", err
}
}
// Create the document in ArangoDB
meta, err := collection.CreateDocument(tracerCtx, document)
if err != nil {
return "", err
}
return meta.Key, nil
}
// GetDocument retrieves a document by its ID from the specified collection.
func (d *Document) GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error {
collection, tracerCtx, err := executeCollectionOperation(ctx, *d, dbName, collectionName,
"getDocument", documentID)
if err != nil {
return err
}
_, err = collection.ReadDocument(tracerCtx, documentID, result)
return err
}
// UpdateDocument updates an existing document in the specified collection.
func (d *Document) UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error {
collection, tracerCtx, err := executeCollectionOperation(ctx, *d, dbName, collectionName,
"updateDocument", documentID)
if err != nil {
return err
}
_, err = collection.UpdateDocument(tracerCtx, documentID, document)
return err
}
// DeleteDocument deletes a document by its ID from the specified collection.
func (d *Document) DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error {
collection, tracerCtx, err := executeCollectionOperation(ctx, *d, dbName, collectionName,
"deleteDocument", documentID)
if err != nil {
return err
}
_, err = collection.DeleteDocument(tracerCtx, documentID)
return err
}
// isEdgeCollection checks if the given collection is an edge collection.
func (d *Document) isEdgeCollection(ctx context.Context, dbName, collectionName string) (bool, error) {
collection, err := d.client.getCollection(ctx, dbName, collectionName)
if err != nil {
return false, err
}
properties, err := collection.Properties(ctx)
if err != nil {
return false, err
}
// ArangoDB type: 3 = Edge Collection, 2 = Document Collection
return properties.Type == arangoEdgeCollectionType, nil
}
func executeCollectionOperation(ctx context.Context, d Document, dbName, collectionName,
operation string, documentID string) (arangodb.Collection, context.Context, error) {
tracerCtx, span := d.client.addTrace(ctx, operation, map[string]string{"collection": collectionName})
startTime := time.Now()
ql := &QueryLog{Operation: operation,
Database: dbName,
Collection: collectionName}
if documentID != "" {
ql.ID = documentID
}
defer d.client.sendOperationStats(ql, startTime, operation, span)
collection, err := d.client.getCollection(tracerCtx, dbName, collectionName)
if err != nil {
return nil, nil, err
}
return collection, tracerCtx, nil
}
// validateEdgeDocument ensures the document contains valid `_from` and `_to` fields when creating an edge.
func validateEdgeDocument(document any) error {
docMap, ok := document.(map[string]any)
if !ok {
return errInvalidEdgeDocumentType
}
from, fromExists := docMap["_from"]
to, toExists := docMap["_to"]
if !fromExists || !toExists {
return errMissingEdgeFields
}
// Ensure `_from` and `_to` are strings
if _, ok := from.(string); !ok {
return errInvalidFromField
}
if _, ok := to.(string); !ok {
return errInvalidToField
}
return nil
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_document_test.go
================================================
package arangodb
import (
"context"
"testing"
"github.com/arangodb/go-driver/v2/arangodb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_Client_CreateDocument(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().Properties(gomock.Any()).Return(arangodb.CollectionProperties{}, nil)
mockCollection.EXPECT().CreateDocument(gomock.Any(), "testDocument").
Return(arangodb.CollectionDocumentCreateResponse{DocumentMeta: arangodb.DocumentMeta{
Key: "testDocument", ID: "1"}}, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
docName, err := client.CreateDocument(context.Background(), "testDB",
"testCollection", "testDocument")
require.Equal(t, "testDocument", docName)
require.NoError(t, err, "Expected no error while truncating the collection")
}
func Test_Client_CreateDocument_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().Properties(gomock.Any()).Return(arangodb.CollectionProperties{}, nil)
mockCollection.EXPECT().CreateDocument(gomock.Any(), "testDocument").
Return(arangodb.CollectionDocumentCreateResponse{}, errDocumentNotFound)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
docName, err := client.CreateDocument(context.Background(), "testDB",
"testCollection", "testDocument")
require.Empty(t, docName)
require.ErrorIs(t, err, errDocumentNotFound, "Expected error when document not found")
}
func Test_Client_GetDocument(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().ReadDocument(gomock.Any(), "testDocument", "").Return(arangodb.DocumentMeta{
Key: "testKey", ID: "1"}, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.GetDocument(context.Background(), "testDB",
"testCollection", "testDocument", "")
require.NoError(t, err, "Expected no error while reading the document")
}
func Test_Client_GetDocument_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().ReadDocument(gomock.Any(), "testDocument", "").
Return(arangodb.DocumentMeta{}, errDocumentNotFound)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
err := client.GetDocument(context.Background(), "testDB",
"testCollection", "testDocument", "")
require.ErrorIs(t, err, errDocumentNotFound, "Expected error when document not found")
}
func Test_Client_UpdateDocument(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
testDocument := map[string]any{"field": "value"}
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().UpdateDocument(gomock.Any(), "testDocument", testDocument).
Return(arangodb.CollectionDocumentUpdateResponse{
DocumentMetaWithOldRev: arangodb.DocumentMetaWithOldRev{DocumentMeta: arangodb.DocumentMeta{Key: "testKey", ID: "1", Rev: ""}}}, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.UpdateDocument(context.Background(), "testDB", "testCollection",
"testDocument", testDocument)
require.NoError(t, err, "Expected no error while updating the document")
}
func Test_Client_UpdateDocument_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
testDocument := map[string]any{"field": "value"}
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().UpdateDocument(gomock.Any(), "testDocument", testDocument).
Return(arangodb.CollectionDocumentUpdateResponse{}, errDocumentNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.UpdateDocument(context.Background(), "testDB", "testCollection", "testDocument", testDocument)
require.ErrorIs(t, err, errDocumentNotFound, "Expected error while updating the document")
}
func Test_Client_DeleteDocument(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().DeleteDocument(gomock.Any(), "testDocument").
Return(arangodb.CollectionDocumentDeleteResponse{
DocumentMeta: arangodb.DocumentMeta{Key: "testKey", ID: "1", Rev: ""}}, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DeleteDocument(context.Background(), "testDB", "testCollection",
"testDocument")
require.NoError(t, err, "Expected no error while updating the document")
}
func Test_Client_DeleteDocument_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockDB := NewMockDatabase(gomock.NewController(t))
mockCollection := NewMockCollection(gomock.NewController(t))
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDB, nil).AnyTimes()
mockDB.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockCollection.EXPECT().DeleteDocument(gomock.Any(), "testDocument").
Return(arangodb.CollectionDocumentDeleteResponse{}, errDocumentNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.DeleteDocument(context.Background(), "testDB", "testCollection",
"testDocument")
require.ErrorIs(t, err, errDocumentNotFound, "Expected error while updating the document")
}
func TestExecuteCollectionOperation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockArango := NewMockClient(ctrl)
mockDatabase := NewMockDatabase(ctrl)
mockCollection := NewMockCollection(ctrl)
client := New(Config{Host: "localhost", Port: 8527, User: "root", Password: "root"})
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.client = mockArango
d := Document{client: client}
ctx := context.Background()
dbName := "testDB"
collectionName := "testCollection"
operation := "createDocument"
documentID := "doc123"
mockArango.EXPECT().GetDatabase(gomock.Any(), "testDB", nil).
Return(mockDatabase, nil).AnyTimes()
mockDatabase.EXPECT().GetCollection(gomock.Any(), "testCollection", nil).
Return(mockCollection, nil).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(ctx, "app_arango_stats", gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
_, _, err := executeCollectionOperation(ctx, d, dbName, collectionName, operation, documentID)
require.NoError(t, err)
}
func TestValidateEdgeDocument(t *testing.T) {
tests := []struct {
name string
document any
expectedError error
}{
{
name: "Success - Valid Edge Document",
document: map[string]any{
"_from": "vertex1",
"_to": "vertex2",
},
expectedError: nil,
},
{
name: "Fail - Document is Not a Map",
document: "invalid",
expectedError: errInvalidEdgeDocumentType,
},
{
name: "Fail - Missing _from Field",
document: map[string]any{
"_to": "vertex2",
},
expectedError: errMissingEdgeFields,
},
{
name: "Fail - Missing _to Field",
document: map[string]any{
"_from": "vertex1",
},
expectedError: errMissingEdgeFields,
},
{
name: "Fail - _from is Not a String",
document: map[string]any{
"_from": 123,
"_to": "vertex2",
},
expectedError: errInvalidFromField,
},
{
name: "Fail - _to is Not a String",
document: map[string]any{
"_from": "vertex1",
"_to": 123,
},
expectedError: errInvalidToField,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateEdgeDocument(tc.document)
assert.Equal(t, tc.expectedError, err)
})
}
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_graph.go
================================================
package arangodb
import (
"context"
"errors"
"fmt"
"time"
"github.com/arangodb/go-driver/v2/arangodb"
)
var (
errInvalidEdgeDefinitionsType = errors.New("edgeDefinitions must be a *EdgeDefinition type")
errNilEdgeDefinitions = errors.New("edgeDefinitions cannot be nil")
errInvalidInput = errors.New("invalid input parameter")
errInvalidResponseType = errors.New("invalid response type")
)
type EdgeDetails []arangodb.EdgeDetails
type Graph struct {
client *Client
}
// CreateGraph creates a new graph in a database.
// It first checks if the graph already exists before attempting to create it.
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - database: Name of the database where the graph will be created.
// - graph: Name of the graph to be created.
// - edgeDefinitions: Pointer to EdgeDefinition struct containing edge definitions.
//
// Returns ErrGraphExists if the graph already exists.
// Returns an error if the edgeDefinitions parameter is not of type *EdgeDefinition or is nil.
func (g *Graph) CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error {
tracerCtx, span := g.client.addTrace(ctx, "createGraph", map[string]string{"graph": graph})
startTime := time.Now()
defer g.client.sendOperationStats(&QueryLog{Operation: "createGraph",
Database: database, Collection: graph}, startTime, "createGraph", span)
db, err := g.client.client.GetDatabase(tracerCtx, database, nil)
if err != nil {
return err
}
// Check if the graph already exists
exists, err := db.GraphExists(tracerCtx, graph)
if err != nil {
return err
}
if exists {
g.client.logger.Debugf("graph %s already exists in database %s", graph, database)
return ErrGraphExists
}
edgeDefs, ok := edgeDefinitions.(*EdgeDefinition)
if !ok {
return fmt.Errorf("%w", errInvalidEdgeDefinitionsType)
}
if edgeDefs == nil {
return fmt.Errorf("%w", errNilEdgeDefinitions)
}
arangoEdgeDefs := make(EdgeDefinition, 0, len(*edgeDefs))
for _, ed := range *edgeDefs {
arangoEdgeDefs = append(arangoEdgeDefs, arangodb.EdgeDefinition{
Collection: ed.Collection,
From: ed.From,
To: ed.To,
})
}
options := &arangodb.GraphDefinition{
EdgeDefinitions: arangoEdgeDefs,
}
_, err = db.CreateGraph(tracerCtx, graph, options, nil)
return err
}
// DropGraph deletes an existing graph from a database.
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - database: Name of the database where the graph exists.
// - graphName: Name of the graph to be deleted.
//
// Returns an error if the graph does not exist or if there is an issue with the database connection.
func (g *Graph) DropGraph(ctx context.Context, database, graphName string) error {
tracerCtx, span := g.client.addTrace(ctx, "dropGraph", map[string]string{"graph": graphName})
startTime := time.Now()
defer g.client.sendOperationStats(&QueryLog{Operation: "dropGraph",
Database: database}, startTime, "dropGraph", span)
db, err := g.client.client.GetDatabase(tracerCtx, database, nil)
if err != nil {
return err
}
graph, err := db.Graph(tracerCtx, graphName, nil)
if err != nil {
return err
}
err = graph.Remove(tracerCtx, &arangodb.RemoveGraphOptions{DropCollections: true})
if err != nil {
return err
}
return err
}
// GetEdges fetches all edges connected to a given vertex in the specified edge collection.
//
// Parameters:
// - ctx: Request context for tracing and cancellation.
// - dbName: Database name.
// - graphName: Graph name.
// - edgeCollection: Edge collection name.
// - vertexID: Full vertex ID (e.g., "persons/16563").
// - resp: Pointer to `*EdgeDetails` to store results.
//
// Returns an error if input is invalid, `resp` is of the wrong type, or the query fails.
func (c *Client) GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string,
resp any) error {
if vertexID == "" || edgeCollection == "" {
return errInvalidInput
}
// Type check the response parameter
edgeResp, ok := resp.(*EdgeDetails)
if !ok {
return fmt.Errorf("%w: must be *[]arangodb.EdgeDetails", errInvalidResponseType)
}
tracerCtx, span := c.addTrace(ctx, "getEdges", map[string]string{
"DB": dbName, "Graph": graphName, "Collection": edgeCollection, "Vertex": vertexID,
})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Operation: "getEdges",
Database: dbName,
Collection: edgeCollection,
}, startTime, "getEdges", span)
db, err := c.client.GetDatabase(tracerCtx, dbName, nil)
if err != nil {
return err
}
edges, err := db.GetEdges(tracerCtx, edgeCollection, vertexID, nil)
if err != nil {
return err
}
// Assign the result to the provided response parameter
*edgeResp = edges
return nil
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_graph_test.go
================================================
package arangodb
import (
"context"
"testing"
"github.com/arangodb/go-driver/v2/arangodb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
// TestGraph represents the test environment for graph-related tests.
type TestGraph struct {
Ctrl *gomock.Controller
MockArango *MockClient
MockDB *MockDatabase
MockLogger *MockLogger
MockMetrics *MockMetrics
Client *Client
Graph *Graph
Ctx context.Context
DBName string
GraphName string
EdgeDefs *EdgeDefinition
}
// setupGraphTest creates a new test environment for graph tests.
func setupGraphTest(t *testing.T) *TestGraph {
t.Helper()
ctrl := gomock.NewController(t)
mockArango := NewMockClient(ctrl)
mockDB := NewMockDatabase(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Setup common expectations
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
client := &Client{
logger: mockLogger,
metrics: mockMetrics,
client: mockArango,
}
graph := &Graph{client: client}
ctx := context.Background()
return &TestGraph{
Ctrl: ctrl,
MockArango: mockArango,
MockDB: mockDB,
MockLogger: mockLogger,
MockMetrics: mockMetrics,
Client: client,
Graph: graph,
Ctx: ctx,
DBName: "testDB",
GraphName: "testGraph",
EdgeDefs: &EdgeDefinition{{Collection: "edgeColl", From: []string{"fromColl"}, To: []string{"toColl"}}},
}
}
func TestGraph_CreateGraph_Success(t *testing.T) {
// Setup
test := setupGraphTest(t)
defer test.Ctrl.Finish()
mockGraph := NewMockGraph(test.Ctrl)
graphInterface := arangodb.Graph(mockGraph)
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().GraphExists(test.Ctx, test.GraphName).Return(false, nil)
test.MockDB.EXPECT().CreateGraph(
test.Ctx, test.GraphName, gomock.Any(), nil,
).Return(graphInterface, nil)
err := test.Graph.CreateGraph(test.Ctx, test.DBName, test.GraphName, test.EdgeDefs)
require.NoError(t, err, "expected err to be nil but got %v", err)
}
func TestGraph_CreateGraph_AlreadyExists(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().GraphExists(test.Ctx, test.GraphName).Return(true, nil)
test.MockLogger.EXPECT().Debugf("graph %s already exists in database %s", test.GraphName, test.DBName)
err := test.Graph.CreateGraph(test.Ctx, test.DBName, test.GraphName, test.EdgeDefs)
assert.Equal(t, ErrGraphExists, err, "Expected graph already exits error but got %v", err)
}
func TestGraph_CreateGraph_Error(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
options := &arangodb.GraphDefinition{EdgeDefinitions: []arangodb.EdgeDefinition{{
Collection: "edgeColl",
From: []string{"fromColl"},
To: []string{"toColl"},
}}}
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().GraphExists(test.Ctx, test.GraphName).Return(false, nil)
test.MockDB.EXPECT().CreateGraph(test.Ctx, test.GraphName, options, nil).Return(nil, errInvalidEdgeDocumentType)
err := test.Graph.CreateGraph(test.Ctx, test.DBName, test.GraphName, test.EdgeDefs)
assert.Equal(t, errInvalidEdgeDocumentType, err, "Expected err %v but got %v",
errInvalidEdgeDocumentType, err)
}
func TestGraph_DropGraph_Success(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
mockGraph := NewMockGraph(test.Ctrl)
graphInterface := arangodb.Graph(mockGraph)
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().Graph(test.Ctx, test.GraphName, nil).Return(graphInterface, nil)
mockGraph.EXPECT().Remove(test.Ctx, &arangodb.RemoveGraphOptions{DropCollections: true}).Return(nil)
err := test.Graph.DropGraph(test.Ctx, test.DBName, test.GraphName)
require.NoError(t, err, "expected err to be nil but got %v", err)
}
func TestGraph_DropGraph_DBError(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(nil, errDBNotFound)
err := test.Graph.DropGraph(test.Ctx, test.DBName, test.GraphName)
assert.Equal(t, errDBNotFound, err, "expected err %v but got %v", errDBNotFound, err)
}
func TestGraph_DropGraph_Error(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
mockGraph := NewMockGraph(test.Ctrl)
graphInterface := arangodb.Graph(mockGraph)
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().Graph(test.Ctx, test.GraphName, nil).Return(graphInterface, nil)
mockGraph.EXPECT().Remove(test.Ctx, &arangodb.RemoveGraphOptions{DropCollections: true}).Return(errStatusDown)
err := test.Graph.DropGraph(test.Ctx, test.DBName, test.GraphName)
assert.Equal(t, errStatusDown, err, "expected err %v but got %v", errStatusDown, err)
}
func TestClient_GetEdges_Success(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
edgeCollection := "edgeColl"
vertexID := "vertexID"
expectedEdges := []arangodb.EdgeDetails{{
To: "toColl",
From: "fromColl",
Label: "label",
}}
var resp EdgeDetails
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().GetEdges(test.Ctx, edgeCollection, vertexID, nil).Return(expectedEdges, nil)
err := test.Client.GetEdges(test.Ctx, test.DBName, test.GraphName, edgeCollection, vertexID, &resp)
require.NoError(t, err)
assert.Equal(t, expectedEdges, []arangodb.EdgeDetails(resp))
}
func TestClient_GetEdges_DBError(t *testing.T) {
// Setup
test := setupGraphTest(t)
defer test.Ctrl.Finish()
edgeCollection := "edgeColl"
vertexID := "vertexID"
var resp EdgeDetails
test.MockArango.EXPECT().GetDatabase(test.Ctx, test.DBName, nil).
Return(nil, errDBNotFound)
err := test.Client.GetEdges(test.Ctx, test.DBName, test.GraphName, edgeCollection, vertexID, &resp)
require.Error(t, err)
require.Equal(t, errDBNotFound, err)
}
func TestClient_GetEdges_InvalidInput(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
var resp EdgeDetails
err := test.Client.GetEdges(test.Ctx, test.DBName, test.GraphName, "", "", &resp)
require.Error(t, err)
require.Equal(t, errInvalidInput, err)
}
func TestClient_GetEdges_InvalidResponseType(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
edgeCollection := "edgeColl"
vertexID := "vertexID"
var resp string
err := test.Client.GetEdges(test.Ctx, test.DBName, test.GraphName, edgeCollection, vertexID, &resp)
require.Error(t, err)
require.ErrorIs(t, err, errInvalidResponseType)
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_helper.go
================================================
package arangodb
import (
"context"
"fmt"
"time"
"github.com/arangodb/go-driver/v2/arangodb"
)
func (c *Client) user(ctx context.Context, username string) (arangodb.User, error) {
return c.client.User(ctx, username)
}
func (c *Client) database(ctx context.Context, name string) (arangodb.Database, error) {
return c.client.GetDatabase(ctx, name, nil)
}
// createUser creates a new user in ArangoDB.
func (c *Client) createUser(ctx context.Context, username string, options any) error {
tracerCtx, span := c.addTrace(ctx, "createUser", map[string]string{"user": username})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{Operation: "createUser", ID: username},
startTime, "createUser", span)
userOptions, ok := options.(UserOptions)
if !ok {
return fmt.Errorf("%w", errInvalidUserOptionsType)
}
_, err := c.client.CreateUser(tracerCtx, username, userOptions.toArangoUserOptions())
if err != nil {
return err
}
return nil
}
// dropUser deletes a user from ArangoDB.
func (c *Client) dropUser(ctx context.Context, username string) error {
tracerCtx, span := c.addTrace(ctx, "dropUser", map[string]string{"user": username})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{Operation: "dropUser",
ID: username}, startTime, "dropUser", span)
err := c.client.RemoveUser(tracerCtx, username)
if err != nil {
return err
}
return err
}
// grantDB grants permissions for a database to a user.
func (c *Client) grantDB(ctx context.Context, database, username, permission string) error {
tracerCtx, span := c.addTrace(ctx, "grantDB", map[string]string{"DB": database})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{Operation: "grantDB",
Database: database, ID: username}, startTime, "grantDB", span)
user, err := c.client.User(tracerCtx, username)
if err != nil {
return err
}
err = user.SetDatabaseAccess(tracerCtx, database, arangodb.Grant(permission))
return err
}
// grantCollection grants permissions for a collection to a user.
func (c *Client) grantCollection(ctx context.Context, database, collection, username, permission string) error {
tracerCtx, span := c.addTrace(ctx, "GrantCollection", map[string]string{"collection": collection})
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{Operation: "GrantCollection",
Database: database, Collection: collection, ID: username}, startTime,
"GrantCollection", span)
user, err := c.client.User(tracerCtx, username)
if err != nil {
return err
}
err = user.SetCollectionAccess(tracerCtx, database, collection, arangodb.Grant(permission))
return err
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_helper_test.go
================================================
package arangodb
import (
"context"
"testing"
"github.com/arangodb/go-driver/v2/arangodb"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
func Test_Client_CreateUser(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockArango.EXPECT().CreateUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(context.Background(), "app_arango_stats",
gomock.Any(), "endpoint", gomock.Any(), gomock.Any(), gomock.Any())
err := client.createUser(context.Background(), "test", UserOptions{
Password: "user123",
Extra: nil,
})
require.NoError(t, err, "Test_Arango_CreateUser: failed to create user")
}
func Test_Client_DropUser(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockArango.EXPECT().RemoveUser(gomock.Any(), gomock.Any()).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(context.Background(), "app_arango_stats", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any())
err := client.dropUser(context.Background(), "test")
require.NoError(t, err, "Test_Arango_DropUser: failed to drop user")
}
func Test_Client_GrantDB(t *testing.T) {
client, mockArango, mockUser, mockLogger, mockMetrics := setupDB(t)
// Test data
ctx := context.Background()
dbName := "testDB"
username := "testUser"
// Expectations
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
ctx, "app_arango_stats", gomock.Any(), "endpoint",
gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Expect user() call and return our mock user that implements the full interface
mockArango.EXPECT().User(gomock.Any(), username).Return(mockUser, nil).MaxTimes(2)
// Test cases
testCases := []struct {
name string
dbName string
username string
permission string
expectErr bool
}{
{
name: "Valid grant read-write",
dbName: dbName,
username: username,
permission: string(arangodb.GrantReadWrite),
expectErr: false,
},
{
name: "Valid grant read-only",
dbName: dbName,
username: username,
permission: string(arangodb.GrantReadOnly),
expectErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := client.grantDB(ctx, tc.dbName, tc.username, tc.permission)
if tc.expectErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func Test_Client_GrantDB_Errors(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
ctx := context.Background()
dbName := "testDB"
username := "testUser"
// Expect user() call to return error
mockArango.EXPECT().User(gomock.Any(), username).Return(nil, errUserNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(ctx, "app_arango_stats", gomock.Any(), "endpoint",
gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := client.grantDB(ctx, dbName, username, string(arangodb.GrantReadWrite))
require.Error(t, err)
}
func TestUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockArango := NewMockClient(ctrl)
mockUser := NewMockUser(ctrl)
client := &Client{client: mockArango}
ctx := context.Background()
username := "testUser"
t.Run("Successful user fetch", func(t *testing.T) {
mockArango.EXPECT().
User(ctx, username).
Return(mockUser, nil)
user, err := client.user(ctx, username)
require.NoError(t, err)
require.NotNil(t, user)
})
t.Run("user fetch error", func(t *testing.T) {
mockArango.EXPECT().
User(ctx, username).
Return(nil, errUserNotFound)
user, err := client.user(ctx, username)
require.Error(t, err)
require.Nil(t, user)
})
}
func TestClient_Database(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockArango := NewMockClient(ctrl)
config := Config{Host: "localhost", Port: 8527, User: "root", Password: "root"}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-arangodb"))
client.client = mockArango
mockDatabase := NewMockDatabase(gomock.NewController(t))
ctx := context.Background()
dbName := "testDB"
t.Run("Get database Success", func(t *testing.T) {
mockArango.EXPECT().
GetDatabase(ctx, dbName, nil).
Return(mockDatabase, nil)
mockDatabase.EXPECT().Name().Return(dbName)
db, err := client.database(ctx, dbName)
require.NoError(t, err)
require.NotNil(t, db)
require.Equal(t, dbName, db.Name())
})
t.Run("Get database Error", func(t *testing.T) {
mockArango.EXPECT().
GetDatabase(ctx, dbName, nil).
Return(nil, errDBNotFound)
db, err := client.database(ctx, dbName)
require.Error(t, err)
require.Nil(t, db)
})
// Test database operations
t.Run("database Operations", func(t *testing.T) {
mockArango.EXPECT().
GetDatabase(ctx, dbName, nil).
Return(mockDatabase, nil)
mockDatabase.EXPECT().Name().Return(dbName)
mockDatabase.EXPECT().Remove(ctx).Return(nil)
mockDatabase.EXPECT().GetCollection(ctx, "testCollection", nil).
Return(nil, nil)
db, err := client.database(ctx, dbName)
require.NoError(t, err)
require.Equal(t, dbName, db.Name())
err = db.Remove(ctx)
require.NoError(t, err)
coll, err := db.GetCollection(ctx, "testCollection", nil)
require.NoError(t, err)
require.Nil(t, coll)
})
}
func Test_Client_GrantCollection(t *testing.T) {
client, mockArango, mockUser, mockLogger, mockMetrics := setupDB(t)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
context.Background(), "app_arango_stats", gomock.Any(), "endpoint",
gomock.Any(), gomock.Any(), gomock.Any())
mockArango.EXPECT().User(gomock.Any(), "testUser").Return(mockUser, nil)
err := client.grantCollection(context.Background(), "testDB", "testCollection",
"testUser", string(arangodb.GrantReadOnly))
require.NoError(t, err)
}
func Test_Client_GrantCollection_Error(t *testing.T) {
client, mockArango, _, mockLogger, mockMetrics := setupDB(t)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
context.Background(), "app_arango_stats", gomock.Any(), "endpoint",
gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockArango.EXPECT().User(gomock.Any(), "testUser").Return(nil, errUserNotFound)
err := client.grantCollection(context.Background(), "testDB", "testCollection",
"testUser", string(arangodb.GrantReadOnly))
require.ErrorIs(t, errUserNotFound, err, "Expected error when user not found")
}
================================================
FILE: pkg/gofr/datasource/arangodb/arango_test.go
================================================
package arangodb
import (
"context"
"errors"
"testing"
"github.com/arangodb/go-driver/v2/arangodb"
"github.com/arangodb/go-driver/v2/arangodb/shared"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
var (
errUserNotFound = errors.New("user not found")
errDBNotFound = errors.New("database not found")
errDocumentNotFound = errors.New("document not found")
)
func setupDB(t *testing.T) (*Client, *MockClient, *MockUser, *MockLogger, *MockMetrics) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Setup
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockArango := NewMockClient(ctrl)
mockUser := NewMockUser(ctrl)
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
config := Config{Host: "localhost", Port: 8527, User: "root", Password: "root"}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-arangodb"))
client.client = mockArango
return client, mockArango, mockUser, mockLogger, mockMetrics
}
func Test_NewArangoClient_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
logger.EXPECT().Errorf("failed to verify connection: %v", gomock.Any())
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
client := New(Config{Host: "localhost", Port: 8529, Password: "root", User: "admin"})
client.UseLogger(logger)
client.UseMetrics(metrics)
client.Connect()
require.NotNil(t, client)
}
func TestClient_Query_Success(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
dbName := "testDB"
query := "FOR doc IN collection RETURN doc"
bindVars := map[string]any{"key": "value"}
var result []map[string]any
expectedResult := []map[string]any{
{"_key": "doc1", "value": "test1"},
{"_key": "doc2", "value": "test2"},
}
test.MockArango.EXPECT().GetDatabase(test.Ctx, dbName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().Query(test.Ctx, query, &arangodb.QueryOptions{BindVars: bindVars}).
Return(NewMockQueryCursor(test.Ctrl, expectedResult), nil)
err := test.Client.Query(test.Ctx, dbName, query, bindVars, &result)
require.NoError(t, err)
require.Equal(t, expectedResult, result)
}
func TestValidateConfig(t *testing.T) {
testCases := []struct {
name string
config Config
expectErr bool
errMsg string
}{
{
name: "Valid config",
config: Config{
Host: "localhost",
Port: 8529,
User: "root",
Password: "password",
},
expectErr: false,
},
{
name: "Empty host",
config: Config{
Port: 8529,
User: "root",
Password: "password",
},
expectErr: true,
errMsg: "missing required field in config: host is empty",
},
{
name: "Empty port",
config: Config{
Host: "localhost",
User: "root",
Password: "password",
},
expectErr: true,
errMsg: "missing required field in config: port is empty",
},
{
name: "Empty user",
config: Config{
Host: "localhost",
Port: 8529,
Password: "password",
},
expectErr: true,
errMsg: "missing required field in config: user is empty",
},
{
name: "Empty password",
config: Config{
Host: "localhost",
Port: 8529,
User: "root",
},
expectErr: true,
errMsg: "missing required field in config: password is empty",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := &Client{config: &tc.config}
err := client.validateConfig()
if tc.expectErr {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errMsg)
} else {
require.NoError(t, err)
}
})
}
}
func TestClient_HealthCheck_Success(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
expectedVersion := arangodb.VersionInfo{
Version: "3.9.0",
Server: "arango",
}
test.MockArango.EXPECT().Version(test.Ctx).Return(expectedVersion, nil)
health, err := test.Client.HealthCheck(test.Ctx)
require.NoError(t, err)
h, ok := health.(*Health)
require.True(t, ok)
require.Equal(t, "UP", h.Status)
require.Equal(t, test.Client.endpoint, h.Details["endpoint"])
require.Equal(t, expectedVersion.Version, h.Details["version"])
require.Equal(t, expectedVersion.Server, h.Details["server"])
}
func TestClient_HealthCheck_Error(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
test.MockArango.EXPECT().Version(test.Ctx).Return(arangodb.VersionInfo{}, errStatusDown)
health, err := test.Client.HealthCheck(test.Ctx)
require.Error(t, err)
require.Equal(t, errStatusDown, err)
h, ok := health.(*Health)
require.True(t, ok)
require.Equal(t, "DOWN", h.Status)
require.Equal(t, test.Client.endpoint, h.Details["endpoint"])
}
type MockQueryCursor struct {
ctrl *gomock.Controller
data []map[string]any
idx int
}
func NewMockQueryCursor(ctrl *gomock.Controller, data []map[string]any) *MockQueryCursor {
return &MockQueryCursor{
ctrl: ctrl,
data: data,
idx: 0,
}
}
func (*MockQueryCursor) Close() error {
return nil
}
func (*MockQueryCursor) CloseWithContext(_ context.Context) error {
return nil
}
func (m *MockQueryCursor) HasMore() bool {
return m.idx < len(m.data)
}
func (m *MockQueryCursor) ReadDocument(_ context.Context, document any) (arangodb.DocumentMeta, error) {
if m.idx >= len(m.data) {
return arangodb.DocumentMeta{}, shared.NoMoreDocumentsError{}
}
doc, ok := document.(*map[string]any)
if !ok {
return arangodb.DocumentMeta{}, errInvalidEdgeDocumentType
}
*doc = m.data[m.idx]
meta := arangodb.DocumentMeta{}
m.idx++
return meta, nil
}
func (m *MockQueryCursor) Count() int64 {
return int64(len(m.data))
}
func (*MockQueryCursor) Statistics() arangodb.CursorStats {
return arangodb.CursorStats{}
}
func (*MockQueryCursor) Plan() arangodb.CursorPlan {
return arangodb.CursorPlan{}
}
func TestClient_Query_WithBatchSizeAndFullCount(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
dbName := "testDB"
query := "FOR doc IN collection RETURN doc"
bindVars := map[string]any{"key": "value"}
var result []map[string]any
expectedResult := []map[string]any{
{"_key": "doc1", "value": "v1"},
{"_key": "doc2", "value": "v2"},
}
// Define QueryOptions with batchSize and fullCount
queryOpts := map[string]any{
"batchSize": 50,
"options": map[string]any{
"fullCount": true,
},
}
test.MockArango.EXPECT().GetDatabase(test.Ctx, dbName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().
Query(test.Ctx, query, gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, opts *arangodb.QueryOptions) (arangodb.Cursor, error) {
require.NotNil(t, opts)
require.Equal(t, 50, opts.BatchSize)
require.True(t, opts.Options.FullCount)
require.Equal(t, bindVars, opts.BindVars)
return NewMockQueryCursor(test.Ctrl, expectedResult), nil
})
err := test.Client.Query(test.Ctx, dbName, query, bindVars, &result, queryOpts)
require.NoError(t, err)
require.Equal(t, expectedResult, result)
}
func TestClient_Query_WithMaxPlans(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
dbName := "testDB"
query := "FOR doc IN collection RETURN doc"
bindVars := map[string]any{"key": "value"}
var result []map[string]any
expectedResult := []map[string]any{
{"_key": "doc1", "value": "v1"},
}
// Define QueryOptions with maxPlans sub-option
queryOpts := map[string]any{
"options": map[string]any{
"maxPlans": 5,
},
}
test.MockArango.EXPECT().GetDatabase(test.Ctx, dbName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().
Query(test.Ctx, query, gomock.Any()).
DoAndReturn(func(_ context.Context, _ string, opts *arangodb.QueryOptions) (arangodb.Cursor, error) {
require.NotNil(t, opts)
require.Equal(t, 5, opts.Options.MaxPlans)
return NewMockQueryCursor(test.Ctrl, expectedResult), nil
})
err := test.Client.Query(test.Ctx, dbName, query, bindVars, &result, queryOpts)
require.NoError(t, err)
require.Equal(t, expectedResult, result)
}
func TestClient_Query_InvalidResultType(t *testing.T) {
test := setupGraphTest(t)
defer test.Ctrl.Finish()
dbName := "testDB"
query := "FOR doc IN collection RETURN doc"
bindVars := map[string]any{"key": "value"}
var result int // Incorrect type
test.MockArango.EXPECT().GetDatabase(test.Ctx, dbName, nil).
Return(test.MockDB, nil)
test.MockDB.EXPECT().Query(test.Ctx, query, gomock.Any()).Return(NewMockQueryCursor(test.Ctrl, nil), nil)
err := test.Client.Query(test.Ctx, dbName, query, bindVars, &result)
require.Error(t, err)
require.Equal(t, errInvalidResultType, err)
}
================================================
FILE: pkg/gofr/datasource/arangodb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/arangodb
go 1.25.0
require (
github.com/arangodb/go-driver/v2 v2.1.6
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dchest/siphash v1.2.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kkdai/maglev v0.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/net v0.46.0 // indirect
golang.org/x/sys v0.37.0 // indirect
golang.org/x/text v0.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/arangodb/go.sum
================================================
github.com/arangodb/go-driver/v2 v2.1.6 h1:TwZKYwQZzDStaEAjP3vnnnhVbe9691coMS92F0HfIQ8=
github.com/arangodb/go-driver/v2 v2.1.6/go.mod h1:7iQ62d9iqIeSOgj12e86zN+LifSCCFhlCpsJ7dMC3Uw=
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e h1:Xg+hGrY2LcQBbxd0ZFdbGSyRKTYMZCfBbw/pMJFOk1g=
github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e/go.mod h1:mq7Shfa/CaixoDxiyAAc5jZ6CVBAyPaNQCGS7mkj4Ho=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
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/dchest/siphash v1.2.2/go.mod h1:q+IRvb2gOSrUnYoPqHiyHXS0FOBBOdl6tONBlVnOnt4=
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/kkdai/maglev v0.2.0 h1:w6DCW0kAA6fstZqXkrBrlgIC3jeIRXkjOYea/m6EK/Y=
github.com/kkdai/maglev v0.2.0/go.mod h1:d+mt8Lmt3uqi9aRb/BnPjzD0fy+ETs1vVXiGRnqHVZ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
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/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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/arangodb/interface.go
================================================
package arangodb
import (
"context"
"github.com/arangodb/go-driver/v2/arangodb"
)
type ArangoDB interface {
Connect()
user(ctx context.Context, username string) (arangodb.User, error)
database(ctx context.Context, name string) (arangodb.Database, error)
databases(ctx context.Context) ([]arangodb.Database, error)
version(ctx context.Context) (arangodb.VersionInfo, error)
// CreateDB creates a new database in ArangoDB.
CreateDB(ctx context.Context, database string) error
// DropDB deletes an existing database in ArangoDB.
DropDB(ctx context.Context, database string) error
// CreateCollection creates a new collection in a database with specified type.
CreateCollection(ctx context.Context, database, collection string, isEdge bool) error
// DropCollection deletes an existing collection from a database.
DropCollection(ctx context.Context, database, collection string) error
// CreateGraph creates a new graph in a database.
CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error
// DropGraph deletes an existing graph from a database.
DropGraph(ctx context.Context, database, graph string) error
CreateDocument(ctx context.Context, dbName, collectionName string, document any) (string, error)
GetDocument(ctx context.Context, dbName, collectionName, documentID string, result any) error
UpdateDocument(ctx context.Context, dbName, collectionName, documentID string, document any) error
DeleteDocument(ctx context.Context, dbName, collectionName, documentID string) error
// GetEdges retrieves all the edge documents connected to a specific vertex in an ArangoDB graph.
GetEdges(ctx context.Context, dbName, graphName, edgeCollection, vertexID string, resp any) error
// Query operations
Query(ctx context.Context, dbName string, query string, bindVars map[string]any, result any) error
HealthCheck(ctx context.Context) (any, error)
}
================================================
FILE: pkg/gofr/datasource/arangodb/logger.go
================================================
package arangodb
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
type QueryLog struct {
Query string `json:"query"`
Duration int64 `json:"duration"`
Database string `json:"database,omitempty"`
Collection string `json:"collection,omitempty"`
Filter any `json:"filter,omitempty"`
ID any `json:"id,omitempty"`
Operation string `json:"operation,omitempty"`
}
// PrettyPrint formats the QueryLog for output.
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
if ql.Filter == nil {
ql.Filter = ""
}
if ql.ID == nil {
ql.ID = ""
}
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s\n",
clean(ql.Operation), "ARANGODB", ql.Duration,
clean(strings.Join([]string{ql.Database, ql.Collection, fmt.Sprint(ql.Filter), fmt.Sprint(ql.ID)}, " ")), clean(ql.Query))
}
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
return strings.TrimSpace(query)
}
================================================
FILE: pkg/gofr/datasource/arangodb/logger_test.go
================================================
package arangodb
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := QueryLog{
Query: "",
Duration: 12345,
Database: "test",
Collection: "test",
Filter: true,
ID: "12345",
Operation: "getDocument",
}
expected := "getDocument"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
================================================
FILE: pkg/gofr/datasource/arangodb/metrics.go
================================================
package arangodb
import "context"
// Metrics defines the interface for capturing metrics.
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_collection.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/collection.go
//
// Generated by this command:
//
// mockgen -source=/Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/collection.go -destination=mock_collection.go -package=arangodb
//
// Package arangodb is a generated GoMock package.
package arangodb
import (
context "context"
reflect "reflect"
arangodb "github.com/arangodb/go-driver/v2/arangodb"
gomock "go.uber.org/mock/gomock"
)
// MockCollection is a mock of Collection interface.
type MockCollection struct {
ctrl *gomock.Controller
recorder *MockCollectionMockRecorder
isgomock struct{}
}
// MockCollectionMockRecorder is the mock recorder for MockCollection.
type MockCollectionMockRecorder struct {
mock *MockCollection
}
// NewMockCollection creates a new mock instance.
func NewMockCollection(ctrl *gomock.Controller) *MockCollection {
mock := &MockCollection{ctrl: ctrl}
mock.recorder = &MockCollectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCollection) EXPECT() *MockCollectionMockRecorder {
return m.recorder
}
// Checksum mocks base method.
func (m *MockCollection) Checksum(ctx context.Context, withRevisions, withData *bool) (arangodb.CollectionChecksum, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Checksum", ctx, withRevisions, withData)
ret0, _ := ret[0].(arangodb.CollectionChecksum)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Checksum indicates an expected call of Checksum.
func (mr *MockCollectionMockRecorder) Checksum(ctx, withRevisions, withData any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Checksum", reflect.TypeOf((*MockCollection)(nil).Checksum), ctx, withRevisions, withData)
}
// Compact mocks base method.
func (m *MockCollection) Compact(ctx context.Context) (arangodb.CollectionInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Compact", ctx)
ret0, _ := ret[0].(arangodb.CollectionInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Compact indicates an expected call of Compact.
func (mr *MockCollectionMockRecorder) Compact(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Compact", reflect.TypeOf((*MockCollection)(nil).Compact), ctx)
}
// Count mocks base method.
func (m *MockCollection) Count(ctx context.Context) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Count", ctx)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Count indicates an expected call of Count.
func (mr *MockCollectionMockRecorder) Count(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Count", reflect.TypeOf((*MockCollection)(nil).Count), ctx)
}
// CreateDocument mocks base method.
func (m *MockCollection) CreateDocument(ctx context.Context, document any) (arangodb.CollectionDocumentCreateResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocument", ctx, document)
ret0, _ := ret[0].(arangodb.CollectionDocumentCreateResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocument indicates an expected call of CreateDocument.
func (mr *MockCollectionMockRecorder) CreateDocument(ctx, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocument", reflect.TypeOf((*MockCollection)(nil).CreateDocument), ctx, document)
}
// CreateDocumentWithOptions mocks base method.
func (m *MockCollection) CreateDocumentWithOptions(ctx context.Context, document any, options *arangodb.CollectionDocumentCreateOptions) (arangodb.CollectionDocumentCreateResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocumentWithOptions", ctx, document, options)
ret0, _ := ret[0].(arangodb.CollectionDocumentCreateResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocumentWithOptions indicates an expected call of CreateDocumentWithOptions.
func (mr *MockCollectionMockRecorder) CreateDocumentWithOptions(ctx, document, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocumentWithOptions", reflect.TypeOf((*MockCollection)(nil).CreateDocumentWithOptions), ctx, document, options)
}
// CreateDocuments mocks base method.
func (m *MockCollection) CreateDocuments(ctx context.Context, documents any) (arangodb.CollectionDocumentCreateResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocuments", ctx, documents)
ret0, _ := ret[0].(arangodb.CollectionDocumentCreateResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocuments indicates an expected call of CreateDocuments.
func (mr *MockCollectionMockRecorder) CreateDocuments(ctx, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocuments", reflect.TypeOf((*MockCollection)(nil).CreateDocuments), ctx, documents)
}
// CreateDocumentsWithOptions mocks base method.
func (m *MockCollection) CreateDocumentsWithOptions(ctx context.Context, documents any, opts *arangodb.CollectionDocumentCreateOptions) (arangodb.CollectionDocumentCreateResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDocumentsWithOptions", ctx, documents, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentCreateResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDocumentsWithOptions indicates an expected call of CreateDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) CreateDocumentsWithOptions(ctx, documents, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).CreateDocumentsWithOptions), ctx, documents, opts)
}
// Database mocks base method.
func (m *MockCollection) Database() arangodb.Database {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Database")
ret0, _ := ret[0].(arangodb.Database)
return ret0
}
// Database indicates an expected call of Database.
func (mr *MockCollectionMockRecorder) Database() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Database", reflect.TypeOf((*MockCollection)(nil).Database))
}
// DeleteDocument mocks base method.
func (m *MockCollection) DeleteDocument(ctx context.Context, key string) (arangodb.CollectionDocumentDeleteResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, key)
ret0, _ := ret[0].(arangodb.CollectionDocumentDeleteResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockCollectionMockRecorder) DeleteDocument(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockCollection)(nil).DeleteDocument), ctx, key)
}
// DeleteDocumentWithOptions mocks base method.
func (m *MockCollection) DeleteDocumentWithOptions(ctx context.Context, key string, opts *arangodb.CollectionDocumentDeleteOptions) (arangodb.CollectionDocumentDeleteResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocumentWithOptions", ctx, key, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentDeleteResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteDocumentWithOptions indicates an expected call of DeleteDocumentWithOptions.
func (mr *MockCollectionMockRecorder) DeleteDocumentWithOptions(ctx, key, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocumentWithOptions", reflect.TypeOf((*MockCollection)(nil).DeleteDocumentWithOptions), ctx, key, opts)
}
// DeleteDocuments mocks base method.
func (m *MockCollection) DeleteDocuments(ctx context.Context, keys []string) (arangodb.CollectionDocumentDeleteResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocuments", ctx, keys)
ret0, _ := ret[0].(arangodb.CollectionDocumentDeleteResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteDocuments indicates an expected call of DeleteDocuments.
func (mr *MockCollectionMockRecorder) DeleteDocuments(ctx, keys any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocuments", reflect.TypeOf((*MockCollection)(nil).DeleteDocuments), ctx, keys)
}
// DeleteDocumentsWithOptions mocks base method.
func (m *MockCollection) DeleteDocumentsWithOptions(ctx context.Context, documents any, opts *arangodb.CollectionDocumentDeleteOptions) (arangodb.CollectionDocumentDeleteResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocumentsWithOptions", ctx, documents, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentDeleteResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteDocumentsWithOptions indicates an expected call of DeleteDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) DeleteDocumentsWithOptions(ctx, documents, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).DeleteDocumentsWithOptions), ctx, documents, opts)
}
// DeleteIndex mocks base method.
func (m *MockCollection) DeleteIndex(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIndex", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIndex indicates an expected call of DeleteIndex.
func (mr *MockCollectionMockRecorder) DeleteIndex(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndex", reflect.TypeOf((*MockCollection)(nil).DeleteIndex), ctx, name)
}
// DeleteIndexByID mocks base method.
func (m *MockCollection) DeleteIndexByID(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIndexByID", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIndexByID indicates an expected call of DeleteIndexByID.
func (mr *MockCollectionMockRecorder) DeleteIndexByID(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndexByID", reflect.TypeOf((*MockCollection)(nil).DeleteIndexByID), ctx, id)
}
// DocumentExists mocks base method.
func (m *MockCollection) DocumentExists(ctx context.Context, key string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DocumentExists", ctx, key)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DocumentExists indicates an expected call of DocumentExists.
func (mr *MockCollectionMockRecorder) DocumentExists(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DocumentExists", reflect.TypeOf((*MockCollection)(nil).DocumentExists), ctx, key)
}
// EnsureGeoIndex mocks base method.
func (m *MockCollection) EnsureGeoIndex(ctx context.Context, fields []string, options *arangodb.CreateGeoIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureGeoIndex", ctx, fields, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureGeoIndex indicates an expected call of EnsureGeoIndex.
func (mr *MockCollectionMockRecorder) EnsureGeoIndex(ctx, fields, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureGeoIndex", reflect.TypeOf((*MockCollection)(nil).EnsureGeoIndex), ctx, fields, options)
}
// EnsureInvertedIndex mocks base method.
func (m *MockCollection) EnsureInvertedIndex(ctx context.Context, options *arangodb.InvertedIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureInvertedIndex", ctx, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureInvertedIndex indicates an expected call of EnsureInvertedIndex.
func (mr *MockCollectionMockRecorder) EnsureInvertedIndex(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureInvertedIndex", reflect.TypeOf((*MockCollection)(nil).EnsureInvertedIndex), ctx, options)
}
// EnsureMDIIndex mocks base method.
func (m *MockCollection) EnsureMDIIndex(ctx context.Context, fields []string, options *arangodb.CreateMDIIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureMDIIndex", ctx, fields, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureMDIIndex indicates an expected call of EnsureMDIIndex.
func (mr *MockCollectionMockRecorder) EnsureMDIIndex(ctx, fields, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureMDIIndex", reflect.TypeOf((*MockCollection)(nil).EnsureMDIIndex), ctx, fields, options)
}
// EnsureMDIPrefixedIndex mocks base method.
func (m *MockCollection) EnsureMDIPrefixedIndex(ctx context.Context, fields []string, options *arangodb.CreateMDIPrefixedIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureMDIPrefixedIndex", ctx, fields, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureMDIPrefixedIndex indicates an expected call of EnsureMDIPrefixedIndex.
func (mr *MockCollectionMockRecorder) EnsureMDIPrefixedIndex(ctx, fields, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureMDIPrefixedIndex", reflect.TypeOf((*MockCollection)(nil).EnsureMDIPrefixedIndex), ctx, fields, options)
}
// EnsurePersistentIndex mocks base method.
func (m *MockCollection) EnsurePersistentIndex(ctx context.Context, fields []string, options *arangodb.CreatePersistentIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsurePersistentIndex", ctx, fields, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsurePersistentIndex indicates an expected call of EnsurePersistentIndex.
func (mr *MockCollectionMockRecorder) EnsurePersistentIndex(ctx, fields, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsurePersistentIndex", reflect.TypeOf((*MockCollection)(nil).EnsurePersistentIndex), ctx, fields, options)
}
// EnsureTTLIndex mocks base method.
func (m *MockCollection) EnsureTTLIndex(ctx context.Context, fields []string, expireAfter int, options *arangodb.CreateTTLIndexOptions) (arangodb.IndexResponse, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureTTLIndex", ctx, fields, expireAfter, options)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureTTLIndex indicates an expected call of EnsureTTLIndex.
func (mr *MockCollectionMockRecorder) EnsureTTLIndex(ctx, fields, expireAfter, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureTTLIndex", reflect.TypeOf((*MockCollection)(nil).EnsureTTLIndex), ctx, fields, expireAfter, options)
}
// ImportDocuments mocks base method.
func (m *MockCollection) ImportDocuments(ctx context.Context, documents string, documentsType arangodb.CollectionDocumentImportDocumentType) (arangodb.CollectionDocumentImportResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImportDocuments", ctx, documents, documentsType)
ret0, _ := ret[0].(arangodb.CollectionDocumentImportResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImportDocuments indicates an expected call of ImportDocuments.
func (mr *MockCollectionMockRecorder) ImportDocuments(ctx, documents, documentsType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportDocuments", reflect.TypeOf((*MockCollection)(nil).ImportDocuments), ctx, documents, documentsType)
}
// ImportDocumentsWithOptions mocks base method.
func (m *MockCollection) ImportDocumentsWithOptions(ctx context.Context, documents string, documentsType arangodb.CollectionDocumentImportDocumentType, options *arangodb.CollectionDocumentImportOptions) (arangodb.CollectionDocumentImportResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ImportDocumentsWithOptions", ctx, documents, documentsType, options)
ret0, _ := ret[0].(arangodb.CollectionDocumentImportResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ImportDocumentsWithOptions indicates an expected call of ImportDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) ImportDocumentsWithOptions(ctx, documents, documentsType, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).ImportDocumentsWithOptions), ctx, documents, documentsType, options)
}
// Index mocks base method.
func (m *MockCollection) Index(ctx context.Context, name string) (arangodb.IndexResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Index", ctx, name)
ret0, _ := ret[0].(arangodb.IndexResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Index indicates an expected call of Index.
func (mr *MockCollectionMockRecorder) Index(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Index", reflect.TypeOf((*MockCollection)(nil).Index), ctx, name)
}
// IndexExists mocks base method.
func (m *MockCollection) IndexExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IndexExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IndexExists indicates an expected call of IndexExists.
func (mr *MockCollectionMockRecorder) IndexExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexExists", reflect.TypeOf((*MockCollection)(nil).IndexExists), ctx, name)
}
// Indexes mocks base method.
func (m *MockCollection) Indexes(ctx context.Context) ([]arangodb.IndexResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Indexes", ctx)
ret0, _ := ret[0].([]arangodb.IndexResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Indexes indicates an expected call of Indexes.
func (mr *MockCollectionMockRecorder) Indexes(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Indexes", reflect.TypeOf((*MockCollection)(nil).Indexes), ctx)
}
// LoadIndexesIntoMemory mocks base method.
func (m *MockCollection) LoadIndexesIntoMemory(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoadIndexesIntoMemory", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoadIndexesIntoMemory indicates an expected call of LoadIndexesIntoMemory.
func (mr *MockCollectionMockRecorder) LoadIndexesIntoMemory(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadIndexesIntoMemory", reflect.TypeOf((*MockCollection)(nil).LoadIndexesIntoMemory), ctx)
}
// Name mocks base method.
func (m *MockCollection) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockCollectionMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockCollection)(nil).Name))
}
// Properties mocks base method.
func (m *MockCollection) Properties(ctx context.Context) (arangodb.CollectionProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Properties", ctx)
ret0, _ := ret[0].(arangodb.CollectionProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Properties indicates an expected call of Properties.
func (mr *MockCollectionMockRecorder) Properties(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Properties", reflect.TypeOf((*MockCollection)(nil).Properties), ctx)
}
// ReadDocument mocks base method.
func (m *MockCollection) ReadDocument(ctx context.Context, key string, result any) (arangodb.DocumentMeta, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDocument", ctx, key, result)
ret0, _ := ret[0].(arangodb.DocumentMeta)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDocument indicates an expected call of ReadDocument.
func (mr *MockCollectionMockRecorder) ReadDocument(ctx, key, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDocument", reflect.TypeOf((*MockCollection)(nil).ReadDocument), ctx, key, result)
}
// ReadDocumentWithOptions mocks base method.
func (m *MockCollection) ReadDocumentWithOptions(ctx context.Context, key string, result any, opts *arangodb.CollectionDocumentReadOptions) (arangodb.DocumentMeta, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDocumentWithOptions", ctx, key, result, opts)
ret0, _ := ret[0].(arangodb.DocumentMeta)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDocumentWithOptions indicates an expected call of ReadDocumentWithOptions.
func (mr *MockCollectionMockRecorder) ReadDocumentWithOptions(ctx, key, result, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDocumentWithOptions", reflect.TypeOf((*MockCollection)(nil).ReadDocumentWithOptions), ctx, key, result, opts)
}
// ReadDocuments mocks base method.
func (m *MockCollection) ReadDocuments(ctx context.Context, keys []string) (arangodb.CollectionDocumentReadResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDocuments", ctx, keys)
ret0, _ := ret[0].(arangodb.CollectionDocumentReadResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDocuments indicates an expected call of ReadDocuments.
func (mr *MockCollectionMockRecorder) ReadDocuments(ctx, keys any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDocuments", reflect.TypeOf((*MockCollection)(nil).ReadDocuments), ctx, keys)
}
// ReadDocumentsWithOptions mocks base method.
func (m *MockCollection) ReadDocumentsWithOptions(ctx context.Context, documents any, opts *arangodb.CollectionDocumentReadOptions) (arangodb.CollectionDocumentReadResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDocumentsWithOptions", ctx, documents, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentReadResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDocumentsWithOptions indicates an expected call of ReadDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) ReadDocumentsWithOptions(ctx, documents, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).ReadDocumentsWithOptions), ctx, documents, opts)
}
// RecalculateCount mocks base method.
func (m *MockCollection) RecalculateCount(ctx context.Context) (bool, *int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RecalculateCount", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(*int64)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// RecalculateCount indicates an expected call of RecalculateCount.
func (mr *MockCollectionMockRecorder) RecalculateCount(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecalculateCount", reflect.TypeOf((*MockCollection)(nil).RecalculateCount), ctx)
}
// Remove mocks base method.
func (m *MockCollection) Remove(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockCollectionMockRecorder) Remove(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCollection)(nil).Remove), ctx)
}
// RemoveWithOptions mocks base method.
func (m *MockCollection) RemoveWithOptions(ctx context.Context, opts *arangodb.RemoveCollectionOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveWithOptions", ctx, opts)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveWithOptions indicates an expected call of RemoveWithOptions.
func (mr *MockCollectionMockRecorder) RemoveWithOptions(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveWithOptions", reflect.TypeOf((*MockCollection)(nil).RemoveWithOptions), ctx, opts)
}
// Rename mocks base method.
func (m *MockCollection) Rename(ctx context.Context, req arangodb.RenameCollectionRequest) (arangodb.CollectionInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", ctx, req)
ret0, _ := ret[0].(arangodb.CollectionInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Rename indicates an expected call of Rename.
func (mr *MockCollectionMockRecorder) Rename(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockCollection)(nil).Rename), ctx, req)
}
// ReplaceDocument mocks base method.
func (m *MockCollection) ReplaceDocument(ctx context.Context, key string, document any) (arangodb.CollectionDocumentReplaceResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceDocument", ctx, key, document)
ret0, _ := ret[0].(arangodb.CollectionDocumentReplaceResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceDocument indicates an expected call of ReplaceDocument.
func (mr *MockCollectionMockRecorder) ReplaceDocument(ctx, key, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceDocument", reflect.TypeOf((*MockCollection)(nil).ReplaceDocument), ctx, key, document)
}
// ReplaceDocumentWithOptions mocks base method.
func (m *MockCollection) ReplaceDocumentWithOptions(ctx context.Context, key string, document any, options *arangodb.CollectionDocumentReplaceOptions) (arangodb.CollectionDocumentReplaceResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceDocumentWithOptions", ctx, key, document, options)
ret0, _ := ret[0].(arangodb.CollectionDocumentReplaceResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceDocumentWithOptions indicates an expected call of ReplaceDocumentWithOptions.
func (mr *MockCollectionMockRecorder) ReplaceDocumentWithOptions(ctx, key, document, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceDocumentWithOptions", reflect.TypeOf((*MockCollection)(nil).ReplaceDocumentWithOptions), ctx, key, document, options)
}
// ReplaceDocuments mocks base method.
func (m *MockCollection) ReplaceDocuments(ctx context.Context, documents any) (arangodb.CollectionDocumentReplaceResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceDocuments", ctx, documents)
ret0, _ := ret[0].(arangodb.CollectionDocumentReplaceResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceDocuments indicates an expected call of ReplaceDocuments.
func (mr *MockCollectionMockRecorder) ReplaceDocuments(ctx, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceDocuments", reflect.TypeOf((*MockCollection)(nil).ReplaceDocuments), ctx, documents)
}
// ReplaceDocumentsWithOptions mocks base method.
func (m *MockCollection) ReplaceDocumentsWithOptions(ctx context.Context, documents any, opts *arangodb.CollectionDocumentReplaceOptions) (arangodb.CollectionDocumentReplaceResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceDocumentsWithOptions", ctx, documents, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentReplaceResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceDocumentsWithOptions indicates an expected call of ReplaceDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) ReplaceDocumentsWithOptions(ctx, documents, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).ReplaceDocumentsWithOptions), ctx, documents, opts)
}
// ResponsibleShard mocks base method.
func (m *MockCollection) ResponsibleShard(ctx context.Context, options map[string]any) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResponsibleShard", ctx, options)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResponsibleShard indicates an expected call of ResponsibleShard.
func (mr *MockCollectionMockRecorder) ResponsibleShard(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResponsibleShard", reflect.TypeOf((*MockCollection)(nil).ResponsibleShard), ctx, options)
}
// Revision mocks base method.
func (m *MockCollection) Revision(ctx context.Context) (arangodb.CollectionProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Revision", ctx)
ret0, _ := ret[0].(arangodb.CollectionProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Revision indicates an expected call of Revision.
func (mr *MockCollectionMockRecorder) Revision(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Revision", reflect.TypeOf((*MockCollection)(nil).Revision), ctx)
}
// SetPropertiesV2 mocks base method.
func (m *MockCollection) SetPropertiesV2(ctx context.Context, options arangodb.SetCollectionPropertiesOptionsV2) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetPropertiesV2", ctx, options)
ret0, _ := ret[0].(error)
return ret0
}
// SetPropertiesV2 indicates an expected call of SetPropertiesV2.
func (mr *MockCollectionMockRecorder) SetPropertiesV2(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPropertiesV2", reflect.TypeOf((*MockCollection)(nil).SetPropertiesV2), ctx, options)
}
// Shards mocks base method.
func (m *MockCollection) Shards(ctx context.Context, details bool) (arangodb.CollectionShards, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Shards", ctx, details)
ret0, _ := ret[0].(arangodb.CollectionShards)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Shards indicates an expected call of Shards.
func (mr *MockCollectionMockRecorder) Shards(ctx, details any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Shards", reflect.TypeOf((*MockCollection)(nil).Shards), ctx, details)
}
// Statistics mocks base method.
func (m *MockCollection) Statistics(ctx context.Context, details bool) (arangodb.CollectionFigures, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Statistics", ctx, details)
ret0, _ := ret[0].(arangodb.CollectionFigures)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Statistics indicates an expected call of Statistics.
func (mr *MockCollectionMockRecorder) Statistics(ctx, details any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Statistics", reflect.TypeOf((*MockCollection)(nil).Statistics), ctx, details)
}
// Truncate mocks base method.
func (m *MockCollection) Truncate(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Truncate", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Truncate indicates an expected call of Truncate.
func (mr *MockCollectionMockRecorder) Truncate(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Truncate", reflect.TypeOf((*MockCollection)(nil).Truncate), ctx)
}
// UpdateDocument mocks base method.
func (m *MockCollection) UpdateDocument(ctx context.Context, key string, document any) (arangodb.CollectionDocumentUpdateResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, key, document)
ret0, _ := ret[0].(arangodb.CollectionDocumentUpdateResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockCollectionMockRecorder) UpdateDocument(ctx, key, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockCollection)(nil).UpdateDocument), ctx, key, document)
}
// UpdateDocumentWithOptions mocks base method.
func (m *MockCollection) UpdateDocumentWithOptions(ctx context.Context, key string, document any, options *arangodb.CollectionDocumentUpdateOptions) (arangodb.CollectionDocumentUpdateResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocumentWithOptions", ctx, key, document, options)
ret0, _ := ret[0].(arangodb.CollectionDocumentUpdateResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDocumentWithOptions indicates an expected call of UpdateDocumentWithOptions.
func (mr *MockCollectionMockRecorder) UpdateDocumentWithOptions(ctx, key, document, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocumentWithOptions", reflect.TypeOf((*MockCollection)(nil).UpdateDocumentWithOptions), ctx, key, document, options)
}
// UpdateDocuments mocks base method.
func (m *MockCollection) UpdateDocuments(ctx context.Context, documents any) (arangodb.CollectionDocumentUpdateResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocuments", ctx, documents)
ret0, _ := ret[0].(arangodb.CollectionDocumentUpdateResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDocuments indicates an expected call of UpdateDocuments.
func (mr *MockCollectionMockRecorder) UpdateDocuments(ctx, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocuments", reflect.TypeOf((*MockCollection)(nil).UpdateDocuments), ctx, documents)
}
// UpdateDocumentsWithOptions mocks base method.
func (m *MockCollection) UpdateDocumentsWithOptions(ctx context.Context, documents any, opts *arangodb.CollectionDocumentUpdateOptions) (arangodb.CollectionDocumentUpdateResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocumentsWithOptions", ctx, documents, opts)
ret0, _ := ret[0].(arangodb.CollectionDocumentUpdateResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateDocumentsWithOptions indicates an expected call of UpdateDocumentsWithOptions.
func (mr *MockCollectionMockRecorder) UpdateDocumentsWithOptions(ctx, documents, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocumentsWithOptions", reflect.TypeOf((*MockCollection)(nil).UpdateDocumentsWithOptions), ctx, documents, opts)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_database.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/database.go
//
// Generated by this command:
//
// mockgen -source=/Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/database.go -destination=mock_database.go -package=arangodb
//
// Package arangodb is a generated GoMock package.
package arangodb
import (
context "context"
reflect "reflect"
arangodb "github.com/arangodb/go-driver/v2/arangodb"
gomock "go.uber.org/mock/gomock"
)
// MockDatabase is a mock of Database interface.
type MockDatabase struct {
ctrl *gomock.Controller
recorder *MockDatabaseMockRecorder
isgomock struct{}
}
// MockDatabaseMockRecorder is the mock recorder for MockDatabase.
type MockDatabaseMockRecorder struct {
mock *MockDatabase
}
// NewMockDatabase creates a new mock instance.
func NewMockDatabase(ctrl *gomock.Controller) *MockDatabase {
mock := &MockDatabase{ctrl: ctrl}
mock.recorder = &MockDatabaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDatabase) EXPECT() *MockDatabaseMockRecorder {
return m.recorder
}
// Analyzer mocks base method.
func (m *MockDatabase) Analyzer(ctx context.Context, name string) (arangodb.Analyzer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Analyzer", ctx, name)
ret0, _ := ret[0].(arangodb.Analyzer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Analyzer indicates an expected call of Analyzer.
func (mr *MockDatabaseMockRecorder) Analyzer(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Analyzer", reflect.TypeOf((*MockDatabase)(nil).Analyzer), ctx, name)
}
// Analyzers mocks base method.
func (m *MockDatabase) Analyzers(ctx context.Context) (arangodb.AnalyzersResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Analyzers", ctx)
ret0, _ := ret[0].(arangodb.AnalyzersResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Analyzers indicates an expected call of Analyzers.
func (mr *MockDatabaseMockRecorder) Analyzers(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Analyzers", reflect.TypeOf((*MockDatabase)(nil).Analyzers), ctx)
}
// BeginTransaction mocks base method.
func (m *MockDatabase) BeginTransaction(ctx context.Context, cols arangodb.TransactionCollections, opts *arangodb.BeginTransactionOptions) (arangodb.Transaction, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BeginTransaction", ctx, cols, opts)
ret0, _ := ret[0].(arangodb.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BeginTransaction indicates an expected call of BeginTransaction.
func (mr *MockDatabaseMockRecorder) BeginTransaction(ctx, cols, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeginTransaction", reflect.TypeOf((*MockDatabase)(nil).BeginTransaction), ctx, cols, opts)
}
// ClearQueryCache mocks base method.
func (m *MockDatabase) ClearQueryCache(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearQueryCache", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// ClearQueryCache indicates an expected call of ClearQueryCache.
func (mr *MockDatabaseMockRecorder) ClearQueryCache(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearQueryCache", reflect.TypeOf((*MockDatabase)(nil).ClearQueryCache), ctx)
}
// ClearQueryPlanCache mocks base method.
func (m *MockDatabase) ClearQueryPlanCache(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearQueryPlanCache", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// ClearQueryPlanCache indicates an expected call of ClearQueryPlanCache.
func (mr *MockDatabaseMockRecorder) ClearQueryPlanCache(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearQueryPlanCache", reflect.TypeOf((*MockDatabase)(nil).ClearQueryPlanCache), ctx)
}
// ClearSlowAQLQueries mocks base method.
func (m *MockDatabase) ClearSlowAQLQueries(ctx context.Context, all *bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClearSlowAQLQueries", ctx, all)
ret0, _ := ret[0].(error)
return ret0
}
// ClearSlowAQLQueries indicates an expected call of ClearSlowAQLQueries.
func (mr *MockDatabaseMockRecorder) ClearSlowAQLQueries(ctx, all any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClearSlowAQLQueries", reflect.TypeOf((*MockDatabase)(nil).ClearSlowAQLQueries), ctx, all)
}
// CollectionExists mocks base method.
func (m *MockDatabase) CollectionExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CollectionExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CollectionExists indicates an expected call of CollectionExists.
func (mr *MockDatabaseMockRecorder) CollectionExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CollectionExists", reflect.TypeOf((*MockDatabase)(nil).CollectionExists), ctx, name)
}
// Collections mocks base method.
func (m *MockDatabase) Collections(ctx context.Context) ([]arangodb.Collection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Collections", ctx)
ret0, _ := ret[0].([]arangodb.Collection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Collections indicates an expected call of Collections.
func (mr *MockDatabaseMockRecorder) Collections(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collections", reflect.TypeOf((*MockDatabase)(nil).Collections), ctx)
}
// CreateArangoSearchAliasView mocks base method.
func (m *MockDatabase) CreateArangoSearchAliasView(ctx context.Context, name string, options *arangodb.ArangoSearchAliasViewProperties) (arangodb.ArangoSearchViewAlias, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateArangoSearchAliasView", ctx, name, options)
ret0, _ := ret[0].(arangodb.ArangoSearchViewAlias)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateArangoSearchAliasView indicates an expected call of CreateArangoSearchAliasView.
func (mr *MockDatabaseMockRecorder) CreateArangoSearchAliasView(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateArangoSearchAliasView", reflect.TypeOf((*MockDatabase)(nil).CreateArangoSearchAliasView), ctx, name, options)
}
// CreateArangoSearchView mocks base method.
func (m *MockDatabase) CreateArangoSearchView(ctx context.Context, name string, options *arangodb.ArangoSearchViewProperties) (arangodb.ArangoSearchView, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateArangoSearchView", ctx, name, options)
ret0, _ := ret[0].(arangodb.ArangoSearchView)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateArangoSearchView indicates an expected call of CreateArangoSearchView.
func (mr *MockDatabaseMockRecorder) CreateArangoSearchView(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateArangoSearchView", reflect.TypeOf((*MockDatabase)(nil).CreateArangoSearchView), ctx, name, options)
}
// CreateCollectionV2 mocks base method.
func (m *MockDatabase) CreateCollectionV2(ctx context.Context, name string, props *arangodb.CreateCollectionPropertiesV2) (arangodb.Collection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollectionV2", ctx, name, props)
ret0, _ := ret[0].(arangodb.Collection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateCollectionV2 indicates an expected call of CreateCollectionV2.
func (mr *MockDatabaseMockRecorder) CreateCollectionV2(ctx, name, props any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollectionV2", reflect.TypeOf((*MockDatabase)(nil).CreateCollectionV2), ctx, name, props)
}
// CreateCollectionWithOptionsV2 mocks base method.
func (m *MockDatabase) CreateCollectionWithOptionsV2(ctx context.Context, name string, props *arangodb.CreateCollectionPropertiesV2, options *arangodb.CreateCollectionOptions) (arangodb.Collection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollectionWithOptionsV2", ctx, name, props, options)
ret0, _ := ret[0].(arangodb.Collection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateCollectionWithOptionsV2 indicates an expected call of CreateCollectionWithOptionsV2.
func (mr *MockDatabaseMockRecorder) CreateCollectionWithOptionsV2(ctx, name, props, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollectionWithOptionsV2", reflect.TypeOf((*MockDatabase)(nil).CreateCollectionWithOptionsV2), ctx, name, props, options)
}
// CreateGraph mocks base method.
func (m *MockDatabase) CreateGraph(ctx context.Context, name string, graph *arangodb.GraphDefinition, options *arangodb.CreateGraphOptions) (arangodb.Graph, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateGraph", ctx, name, graph, options)
ret0, _ := ret[0].(arangodb.Graph)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateGraph indicates an expected call of CreateGraph.
func (mr *MockDatabaseMockRecorder) CreateGraph(ctx, name, graph, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGraph", reflect.TypeOf((*MockDatabase)(nil).CreateGraph), ctx, name, graph, options)
}
// CreateUserDefinedFunction mocks base method.
func (m *MockDatabase) CreateUserDefinedFunction(ctx context.Context, options arangodb.UserDefinedFunctionObject) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUserDefinedFunction", ctx, options)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUserDefinedFunction indicates an expected call of CreateUserDefinedFunction.
func (mr *MockDatabaseMockRecorder) CreateUserDefinedFunction(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserDefinedFunction", reflect.TypeOf((*MockDatabase)(nil).CreateUserDefinedFunction), ctx, options)
}
// DeleteUserDefinedFunction mocks base method.
func (m *MockDatabase) DeleteUserDefinedFunction(ctx context.Context, name *string, group *bool) (*int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteUserDefinedFunction", ctx, name, group)
ret0, _ := ret[0].(*int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteUserDefinedFunction indicates an expected call of DeleteUserDefinedFunction.
func (mr *MockDatabaseMockRecorder) DeleteUserDefinedFunction(ctx, name, group any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteUserDefinedFunction", reflect.TypeOf((*MockDatabase)(nil).DeleteUserDefinedFunction), ctx, name, group)
}
// EnsureCreatedAnalyzer mocks base method.
func (m *MockDatabase) EnsureCreatedAnalyzer(ctx context.Context, analyzer *arangodb.AnalyzerDefinition) (arangodb.Analyzer, bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnsureCreatedAnalyzer", ctx, analyzer)
ret0, _ := ret[0].(arangodb.Analyzer)
ret1, _ := ret[1].(bool)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// EnsureCreatedAnalyzer indicates an expected call of EnsureCreatedAnalyzer.
func (mr *MockDatabaseMockRecorder) EnsureCreatedAnalyzer(ctx, analyzer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnsureCreatedAnalyzer", reflect.TypeOf((*MockDatabase)(nil).EnsureCreatedAnalyzer), ctx, analyzer)
}
// ExplainQuery mocks base method.
func (m *MockDatabase) ExplainQuery(ctx context.Context, query string, bindVars map[string]any, opts *arangodb.ExplainQueryOptions) (arangodb.ExplainQueryResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExplainQuery", ctx, query, bindVars, opts)
ret0, _ := ret[0].(arangodb.ExplainQueryResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExplainQuery indicates an expected call of ExplainQuery.
func (mr *MockDatabaseMockRecorder) ExplainQuery(ctx, query, bindVars, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExplainQuery", reflect.TypeOf((*MockDatabase)(nil).ExplainQuery), ctx, query, bindVars, opts)
}
// GetAllOptimizerRules mocks base method.
func (m *MockDatabase) GetAllOptimizerRules(ctx context.Context) ([]arangodb.OptimizerRules, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllOptimizerRules", ctx)
ret0, _ := ret[0].([]arangodb.OptimizerRules)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllOptimizerRules indicates an expected call of GetAllOptimizerRules.
func (mr *MockDatabaseMockRecorder) GetAllOptimizerRules(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllOptimizerRules", reflect.TypeOf((*MockDatabase)(nil).GetAllOptimizerRules), ctx)
}
// GetCollection mocks base method.
func (m *MockDatabase) GetCollection(ctx context.Context, name string, options *arangodb.GetCollectionOptions) (arangodb.Collection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetCollection", ctx, name, options)
ret0, _ := ret[0].(arangodb.Collection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetCollection indicates an expected call of GetCollection.
func (mr *MockDatabaseMockRecorder) GetCollection(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCollection", reflect.TypeOf((*MockDatabase)(nil).GetCollection), ctx, name, options)
}
// GetEdges mocks base method.
func (m *MockDatabase) GetEdges(ctx context.Context, name, vertex string, options *arangodb.GetEdgesOptions) ([]arangodb.EdgeDetails, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdges", ctx, name, vertex, options)
ret0, _ := ret[0].([]arangodb.EdgeDetails)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEdges indicates an expected call of GetEdges.
func (mr *MockDatabaseMockRecorder) GetEdges(ctx, name, vertex, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdges", reflect.TypeOf((*MockDatabase)(nil).GetEdges), ctx, name, vertex, options)
}
// GetQueryCacheProperties mocks base method.
func (m *MockDatabase) GetQueryCacheProperties(ctx context.Context) (arangodb.QueryCacheProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueryCacheProperties", ctx)
ret0, _ := ret[0].(arangodb.QueryCacheProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueryCacheProperties indicates an expected call of GetQueryCacheProperties.
func (mr *MockDatabaseMockRecorder) GetQueryCacheProperties(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueryCacheProperties", reflect.TypeOf((*MockDatabase)(nil).GetQueryCacheProperties), ctx)
}
// GetQueryEntriesCache mocks base method.
func (m *MockDatabase) GetQueryEntriesCache(ctx context.Context) ([]arangodb.QueryCacheEntriesRespObject, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueryEntriesCache", ctx)
ret0, _ := ret[0].([]arangodb.QueryCacheEntriesRespObject)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueryEntriesCache indicates an expected call of GetQueryEntriesCache.
func (mr *MockDatabaseMockRecorder) GetQueryEntriesCache(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueryEntriesCache", reflect.TypeOf((*MockDatabase)(nil).GetQueryEntriesCache), ctx)
}
// GetQueryPlanCache mocks base method.
func (m *MockDatabase) GetQueryPlanCache(ctx context.Context) ([]arangodb.QueryPlanCacheRespObject, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueryPlanCache", ctx)
ret0, _ := ret[0].([]arangodb.QueryPlanCacheRespObject)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueryPlanCache indicates an expected call of GetQueryPlanCache.
func (mr *MockDatabaseMockRecorder) GetQueryPlanCache(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueryPlanCache", reflect.TypeOf((*MockDatabase)(nil).GetQueryPlanCache), ctx)
}
// GetQueryProperties mocks base method.
func (m *MockDatabase) GetQueryProperties(ctx context.Context) (arangodb.QueryProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetQueryProperties", ctx)
ret0, _ := ret[0].(arangodb.QueryProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetQueryProperties indicates an expected call of GetQueryProperties.
func (mr *MockDatabaseMockRecorder) GetQueryProperties(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetQueryProperties", reflect.TypeOf((*MockDatabase)(nil).GetQueryProperties), ctx)
}
// GetUserDefinedFunctions mocks base method.
func (m *MockDatabase) GetUserDefinedFunctions(ctx context.Context) ([]arangodb.UserDefinedFunctionObject, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserDefinedFunctions", ctx)
ret0, _ := ret[0].([]arangodb.UserDefinedFunctionObject)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserDefinedFunctions indicates an expected call of GetUserDefinedFunctions.
func (mr *MockDatabaseMockRecorder) GetUserDefinedFunctions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserDefinedFunctions", reflect.TypeOf((*MockDatabase)(nil).GetUserDefinedFunctions), ctx)
}
// Graph mocks base method.
func (m *MockDatabase) Graph(ctx context.Context, name string, options *arangodb.GetGraphOptions) (arangodb.Graph, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Graph", ctx, name, options)
ret0, _ := ret[0].(arangodb.Graph)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Graph indicates an expected call of Graph.
func (mr *MockDatabaseMockRecorder) Graph(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Graph", reflect.TypeOf((*MockDatabase)(nil).Graph), ctx, name, options)
}
// GraphExists mocks base method.
func (m *MockDatabase) GraphExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GraphExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GraphExists indicates an expected call of GraphExists.
func (mr *MockDatabaseMockRecorder) GraphExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GraphExists", reflect.TypeOf((*MockDatabase)(nil).GraphExists), ctx, name)
}
// Graphs mocks base method.
func (m *MockDatabase) Graphs(ctx context.Context) (arangodb.GraphsResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Graphs", ctx)
ret0, _ := ret[0].(arangodb.GraphsResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Graphs indicates an expected call of Graphs.
func (mr *MockDatabaseMockRecorder) Graphs(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Graphs", reflect.TypeOf((*MockDatabase)(nil).Graphs), ctx)
}
// Info mocks base method.
func (m *MockDatabase) Info(ctx context.Context) (arangodb.DatabaseInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info", ctx)
ret0, _ := ret[0].(arangodb.DatabaseInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockDatabaseMockRecorder) Info(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockDatabase)(nil).Info), ctx)
}
// KeyGenerators mocks base method.
func (m *MockDatabase) KeyGenerators(ctx context.Context) (arangodb.KeyGeneratorsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyGenerators", ctx)
ret0, _ := ret[0].(arangodb.KeyGeneratorsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KeyGenerators indicates an expected call of KeyGenerators.
func (mr *MockDatabaseMockRecorder) KeyGenerators(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyGenerators", reflect.TypeOf((*MockDatabase)(nil).KeyGenerators), ctx)
}
// KillAQLQuery mocks base method.
func (m *MockDatabase) KillAQLQuery(ctx context.Context, queryId string, all *bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KillAQLQuery", ctx, queryId, all)
ret0, _ := ret[0].(error)
return ret0
}
// KillAQLQuery indicates an expected call of KillAQLQuery.
func (mr *MockDatabaseMockRecorder) KillAQLQuery(ctx, queryId, all any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KillAQLQuery", reflect.TypeOf((*MockDatabase)(nil).KillAQLQuery), ctx, queryId, all)
}
// ListOfRunningAQLQueries mocks base method.
func (m *MockDatabase) ListOfRunningAQLQueries(ctx context.Context, all *bool) ([]arangodb.RunningAQLQuery, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOfRunningAQLQueries", ctx, all)
ret0, _ := ret[0].([]arangodb.RunningAQLQuery)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOfRunningAQLQueries indicates an expected call of ListOfRunningAQLQueries.
func (mr *MockDatabaseMockRecorder) ListOfRunningAQLQueries(ctx, all any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOfRunningAQLQueries", reflect.TypeOf((*MockDatabase)(nil).ListOfRunningAQLQueries), ctx, all)
}
// ListOfSlowAQLQueries mocks base method.
func (m *MockDatabase) ListOfSlowAQLQueries(ctx context.Context, all *bool) ([]arangodb.RunningAQLQuery, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListOfSlowAQLQueries", ctx, all)
ret0, _ := ret[0].([]arangodb.RunningAQLQuery)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListOfSlowAQLQueries indicates an expected call of ListOfSlowAQLQueries.
func (mr *MockDatabaseMockRecorder) ListOfSlowAQLQueries(ctx, all any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOfSlowAQLQueries", reflect.TypeOf((*MockDatabase)(nil).ListOfSlowAQLQueries), ctx, all)
}
// ListTransactions mocks base method.
func (m *MockDatabase) ListTransactions(ctx context.Context) ([]arangodb.Transaction, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListTransactions", ctx)
ret0, _ := ret[0].([]arangodb.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListTransactions indicates an expected call of ListTransactions.
func (mr *MockDatabaseMockRecorder) ListTransactions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransactions", reflect.TypeOf((*MockDatabase)(nil).ListTransactions), ctx)
}
// ListTransactionsWithStatuses mocks base method.
func (m *MockDatabase) ListTransactionsWithStatuses(ctx context.Context, statuses ...arangodb.TransactionStatus) ([]arangodb.Transaction, error) {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range statuses {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ListTransactionsWithStatuses", varargs...)
ret0, _ := ret[0].([]arangodb.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListTransactionsWithStatuses indicates an expected call of ListTransactionsWithStatuses.
func (mr *MockDatabaseMockRecorder) ListTransactionsWithStatuses(ctx any, statuses ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, statuses...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTransactionsWithStatuses", reflect.TypeOf((*MockDatabase)(nil).ListTransactionsWithStatuses), varargs...)
}
// Name mocks base method.
func (m *MockDatabase) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockDatabaseMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockDatabase)(nil).Name))
}
// Query mocks base method.
func (m *MockDatabase) Query(ctx context.Context, query string, opts *arangodb.QueryOptions) (arangodb.Cursor, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query, opts)
ret0, _ := ret[0].(arangodb.Cursor)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockDatabaseMockRecorder) Query(ctx, query, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDatabase)(nil).Query), ctx, query, opts)
}
// QueryBatch mocks base method.
func (m *MockDatabase) QueryBatch(ctx context.Context, query string, opts *arangodb.QueryOptions, result any) (arangodb.CursorBatch, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryBatch", ctx, query, opts, result)
ret0, _ := ret[0].(arangodb.CursorBatch)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryBatch indicates an expected call of QueryBatch.
func (mr *MockDatabaseMockRecorder) QueryBatch(ctx, query, opts, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryBatch", reflect.TypeOf((*MockDatabase)(nil).QueryBatch), ctx, query, opts, result)
}
// Remove mocks base method.
func (m *MockDatabase) Remove(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockDatabaseMockRecorder) Remove(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockDatabase)(nil).Remove), ctx)
}
// SetQueryCacheProperties mocks base method.
func (m *MockDatabase) SetQueryCacheProperties(ctx context.Context, options arangodb.QueryCacheProperties) (arangodb.QueryCacheProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetQueryCacheProperties", ctx, options)
ret0, _ := ret[0].(arangodb.QueryCacheProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SetQueryCacheProperties indicates an expected call of SetQueryCacheProperties.
func (mr *MockDatabaseMockRecorder) SetQueryCacheProperties(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetQueryCacheProperties", reflect.TypeOf((*MockDatabase)(nil).SetQueryCacheProperties), ctx, options)
}
// Transaction mocks base method.
func (m *MockDatabase) Transaction(ctx context.Context, id arangodb.TransactionID) (arangodb.Transaction, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Transaction", ctx, id)
ret0, _ := ret[0].(arangodb.Transaction)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Transaction indicates an expected call of Transaction.
func (mr *MockDatabaseMockRecorder) Transaction(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transaction", reflect.TypeOf((*MockDatabase)(nil).Transaction), ctx, id)
}
// TransactionJS mocks base method.
func (m *MockDatabase) TransactionJS(ctx context.Context, options arangodb.TransactionJSOptions) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TransactionJS", ctx, options)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TransactionJS indicates an expected call of TransactionJS.
func (mr *MockDatabaseMockRecorder) TransactionJS(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactionJS", reflect.TypeOf((*MockDatabase)(nil).TransactionJS), ctx, options)
}
// UpdateQueryProperties mocks base method.
func (m *MockDatabase) UpdateQueryProperties(ctx context.Context, options arangodb.QueryProperties) (arangodb.QueryProperties, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateQueryProperties", ctx, options)
ret0, _ := ret[0].(arangodb.QueryProperties)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateQueryProperties indicates an expected call of UpdateQueryProperties.
func (mr *MockDatabaseMockRecorder) UpdateQueryProperties(ctx, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateQueryProperties", reflect.TypeOf((*MockDatabase)(nil).UpdateQueryProperties), ctx, options)
}
// ValidateQuery mocks base method.
func (m *MockDatabase) ValidateQuery(ctx context.Context, query string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ValidateQuery", ctx, query)
ret0, _ := ret[0].(error)
return ret0
}
// ValidateQuery indicates an expected call of ValidateQuery.
func (mr *MockDatabaseMockRecorder) ValidateQuery(ctx, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateQuery", reflect.TypeOf((*MockDatabase)(nil).ValidateQuery), ctx, query)
}
// View mocks base method.
func (m *MockDatabase) View(ctx context.Context, name string) (arangodb.View, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "View", ctx, name)
ret0, _ := ret[0].(arangodb.View)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// View indicates an expected call of View.
func (mr *MockDatabaseMockRecorder) View(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "View", reflect.TypeOf((*MockDatabase)(nil).View), ctx, name)
}
// ViewExists mocks base method.
func (m *MockDatabase) ViewExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ViewExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ViewExists indicates an expected call of ViewExists.
func (mr *MockDatabaseMockRecorder) ViewExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ViewExists", reflect.TypeOf((*MockDatabase)(nil).ViewExists), ctx, name)
}
// Views mocks base method.
func (m *MockDatabase) Views(ctx context.Context) (arangodb.ViewsResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Views", ctx)
ret0, _ := ret[0].(arangodb.ViewsResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Views indicates an expected call of Views.
func (mr *MockDatabaseMockRecorder) Views(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Views", reflect.TypeOf((*MockDatabase)(nil).Views), ctx)
}
// ViewsAll mocks base method.
func (m *MockDatabase) ViewsAll(ctx context.Context) ([]arangodb.View, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ViewsAll", ctx)
ret0, _ := ret[0].([]arangodb.View)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ViewsAll indicates an expected call of ViewsAll.
func (mr *MockDatabaseMockRecorder) ViewsAll(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ViewsAll", reflect.TypeOf((*MockDatabase)(nil).ViewsAll), ctx)
}
// WithTransaction mocks base method.
func (m *MockDatabase) WithTransaction(ctx context.Context, cols arangodb.TransactionCollections, opts *arangodb.BeginTransactionOptions, commitOptions *arangodb.CommitTransactionOptions, abortOptions *arangodb.AbortTransactionOptions, w arangodb.TransactionWrap) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WithTransaction", ctx, cols, opts, commitOptions, abortOptions, w)
ret0, _ := ret[0].(error)
return ret0
}
// WithTransaction indicates an expected call of WithTransaction.
func (mr *MockDatabaseMockRecorder) WithTransaction(ctx, cols, opts, commitOptions, abortOptions, w any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithTransaction", reflect.TypeOf((*MockDatabase)(nil).WithTransaction), ctx, cols, opts, commitOptions, abortOptions, w)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_graph.go
================================================
package arangodb
import (
"context"
"reflect"
"github.com/arangodb/go-driver/v2/arangodb"
"go.uber.org/mock/gomock"
)
// MockDatabaseGraph is a mock of DatabaseGraph interface.
type MockDatabaseGraph struct {
ctrl *gomock.Controller
recorder *MockDatabaseGraphMockRecorder
}
// MockDatabaseGraphMockRecorder is the mock recorder for MockDatabaseGraph.
type MockDatabaseGraphMockRecorder struct {
mock *MockDatabaseGraph
}
type EdgeDirection string
type GetEdgesOptions struct {
// The direction of the edges. Allowed values are "in" and "out". If not set, edges in both directions are returned.
Direction EdgeDirection `json:"direction,omitempty"`
// Set this to true to allow the Coordinator to ask any shard replica for the data, not only the shard leader.
// This may result array of collection names that is used to create SatelliteCollections for a (Disjoint) SmartGraph
// using SatelliteCollections (Enterprise Edition only). Each array element must be a string and a valid
// collection name. The collection type cannot be modified later.
Satellites []string `json:"satellites,omitempty"`
}
type GraphsResponseReader interface {
// Read returns next Graph. If no Graph left, shared.NoMoreDocumentsError returned
Read() (arangodb.Graph, error)
}
// NewMockDatabaseGraph creates a new mock instance.
func NewMockDatabaseGraph(ctrl *gomock.Controller) *MockDatabaseGraph {
mock := &MockDatabaseGraph{ctrl: ctrl}
mock.recorder = &MockDatabaseGraphMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDatabaseGraph) EXPECT() *MockDatabaseGraphMockRecorder {
return m.recorder
}
// CreateGraph mocks base method.
func (m *MockDatabaseGraph) CreateGraph(ctx context.Context, name string, graph *arangodb.GraphDefinition,
options *arangodb.CreateGraphOptions) (arangodb.Graph, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateGraph", ctx, name, graph, options)
ret0, _ := ret[0].(arangodb.Graph)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateGraph indicates an expected call of CreateGraph.
func (mr *MockDatabaseGraphMockRecorder) CreateGraph(ctx, name, graph, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGraph",
reflect.TypeOf((*MockDatabaseGraph)(nil).CreateGraph), ctx, name, graph, options)
}
// GetEdges mocks base method.
func (m *MockDatabaseGraph) GetEdges(ctx context.Context, name, vertex string,
options *GetEdgesOptions) ([]EdgeDetails, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdges", ctx, name, vertex, options)
ret0, _ := ret[0].([]EdgeDetails)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEdges indicates an expected call of GetEdges.
func (mr *MockDatabaseGraphMockRecorder) GetEdges(ctx, name, vertex, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdges",
reflect.TypeOf((*MockDatabaseGraph)(nil).GetEdges), ctx, name, vertex, options)
}
// Graph mocks base method.
func (m *MockDatabaseGraph) Graph(ctx context.Context, name string,
options *arangodb.GetGraphOptions) (arangodb.Graph, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Graph", ctx, name, options)
ret0, _ := ret[0].(arangodb.Graph)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Graph indicates an expected call of Graph.
func (mr *MockDatabaseGraphMockRecorder) Graph(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Graph",
reflect.TypeOf((*MockDatabaseGraph)(nil).Graph), ctx, name, options)
}
// GraphExists mocks base method.
func (m *MockDatabaseGraph) GraphExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GraphExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GraphExists indicates an expected call of GraphExists.
func (mr *MockDatabaseGraphMockRecorder) GraphExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GraphExists",
reflect.TypeOf((*MockDatabaseGraph)(nil).GraphExists), ctx, name)
}
// Graphs mocks base method.
func (m *MockDatabaseGraph) Graphs(ctx context.Context) (GraphsResponseReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Graphs", ctx)
ret0, _ := ret[0].(GraphsResponseReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Graphs indicates an expected call of Graphs.
func (mr *MockDatabaseGraphMockRecorder) Graphs(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Graphs",
reflect.TypeOf((*MockDatabaseGraph)(nil).Graphs), ctx)
}
// MockGraphsResponseReader is a mock of GraphsResponseReader interface.
type MockGraphsResponseReader struct {
ctrl *gomock.Controller
recorder *MockGraphsResponseReaderMockRecorder
}
// MockGraphsResponseReaderMockRecorder is the mock recorder for MockGraphsResponseReader.
type MockGraphsResponseReaderMockRecorder struct {
mock *MockGraphsResponseReader
}
// NewMockGraphsResponseReader creates a new mock instance.
func NewMockGraphsResponseReader(ctrl *gomock.Controller) *MockGraphsResponseReader {
mock := &MockGraphsResponseReader{ctrl: ctrl}
mock.recorder = &MockGraphsResponseReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGraphsResponseReader) EXPECT() *MockGraphsResponseReaderMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *MockGraphsResponseReader) Read() (arangodb.Graph, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read")
ret0, _ := ret[0].(arangodb.Graph)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockGraphsResponseReaderMockRecorder) Read() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read",
reflect.TypeOf((*MockGraphsResponseReader)(nil).Read))
}
// MockGraph is a mock of Graph interface.
type MockGraph struct {
ctrl *gomock.Controller
recorder *MockGraphMockRecorder
}
// MockGraphMockRecorder is the mock recorder for MockGraph.
type MockGraphMockRecorder struct {
mock *MockGraph
}
// NewMockGraph creates a new mock instance.
func NewMockGraph(ctrl *gomock.Controller) *MockGraph {
mock := &MockGraph{ctrl: ctrl}
mock.recorder = &MockGraphMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGraph) EXPECT() *MockGraphMockRecorder {
return m.recorder
}
// CreateEdgeDefinition mocks base method.
func (m *MockGraph) CreateEdgeDefinition(ctx context.Context, collection string, from, to []string,
opts *arangodb.CreateEdgeDefinitionOptions) (arangodb.CreateEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateEdgeDefinition", ctx, collection, from, to, opts)
ret0, _ := ret[0].(arangodb.CreateEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateEdgeDefinition indicates an expected call of CreateEdgeDefinition.
func (mr *MockGraphMockRecorder) CreateEdgeDefinition(ctx, collection, from, to, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEdgeDefinition",
reflect.TypeOf((*MockGraph)(nil).CreateEdgeDefinition), ctx, collection, from, to, opts)
}
// CreateVertexCollection mocks base method.
func (m *MockGraph) CreateVertexCollection(ctx context.Context, name string,
opts *arangodb.CreateVertexCollectionOptions) (arangodb.CreateVertexCollectionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateVertexCollection", ctx, name, opts)
ret0, _ := ret[0].(arangodb.CreateVertexCollectionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateVertexCollection indicates an expected call of CreateVertexCollection.
func (mr *MockGraphMockRecorder) CreateVertexCollection(ctx, name, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVertexCollection",
reflect.TypeOf((*MockGraph)(nil).CreateVertexCollection), ctx, name, opts)
}
// DeleteEdgeDefinition mocks base method.
func (m *MockGraph) DeleteEdgeDefinition(ctx context.Context, collection string,
opts *arangodb.DeleteEdgeDefinitionOptions) (arangodb.DeleteEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteEdgeDefinition", ctx, collection, opts)
ret0, _ := ret[0].(arangodb.DeleteEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteEdgeDefinition indicates an expected call of DeleteEdgeDefinition.
func (mr *MockGraphMockRecorder) DeleteEdgeDefinition(ctx, collection, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEdgeDefinition",
reflect.TypeOf((*MockGraph)(nil).DeleteEdgeDefinition), ctx, collection, opts)
}
// DeleteVertexCollection mocks base method.
func (m *MockGraph) DeleteVertexCollection(ctx context.Context, name string,
opts *arangodb.DeleteVertexCollectionOptions) (arangodb.DeleteVertexCollectionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteVertexCollection", ctx, name, opts)
ret0, _ := ret[0].(arangodb.DeleteVertexCollectionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteVertexCollection indicates an expected call of DeleteVertexCollection.
func (mr *MockGraphMockRecorder) DeleteVertexCollection(ctx, name, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVertexCollection",
reflect.TypeOf((*MockGraph)(nil).DeleteVertexCollection), ctx, name, opts)
}
// EdgeDefinition mocks base method.
func (m *MockGraph) EdgeDefinition(ctx context.Context, collection string) (arangodb.Edge, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EdgeDefinition", ctx, collection)
ret0, _ := ret[0].(arangodb.Edge)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EdgeDefinition indicates an expected call of EdgeDefinition.
func (mr *MockGraphMockRecorder) EdgeDefinition(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EdgeDefinition",
reflect.TypeOf((*MockGraph)(nil).EdgeDefinition), ctx, collection)
}
// EdgeDefinitionExists mocks base method.
func (m *MockGraph) EdgeDefinitionExists(ctx context.Context, collection string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EdgeDefinitionExists", ctx, collection)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EdgeDefinitionExists indicates an expected call of EdgeDefinitionExists.
func (mr *MockGraphMockRecorder) EdgeDefinitionExists(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EdgeDefinitionExists",
reflect.TypeOf((*MockGraph)(nil).EdgeDefinitionExists), ctx, collection)
}
// EdgeDefinitions mocks base method.
func (m *MockGraph) EdgeDefinitions() []arangodb.EdgeDefinition {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EdgeDefinitions")
ret0, _ := ret[0].([]arangodb.EdgeDefinition)
return ret0
}
// EdgeDefinitions indicates an expected call of EdgeDefinitions.
func (mr *MockGraphMockRecorder) EdgeDefinitions() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EdgeDefinitions", reflect.TypeOf((*MockGraph)(nil).EdgeDefinitions))
}
// GetEdgeDefinitions mocks base method.
func (m *MockGraph) GetEdgeDefinitions(ctx context.Context) ([]arangodb.Edge, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdgeDefinitions", ctx)
ret0, _ := ret[0].([]arangodb.Edge)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEdgeDefinitions indicates an expected call of GetEdgeDefinitions.
func (mr *MockGraphMockRecorder) GetEdgeDefinitions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdgeDefinitions", reflect.TypeOf((*MockGraph)(nil).GetEdgeDefinitions), ctx)
}
// IsDisjoint mocks base method.
func (m *MockGraph) IsDisjoint() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsDisjoint")
ret0, _ := ret[0].(bool)
return ret0
}
// IsDisjoint indicates an expected call of IsDisjoint.
func (mr *MockGraphMockRecorder) IsDisjoint() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDisjoint", reflect.TypeOf((*MockGraph)(nil).IsDisjoint))
}
// IsSatellite mocks base method.
func (m *MockGraph) IsSatellite() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSatellite")
ret0, _ := ret[0].(bool)
return ret0
}
// IsSatellite indicates an expected call of IsSatellite.
func (mr *MockGraphMockRecorder) IsSatellite() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSatellite", reflect.TypeOf((*MockGraph)(nil).IsSatellite))
}
// IsSmart mocks base method.
func (m *MockGraph) IsSmart() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsSmart")
ret0, _ := ret[0].(bool)
return ret0
}
// IsSmart indicates an expected call of IsSmart.
func (mr *MockGraphMockRecorder) IsSmart() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSmart", reflect.TypeOf((*MockGraph)(nil).IsSmart))
}
// Name mocks base method.
func (m *MockGraph) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockGraphMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockGraph)(nil).Name))
}
// NumberOfShards mocks base method.
func (m *MockGraph) NumberOfShards() *int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NumberOfShards")
ret0, _ := ret[0].(*int)
return ret0
}
// NumberOfShards indicates an expected call of NumberOfShards.
func (mr *MockGraphMockRecorder) NumberOfShards() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumberOfShards",
reflect.TypeOf((*MockGraph)(nil).NumberOfShards))
}
// OrphanCollections mocks base method.
func (m *MockGraph) OrphanCollections() []string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OrphanCollections")
ret0, _ := ret[0].([]string)
return ret0
}
// OrphanCollections indicates an expected call of OrphanCollections.
func (mr *MockGraphMockRecorder) OrphanCollections() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrphanCollections",
reflect.TypeOf((*MockGraph)(nil).OrphanCollections))
}
// Remove mocks base method.
func (m *MockGraph) Remove(ctx context.Context, opts *arangodb.RemoveGraphOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, opts)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockGraphMockRecorder) Remove(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockGraph)(nil).Remove), ctx, opts)
}
// ReplaceEdgeDefinition mocks base method.
func (m *MockGraph) ReplaceEdgeDefinition(ctx context.Context, collection string, from, to []string,
opts *arangodb.ReplaceEdgeOptions) (arangodb.ReplaceEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceEdgeDefinition", ctx, collection, from, to, opts)
ret0, _ := ret[0].(arangodb.ReplaceEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceEdgeDefinition indicates an expected call of ReplaceEdgeDefinition.
func (mr *MockGraphMockRecorder) ReplaceEdgeDefinition(ctx, collection, from, to, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceEdgeDefinition",
reflect.TypeOf((*MockGraph)(nil).ReplaceEdgeDefinition), ctx, collection, from, to, opts)
}
// ReplicationFactor mocks base method.
func (m *MockGraph) ReplicationFactor() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplicationFactor")
ret0, _ := ret[0].(int)
return ret0
}
// ReplicationFactor indicates an expected call of ReplicationFactor.
func (mr *MockGraphMockRecorder) ReplicationFactor() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplicationFactor",
reflect.TypeOf((*MockGraph)(nil).ReplicationFactor))
}
// SmartGraphAttribute mocks base method.
func (m *MockGraph) SmartGraphAttribute() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SmartGraphAttribute")
ret0, _ := ret[0].(string)
return ret0
}
// SmartGraphAttribute indicates an expected call of SmartGraphAttribute.
func (mr *MockGraphMockRecorder) SmartGraphAttribute() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SmartGraphAttribute",
reflect.TypeOf((*MockGraph)(nil).SmartGraphAttribute))
}
// VertexCollection mocks base method.
func (m *MockGraph) VertexCollection(ctx context.Context, name string) (arangodb.VertexCollection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollection", ctx, name)
ret0, _ := ret[0].(arangodb.VertexCollection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollection indicates an expected call of VertexCollection.
func (mr *MockGraphMockRecorder) VertexCollection(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VertexCollection",
reflect.TypeOf((*MockGraph)(nil).VertexCollection), ctx, name)
}
// VertexCollectionExists mocks base method.
func (m *MockGraph) VertexCollectionExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollectionExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollectionExists indicates an expected call of VertexCollectionExists.
func (mr *MockGraphMockRecorder) VertexCollectionExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VertexCollectionExists",
reflect.TypeOf((*MockGraph)(nil).VertexCollectionExists), ctx, name)
}
// VertexCollections mocks base method.
func (m *MockGraph) VertexCollections(ctx context.Context) ([]arangodb.VertexCollection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollections", ctx)
ret0, _ := ret[0].([]arangodb.VertexCollection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollections indicates an expected call of VertexCollections.
func (mr *MockGraphMockRecorder) VertexCollections(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VertexCollections",
reflect.TypeOf((*MockGraph)(nil).VertexCollections), ctx)
}
// WriteConcern mocks base method.
func (m *MockGraph) WriteConcern() *int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WriteConcern")
ret0, _ := ret[0].(*int)
return ret0
}
// WriteConcern indicates an expected call of WriteConcern.
func (mr *MockGraphMockRecorder) WriteConcern() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteConcern", reflect.TypeOf((*MockGraph)(nil).WriteConcern))
}
// MockGraphVertexCollections is a mock of GraphVertexCollections interface.
type MockGraphVertexCollections struct {
ctrl *gomock.Controller
recorder *MockGraphVertexCollectionsMockRecorder
}
// MockGraphVertexCollectionsMockRecorder is the mock recorder for MockGraphVertexCollections.
type MockGraphVertexCollectionsMockRecorder struct {
mock *MockGraphVertexCollections
}
// NewMockGraphVertexCollections creates a new mock instance.
func NewMockGraphVertexCollections(ctrl *gomock.Controller) *MockGraphVertexCollections {
mock := &MockGraphVertexCollections{ctrl: ctrl}
mock.recorder = &MockGraphVertexCollectionsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGraphVertexCollections) EXPECT() *MockGraphVertexCollectionsMockRecorder {
return m.recorder
}
// CreateVertexCollection mocks base method.
func (m *MockGraphVertexCollections) CreateVertexCollection(ctx context.Context, name string,
opts *arangodb.CreateVertexCollectionOptions) (arangodb.CreateVertexCollectionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateVertexCollection", ctx, name, opts)
ret0, _ := ret[0].(arangodb.CreateVertexCollectionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateVertexCollection indicates an expected call of CreateVertexCollection.
func (mr *MockGraphVertexCollectionsMockRecorder) CreateVertexCollection(ctx, name, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVertexCollection",
reflect.TypeOf((*MockGraphVertexCollections)(nil).CreateVertexCollection), ctx, name, opts)
}
// DeleteVertexCollection mocks base method.
func (m *MockGraphVertexCollections) DeleteVertexCollection(ctx context.Context, name string,
opts *arangodb.DeleteVertexCollectionOptions) (arangodb.DeleteVertexCollectionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteVertexCollection", ctx, name, opts)
ret0, _ := ret[0].(arangodb.DeleteVertexCollectionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteVertexCollection indicates an expected call of DeleteVertexCollection.
func (mr *MockGraphVertexCollectionsMockRecorder) DeleteVertexCollection(ctx, name, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"DeleteVertexCollection", reflect.TypeOf((*MockGraphVertexCollections)(nil).DeleteVertexCollection), ctx, name, opts)
}
// VertexCollection mocks base method.
func (m *MockGraphVertexCollections) VertexCollection(ctx context.Context, name string) (arangodb.VertexCollection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollection", ctx, name)
ret0, _ := ret[0].(arangodb.VertexCollection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollection indicates an expected call of VertexCollection.
func (mr *MockGraphVertexCollectionsMockRecorder) VertexCollection(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"VertexCollection", reflect.TypeOf((*MockGraphVertexCollections)(nil).VertexCollection), ctx, name)
}
// VertexCollectionExists mocks base method.
func (m *MockGraphVertexCollections) VertexCollectionExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollectionExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollectionExists indicates an expected call of VertexCollectionExists.
func (mr *MockGraphVertexCollectionsMockRecorder) VertexCollectionExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"VertexCollectionExists", reflect.TypeOf((*MockGraphVertexCollections)(nil).VertexCollectionExists), ctx, name)
}
// VertexCollections mocks base method.
func (m *MockGraphVertexCollections) VertexCollections(ctx context.Context) ([]arangodb.VertexCollection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VertexCollections", ctx)
ret0, _ := ret[0].([]arangodb.VertexCollection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VertexCollections indicates an expected call of VertexCollections.
func (mr *MockGraphVertexCollectionsMockRecorder) VertexCollections(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VertexCollections",
reflect.TypeOf((*MockGraphVertexCollections)(nil).VertexCollections), ctx)
}
// MockGraphEdgesDefinition is a mock of GraphEdgesDefinition interface.
type MockGraphEdgesDefinition struct {
ctrl *gomock.Controller
recorder *MockGraphEdgesDefinitionMockRecorder
}
// MockGraphEdgesDefinitionMockRecorder is the mock recorder for MockGraphEdgesDefinition.
type MockGraphEdgesDefinitionMockRecorder struct {
mock *MockGraphEdgesDefinition
}
// NewMockGraphEdgesDefinition creates a new mock instance.
func NewMockGraphEdgesDefinition(ctrl *gomock.Controller) *MockGraphEdgesDefinition {
mock := &MockGraphEdgesDefinition{ctrl: ctrl}
mock.recorder = &MockGraphEdgesDefinitionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockGraphEdgesDefinition) EXPECT() *MockGraphEdgesDefinitionMockRecorder {
return m.recorder
}
// CreateEdgeDefinition mocks base method.
func (m *MockGraphEdgesDefinition) CreateEdgeDefinition(ctx context.Context, collection string, from, to []string,
opts *arangodb.CreateEdgeDefinitionOptions) (arangodb.CreateEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateEdgeDefinition", ctx, collection, from, to, opts)
ret0, _ := ret[0].(arangodb.CreateEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateEdgeDefinition indicates an expected call of CreateEdgeDefinition.
func (mr *MockGraphEdgesDefinitionMockRecorder) CreateEdgeDefinition(ctx, collection, from, to, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEdgeDefinition",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).CreateEdgeDefinition), ctx, collection, from, to, opts)
}
// DeleteEdgeDefinition mocks base method.
func (m *MockGraphEdgesDefinition) DeleteEdgeDefinition(ctx context.Context, collection string,
opts *arangodb.DeleteEdgeDefinitionOptions) (arangodb.DeleteEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteEdgeDefinition", ctx, collection, opts)
ret0, _ := ret[0].(arangodb.DeleteEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteEdgeDefinition indicates an expected call of DeleteEdgeDefinition.
func (mr *MockGraphEdgesDefinitionMockRecorder) DeleteEdgeDefinition(ctx, collection, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteEdgeDefinition",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).DeleteEdgeDefinition), ctx, collection, opts)
}
// EdgeDefinition mocks base method.
func (m *MockGraphEdgesDefinition) EdgeDefinition(ctx context.Context, collection string) (arangodb.Edge, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EdgeDefinition", ctx, collection)
ret0, _ := ret[0].(arangodb.Edge)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EdgeDefinition indicates an expected call of EdgeDefinition.
func (mr *MockGraphEdgesDefinitionMockRecorder) EdgeDefinition(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EdgeDefinition",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).EdgeDefinition), ctx, collection)
}
// EdgeDefinitionExists mocks base method.
func (m *MockGraphEdgesDefinition) EdgeDefinitionExists(ctx context.Context, collection string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EdgeDefinitionExists", ctx, collection)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EdgeDefinitionExists indicates an expected call of EdgeDefinitionExists.
func (mr *MockGraphEdgesDefinitionMockRecorder) EdgeDefinitionExists(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EdgeDefinitionExists",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).EdgeDefinitionExists), ctx, collection)
}
// GetEdgeDefinitions mocks base method.
func (m *MockGraphEdgesDefinition) GetEdgeDefinitions(ctx context.Context) ([]arangodb.Edge, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetEdgeDefinitions", ctx)
ret0, _ := ret[0].([]arangodb.Edge)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetEdgeDefinitions indicates an expected call of GetEdgeDefinitions.
func (mr *MockGraphEdgesDefinitionMockRecorder) GetEdgeDefinitions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEdgeDefinitions",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).GetEdgeDefinitions), ctx)
}
// ReplaceEdgeDefinition mocks base method.
func (m *MockGraphEdgesDefinition) ReplaceEdgeDefinition(ctx context.Context, collection string,
from, to []string, opts *arangodb.ReplaceEdgeOptions) (arangodb.ReplaceEdgeDefinitionResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceEdgeDefinition", ctx, collection, from, to, opts)
ret0, _ := ret[0].(arangodb.ReplaceEdgeDefinitionResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceEdgeDefinition indicates an expected call of ReplaceEdgeDefinition.
func (mr *MockGraphEdgesDefinitionMockRecorder) ReplaceEdgeDefinition(ctx, collection, from, to, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceEdgeDefinition",
reflect.TypeOf((*MockGraphEdgesDefinition)(nil).ReplaceEdgeDefinition), ctx, collection, from, to, opts)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: /Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/client.go
//
// Generated by this command:
//
// mockgen -source=/Users/zopdev/go/pkg/mod/github.com/arangodb/go-driver/v2@v2.1.6/arangodb/client.go -destination=mock_interfaces.go -package=arangodb
//
// Package arangodb is a generated GoMock package.
package arangodb
import (
context "context"
json "encoding/json"
reflect "reflect"
arangodb "github.com/arangodb/go-driver/v2/arangodb"
connection "github.com/arangodb/go-driver/v2/connection"
gomock "go.uber.org/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
isgomock struct{}
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// AccessibleDatabases mocks base method.
func (m *MockClient) AccessibleDatabases(ctx context.Context) ([]arangodb.Database, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccessibleDatabases", ctx)
ret0, _ := ret[0].([]arangodb.Database)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AccessibleDatabases indicates an expected call of AccessibleDatabases.
func (mr *MockClientMockRecorder) AccessibleDatabases(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccessibleDatabases", reflect.TypeOf((*MockClient)(nil).AccessibleDatabases), ctx)
}
// ApplierStart mocks base method.
func (m *MockClient) ApplierStart(ctx context.Context, dbName string, global *bool, from *string) (arangodb.ApplierStateResp, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplierStart", ctx, dbName, global, from)
ret0, _ := ret[0].(arangodb.ApplierStateResp)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ApplierStart indicates an expected call of ApplierStart.
func (mr *MockClientMockRecorder) ApplierStart(ctx, dbName, global, from any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplierStart", reflect.TypeOf((*MockClient)(nil).ApplierStart), ctx, dbName, global, from)
}
// ApplierStop mocks base method.
func (m *MockClient) ApplierStop(ctx context.Context, dbName string, global *bool) (arangodb.ApplierStateResp, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplierStop", ctx, dbName, global)
ret0, _ := ret[0].(arangodb.ApplierStateResp)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ApplierStop indicates an expected call of ApplierStop.
func (mr *MockClientMockRecorder) ApplierStop(ctx, dbName, global any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplierStop", reflect.TypeOf((*MockClient)(nil).ApplierStop), ctx, dbName, global)
}
// AsyncJobCancel mocks base method.
func (m *MockClient) AsyncJobCancel(ctx context.Context, jobID string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AsyncJobCancel", ctx, jobID)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AsyncJobCancel indicates an expected call of AsyncJobCancel.
func (mr *MockClientMockRecorder) AsyncJobCancel(ctx, jobID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncJobCancel", reflect.TypeOf((*MockClient)(nil).AsyncJobCancel), ctx, jobID)
}
// AsyncJobDelete mocks base method.
func (m *MockClient) AsyncJobDelete(ctx context.Context, deleteType arangodb.AsyncJobDeleteType, opts *arangodb.AsyncJobDeleteOptions) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AsyncJobDelete", ctx, deleteType, opts)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AsyncJobDelete indicates an expected call of AsyncJobDelete.
func (mr *MockClientMockRecorder) AsyncJobDelete(ctx, deleteType, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncJobDelete", reflect.TypeOf((*MockClient)(nil).AsyncJobDelete), ctx, deleteType, opts)
}
// AsyncJobList mocks base method.
func (m *MockClient) AsyncJobList(ctx context.Context, jobType arangodb.AsyncJobStatusType, opts *arangodb.AsyncJobListOptions) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AsyncJobList", ctx, jobType, opts)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AsyncJobList indicates an expected call of AsyncJobList.
func (mr *MockClientMockRecorder) AsyncJobList(ctx, jobType, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncJobList", reflect.TypeOf((*MockClient)(nil).AsyncJobList), ctx, jobType, opts)
}
// AsyncJobStatus mocks base method.
func (m *MockClient) AsyncJobStatus(ctx context.Context, jobID string) (arangodb.AsyncJobStatusType, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AsyncJobStatus", ctx, jobID)
ret0, _ := ret[0].(arangodb.AsyncJobStatusType)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AsyncJobStatus indicates an expected call of AsyncJobStatus.
func (mr *MockClientMockRecorder) AsyncJobStatus(ctx, jobID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncJobStatus", reflect.TypeOf((*MockClient)(nil).AsyncJobStatus), ctx, jobID)
}
// BackupCreate mocks base method.
func (m *MockClient) BackupCreate(ctx context.Context, opt *arangodb.BackupCreateOptions) (arangodb.BackupResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupCreate", ctx, opt)
ret0, _ := ret[0].(arangodb.BackupResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BackupCreate indicates an expected call of BackupCreate.
func (mr *MockClientMockRecorder) BackupCreate(ctx, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupCreate", reflect.TypeOf((*MockClient)(nil).BackupCreate), ctx, opt)
}
// BackupDelete mocks base method.
func (m *MockClient) BackupDelete(ctx context.Context, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupDelete", ctx, id)
ret0, _ := ret[0].(error)
return ret0
}
// BackupDelete indicates an expected call of BackupDelete.
func (mr *MockClientMockRecorder) BackupDelete(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupDelete", reflect.TypeOf((*MockClient)(nil).BackupDelete), ctx, id)
}
// BackupDownload mocks base method.
func (m *MockClient) BackupDownload(ctx context.Context, backupId, remoteRepository string, config any) (arangodb.TransferMonitor, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupDownload", ctx, backupId, remoteRepository, config)
ret0, _ := ret[0].(arangodb.TransferMonitor)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BackupDownload indicates an expected call of BackupDownload.
func (mr *MockClientMockRecorder) BackupDownload(ctx, backupId, remoteRepository, config any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupDownload", reflect.TypeOf((*MockClient)(nil).BackupDownload), ctx, backupId, remoteRepository, config)
}
// BackupList mocks base method.
func (m *MockClient) BackupList(ctx context.Context, opt *arangodb.BackupListOptions) (arangodb.ListBackupsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupList", ctx, opt)
ret0, _ := ret[0].(arangodb.ListBackupsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BackupList indicates an expected call of BackupList.
func (mr *MockClientMockRecorder) BackupList(ctx, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupList", reflect.TypeOf((*MockClient)(nil).BackupList), ctx, opt)
}
// BackupRestore mocks base method.
func (m *MockClient) BackupRestore(ctx context.Context, id string) (arangodb.BackupRestoreResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupRestore", ctx, id)
ret0, _ := ret[0].(arangodb.BackupRestoreResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BackupRestore indicates an expected call of BackupRestore.
func (mr *MockClientMockRecorder) BackupRestore(ctx, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupRestore", reflect.TypeOf((*MockClient)(nil).BackupRestore), ctx, id)
}
// BackupUpload mocks base method.
func (m *MockClient) BackupUpload(ctx context.Context, backupId, remoteRepository string, config any) (arangodb.TransferMonitor, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BackupUpload", ctx, backupId, remoteRepository, config)
ret0, _ := ret[0].(arangodb.TransferMonitor)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BackupUpload indicates an expected call of BackupUpload.
func (mr *MockClientMockRecorder) BackupUpload(ctx, backupId, remoteRepository, config any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BackupUpload", reflect.TypeOf((*MockClient)(nil).BackupUpload), ctx, backupId, remoteRepository, config)
}
// CheckAvailability mocks base method.
func (m *MockClient) CheckAvailability(ctx context.Context, serverEndpoint string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CheckAvailability", ctx, serverEndpoint)
ret0, _ := ret[0].(error)
return ret0
}
// CheckAvailability indicates an expected call of CheckAvailability.
func (mr *MockClientMockRecorder) CheckAvailability(ctx, serverEndpoint any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAvailability", reflect.TypeOf((*MockClient)(nil).CheckAvailability), ctx, serverEndpoint)
}
// CleanOutServer mocks base method.
func (m *MockClient) CleanOutServer(ctx context.Context, serverID arangodb.ServerID) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CleanOutServer", ctx, serverID)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CleanOutServer indicates an expected call of CleanOutServer.
func (mr *MockClientMockRecorder) CleanOutServer(ctx, serverID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanOutServer", reflect.TypeOf((*MockClient)(nil).CleanOutServer), ctx, serverID)
}
// ClusterEndpoints mocks base method.
func (m *MockClient) ClusterEndpoints(ctx context.Context) (arangodb.ClusterEndpointsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterEndpoints", ctx)
ret0, _ := ret[0].(arangodb.ClusterEndpointsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ClusterEndpoints indicates an expected call of ClusterEndpoints.
func (mr *MockClientMockRecorder) ClusterEndpoints(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterEndpoints", reflect.TypeOf((*MockClient)(nil).ClusterEndpoints), ctx)
}
// ClusterStatistics mocks base method.
func (m *MockClient) ClusterStatistics(ctx context.Context, dbServer string) (arangodb.ClusterStatisticsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ClusterStatistics", ctx, dbServer)
ret0, _ := ret[0].(arangodb.ClusterStatisticsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ClusterStatistics indicates an expected call of ClusterStatistics.
func (mr *MockClientMockRecorder) ClusterStatistics(ctx, dbServer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ClusterStatistics", reflect.TypeOf((*MockClient)(nil).ClusterStatistics), ctx, dbServer)
}
// CommitFoxxService mocks base method.
func (m *MockClient) CommitFoxxService(ctx context.Context, dbName string, replace *bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CommitFoxxService", ctx, dbName, replace)
ret0, _ := ret[0].(error)
return ret0
}
// CommitFoxxService indicates an expected call of CommitFoxxService.
func (mr *MockClientMockRecorder) CommitFoxxService(ctx, dbName, replace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitFoxxService", reflect.TypeOf((*MockClient)(nil).CommitFoxxService), ctx, dbName, replace)
}
// CompactDatabases mocks base method.
func (m *MockClient) CompactDatabases(ctx context.Context, opts *arangodb.CompactOpts) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CompactDatabases", ctx, opts)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CompactDatabases indicates an expected call of CompactDatabases.
func (mr *MockClientMockRecorder) CompactDatabases(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompactDatabases", reflect.TypeOf((*MockClient)(nil).CompactDatabases), ctx, opts)
}
// ComputeAndExecuteClusterRebalance mocks base method.
func (m *MockClient) ComputeAndExecuteClusterRebalance(ctx context.Context, opts *arangodb.RebalanceRequestBody) (arangodb.RebalancePlan, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ComputeAndExecuteClusterRebalance", ctx, opts)
ret0, _ := ret[0].(arangodb.RebalancePlan)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ComputeAndExecuteClusterRebalance indicates an expected call of ComputeAndExecuteClusterRebalance.
func (mr *MockClientMockRecorder) ComputeAndExecuteClusterRebalance(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeAndExecuteClusterRebalance", reflect.TypeOf((*MockClient)(nil).ComputeAndExecuteClusterRebalance), ctx, opts)
}
// ComputeClusterRebalance mocks base method.
func (m *MockClient) ComputeClusterRebalance(ctx context.Context, opts *arangodb.RebalanceRequestBody) (arangodb.RebalancePlan, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ComputeClusterRebalance", ctx, opts)
ret0, _ := ret[0].(arangodb.RebalancePlan)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ComputeClusterRebalance indicates an expected call of ComputeClusterRebalance.
func (mr *MockClientMockRecorder) ComputeClusterRebalance(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ComputeClusterRebalance", reflect.TypeOf((*MockClient)(nil).ComputeClusterRebalance), ctx, opts)
}
// Connection mocks base method.
func (m *MockClient) Connection() connection.Connection {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Connection")
ret0, _ := ret[0].(connection.Connection)
return ret0
}
// Connection indicates an expected call of Connection.
func (mr *MockClientMockRecorder) Connection() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connection", reflect.TypeOf((*MockClient)(nil).Connection))
}
// CreateAccessToken mocks base method.
func (m *MockClient) CreateAccessToken(ctx context.Context, user *string, req arangodb.AccessTokenRequest) (arangodb.CreateAccessTokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateAccessToken", ctx, user, req)
ret0, _ := ret[0].(arangodb.CreateAccessTokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateAccessToken indicates an expected call of CreateAccessToken.
func (mr *MockClientMockRecorder) CreateAccessToken(ctx, user, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAccessToken", reflect.TypeOf((*MockClient)(nil).CreateAccessToken), ctx, user, req)
}
// CreateDatabase mocks base method.
func (m *MockClient) CreateDatabase(ctx context.Context, name string, options *arangodb.CreateDatabaseOptions) (arangodb.Database, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDatabase", ctx, name, options)
ret0, _ := ret[0].(arangodb.Database)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateDatabase indicates an expected call of CreateDatabase.
func (mr *MockClientMockRecorder) CreateDatabase(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDatabase", reflect.TypeOf((*MockClient)(nil).CreateDatabase), ctx, name, options)
}
// CreateNewBatch mocks base method.
func (m *MockClient) CreateNewBatch(ctx context.Context, dbName string, DBserver *string, state *bool, opt arangodb.CreateNewBatchOptions) (arangodb.CreateNewBatchResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNewBatch", ctx, dbName, DBserver, state, opt)
ret0, _ := ret[0].(arangodb.CreateNewBatchResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateNewBatch indicates an expected call of CreateNewBatch.
func (mr *MockClientMockRecorder) CreateNewBatch(ctx, dbName, DBserver, state, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNewBatch", reflect.TypeOf((*MockClient)(nil).CreateNewBatch), ctx, dbName, DBserver, state, opt)
}
// CreateTask mocks base method.
func (m *MockClient) CreateTask(ctx context.Context, databaseName string, options arangodb.TaskOptions) (arangodb.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTask", ctx, databaseName, options)
ret0, _ := ret[0].(arangodb.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateTask indicates an expected call of CreateTask.
func (mr *MockClientMockRecorder) CreateTask(ctx, databaseName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTask", reflect.TypeOf((*MockClient)(nil).CreateTask), ctx, databaseName, options)
}
// CreateTaskWithID mocks base method.
func (m *MockClient) CreateTaskWithID(ctx context.Context, databaseName, id string, options arangodb.TaskOptions) (arangodb.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTaskWithID", ctx, databaseName, id, options)
ret0, _ := ret[0].(arangodb.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateTaskWithID indicates an expected call of CreateTaskWithID.
func (mr *MockClientMockRecorder) CreateTaskWithID(ctx, databaseName, id, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTaskWithID", reflect.TypeOf((*MockClient)(nil).CreateTaskWithID), ctx, databaseName, id, options)
}
// CreateUser mocks base method.
func (m *MockClient) CreateUser(ctx context.Context, name string, options *arangodb.UserOptions) (arangodb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateUser", ctx, name, options)
ret0, _ := ret[0].(arangodb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateUser indicates an expected call of CreateUser.
func (mr *MockClientMockRecorder) CreateUser(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockClient)(nil).CreateUser), ctx, name, options)
}
// DatabaseExists mocks base method.
func (m *MockClient) DatabaseExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DatabaseExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DatabaseExists indicates an expected call of DatabaseExists.
func (mr *MockClientMockRecorder) DatabaseExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatabaseExists", reflect.TypeOf((*MockClient)(nil).DatabaseExists), ctx, name)
}
// DatabaseInventory mocks base method.
func (m *MockClient) DatabaseInventory(ctx context.Context, dbName string) (arangodb.DatabaseInventory, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DatabaseInventory", ctx, dbName)
ret0, _ := ret[0].(arangodb.DatabaseInventory)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DatabaseInventory indicates an expected call of DatabaseInventory.
func (mr *MockClientMockRecorder) DatabaseInventory(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DatabaseInventory", reflect.TypeOf((*MockClient)(nil).DatabaseInventory), ctx, dbName)
}
// Databases mocks base method.
func (m *MockClient) Databases(ctx context.Context) ([]arangodb.Database, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Databases", ctx)
ret0, _ := ret[0].([]arangodb.Database)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Databases indicates an expected call of Databases.
func (mr *MockClientMockRecorder) Databases(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Databases", reflect.TypeOf((*MockClient)(nil).Databases), ctx)
}
// Delete mocks base method.
func (m *MockClient) Delete(ctx context.Context, output any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockClientMockRecorder) Delete(ctx, output any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockClient)(nil).Delete), varargs...)
}
// DeleteAccessToken mocks base method.
func (m *MockClient) DeleteAccessToken(ctx context.Context, user *string, tokenId *int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAccessToken", ctx, user, tokenId)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAccessToken indicates an expected call of DeleteAccessToken.
func (mr *MockClientMockRecorder) DeleteAccessToken(ctx, user, tokenId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccessToken", reflect.TypeOf((*MockClient)(nil).DeleteAccessToken), ctx, user, tokenId)
}
// DeleteBatch mocks base method.
func (m *MockClient) DeleteBatch(ctx context.Context, dbName string, DBserver *string, batchId string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBatch", ctx, dbName, DBserver, batchId)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBatch indicates an expected call of DeleteBatch.
func (mr *MockClientMockRecorder) DeleteBatch(ctx, dbName, DBserver, batchId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBatch", reflect.TypeOf((*MockClient)(nil).DeleteBatch), ctx, dbName, DBserver, batchId)
}
// DeleteLogLevels mocks base method.
func (m *MockClient) DeleteLogLevels(ctx context.Context, serverId *string) (arangodb.LogLevelResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteLogLevels", ctx, serverId)
ret0, _ := ret[0].(arangodb.LogLevelResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteLogLevels indicates an expected call of DeleteLogLevels.
func (mr *MockClientMockRecorder) DeleteLogLevels(ctx, serverId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLogLevels", reflect.TypeOf((*MockClient)(nil).DeleteLogLevels), ctx, serverId)
}
// DisableDevelopmentMode mocks base method.
func (m *MockClient) DisableDevelopmentMode(ctx context.Context, dbName string, mount *string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DisableDevelopmentMode", ctx, dbName, mount)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DisableDevelopmentMode indicates an expected call of DisableDevelopmentMode.
func (mr *MockClientMockRecorder) DisableDevelopmentMode(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableDevelopmentMode", reflect.TypeOf((*MockClient)(nil).DisableDevelopmentMode), ctx, dbName, mount)
}
// DownloadFoxxServiceBundle mocks base method.
func (m *MockClient) DownloadFoxxServiceBundle(ctx context.Context, dbName string, mount *string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DownloadFoxxServiceBundle", ctx, dbName, mount)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DownloadFoxxServiceBundle indicates an expected call of DownloadFoxxServiceBundle.
func (mr *MockClientMockRecorder) DownloadFoxxServiceBundle(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadFoxxServiceBundle", reflect.TypeOf((*MockClient)(nil).DownloadFoxxServiceBundle), ctx, dbName, mount)
}
// Dump mocks base method.
func (m *MockClient) Dump(ctx context.Context, dbName string, params arangodb.ReplicationDumpParams) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dump", ctx, dbName, params)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Dump indicates an expected call of Dump.
func (mr *MockClientMockRecorder) Dump(ctx, dbName, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dump", reflect.TypeOf((*MockClient)(nil).Dump), ctx, dbName, params)
}
// EnableDevelopmentMode mocks base method.
func (m *MockClient) EnableDevelopmentMode(ctx context.Context, dbName string, mount *string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "EnableDevelopmentMode", ctx, dbName, mount)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// EnableDevelopmentMode indicates an expected call of EnableDevelopmentMode.
func (mr *MockClientMockRecorder) EnableDevelopmentMode(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableDevelopmentMode", reflect.TypeOf((*MockClient)(nil).EnableDevelopmentMode), ctx, dbName, mount)
}
// ExecuteAdminScript mocks base method.
func (m *MockClient) ExecuteAdminScript(ctx context.Context, dbName string, script *string) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteAdminScript", ctx, dbName, script)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecuteAdminScript indicates an expected call of ExecuteAdminScript.
func (mr *MockClientMockRecorder) ExecuteAdminScript(ctx, dbName, script any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteAdminScript", reflect.TypeOf((*MockClient)(nil).ExecuteAdminScript), ctx, dbName, script)
}
// ExecuteClusterRebalance mocks base method.
func (m *MockClient) ExecuteClusterRebalance(ctx context.Context, opts *arangodb.ExecuteRebalanceRequestBody) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteClusterRebalance", ctx, opts)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteClusterRebalance indicates an expected call of ExecuteClusterRebalance.
func (mr *MockClientMockRecorder) ExecuteClusterRebalance(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteClusterRebalance", reflect.TypeOf((*MockClient)(nil).ExecuteClusterRebalance), ctx, opts)
}
// ExtendBatch mocks base method.
func (m *MockClient) ExtendBatch(ctx context.Context, dbName string, DBserver *string, batchId string, opt arangodb.CreateNewBatchOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExtendBatch", ctx, dbName, DBserver, batchId, opt)
ret0, _ := ret[0].(error)
return ret0
}
// ExtendBatch indicates an expected call of ExtendBatch.
func (mr *MockClientMockRecorder) ExtendBatch(ctx, dbName, DBserver, batchId, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtendBatch", reflect.TypeOf((*MockClient)(nil).ExtendBatch), ctx, dbName, DBserver, batchId, opt)
}
// FetchRevisionDocuments mocks base method.
func (m *MockClient) FetchRevisionDocuments(ctx context.Context, dbName string, queryParams arangodb.RevisionQueryParams, opts []string) ([]map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchRevisionDocuments", ctx, dbName, queryParams, opts)
ret0, _ := ret[0].([]map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchRevisionDocuments indicates an expected call of FetchRevisionDocuments.
func (mr *MockClientMockRecorder) FetchRevisionDocuments(ctx, dbName, queryParams, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchRevisionDocuments", reflect.TypeOf((*MockClient)(nil).FetchRevisionDocuments), ctx, dbName, queryParams, opts)
}
// Get mocks base method.
func (m *MockClient) Get(ctx context.Context, output any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Get", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockClientMockRecorder) Get(ctx, output any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockClient)(nil).Get), varargs...)
}
// GetAllAccessToken mocks base method.
func (m *MockClient) GetAllAccessToken(ctx context.Context, user *string) (arangodb.AccessTokenResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAllAccessToken", ctx, user)
ret0, _ := ret[0].(arangodb.AccessTokenResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAllAccessToken indicates an expected call of GetAllAccessToken.
func (mr *MockClientMockRecorder) GetAllAccessToken(ctx, user any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllAccessToken", reflect.TypeOf((*MockClient)(nil).GetAllAccessToken), ctx, user)
}
// GetApplierConfig mocks base method.
func (m *MockClient) GetApplierConfig(ctx context.Context, dbName string, global *bool) (arangodb.ApplierConfigResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetApplierConfig", ctx, dbName, global)
ret0, _ := ret[0].(arangodb.ApplierConfigResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetApplierConfig indicates an expected call of GetApplierConfig.
func (mr *MockClientMockRecorder) GetApplierConfig(ctx, dbName, global any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplierConfig", reflect.TypeOf((*MockClient)(nil).GetApplierConfig), ctx, dbName, global)
}
// GetApplierState mocks base method.
func (m *MockClient) GetApplierState(ctx context.Context, dbName string, global *bool) (arangodb.ApplierStateResp, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetApplierState", ctx, dbName, global)
ret0, _ := ret[0].(arangodb.ApplierStateResp)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetApplierState indicates an expected call of GetApplierState.
func (mr *MockClientMockRecorder) GetApplierState(ctx, dbName, global any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetApplierState", reflect.TypeOf((*MockClient)(nil).GetApplierState), ctx, dbName, global)
}
// GetClusterRebalance mocks base method.
func (m *MockClient) GetClusterRebalance(ctx context.Context) (arangodb.RebalanceResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetClusterRebalance", ctx)
ret0, _ := ret[0].(arangodb.RebalanceResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetClusterRebalance indicates an expected call of GetClusterRebalance.
func (mr *MockClientMockRecorder) GetClusterRebalance(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterRebalance", reflect.TypeOf((*MockClient)(nil).GetClusterRebalance), ctx)
}
// GetDBServerMaintenance mocks base method.
func (m *MockClient) GetDBServerMaintenance(ctx context.Context, dbServer string) (arangodb.ClusterMaintenanceResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDBServerMaintenance", ctx, dbServer)
ret0, _ := ret[0].(arangodb.ClusterMaintenanceResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDBServerMaintenance indicates an expected call of GetDBServerMaintenance.
func (mr *MockClientMockRecorder) GetDBServerMaintenance(ctx, dbServer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBServerMaintenance", reflect.TypeOf((*MockClient)(nil).GetDBServerMaintenance), ctx, dbServer)
}
// GetDatabase mocks base method.
func (m *MockClient) GetDatabase(ctx context.Context, name string, options *arangodb.GetDatabaseOptions) (arangodb.Database, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDatabase", ctx, name, options)
ret0, _ := ret[0].(arangodb.Database)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDatabase indicates an expected call of GetDatabase.
func (mr *MockClientMockRecorder) GetDatabase(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDatabase", reflect.TypeOf((*MockClient)(nil).GetDatabase), ctx, name, options)
}
// GetDeploymentSupportInfo mocks base method.
func (m *MockClient) GetDeploymentSupportInfo(ctx context.Context) (arangodb.SupportInfoResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDeploymentSupportInfo", ctx)
ret0, _ := ret[0].(arangodb.SupportInfoResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDeploymentSupportInfo indicates an expected call of GetDeploymentSupportInfo.
func (mr *MockClientMockRecorder) GetDeploymentSupportInfo(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDeploymentSupportInfo", reflect.TypeOf((*MockClient)(nil).GetDeploymentSupportInfo), ctx)
}
// GetFoxxServiceConfiguration mocks base method.
func (m *MockClient) GetFoxxServiceConfiguration(ctx context.Context, dbName string, mount *string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFoxxServiceConfiguration", ctx, dbName, mount)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFoxxServiceConfiguration indicates an expected call of GetFoxxServiceConfiguration.
func (mr *MockClientMockRecorder) GetFoxxServiceConfiguration(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoxxServiceConfiguration", reflect.TypeOf((*MockClient)(nil).GetFoxxServiceConfiguration), ctx, dbName, mount)
}
// GetFoxxServiceDependencies mocks base method.
func (m *MockClient) GetFoxxServiceDependencies(ctx context.Context, dbName string, mount *string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFoxxServiceDependencies", ctx, dbName, mount)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFoxxServiceDependencies indicates an expected call of GetFoxxServiceDependencies.
func (mr *MockClientMockRecorder) GetFoxxServiceDependencies(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoxxServiceDependencies", reflect.TypeOf((*MockClient)(nil).GetFoxxServiceDependencies), ctx, dbName, mount)
}
// GetFoxxServiceReadme mocks base method.
func (m *MockClient) GetFoxxServiceReadme(ctx context.Context, dbName string, mount *string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFoxxServiceReadme", ctx, dbName, mount)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFoxxServiceReadme indicates an expected call of GetFoxxServiceReadme.
func (mr *MockClientMockRecorder) GetFoxxServiceReadme(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoxxServiceReadme", reflect.TypeOf((*MockClient)(nil).GetFoxxServiceReadme), ctx, dbName, mount)
}
// GetFoxxServiceScripts mocks base method.
func (m *MockClient) GetFoxxServiceScripts(ctx context.Context, dbName string, mount *string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFoxxServiceScripts", ctx, dbName, mount)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFoxxServiceScripts indicates an expected call of GetFoxxServiceScripts.
func (mr *MockClientMockRecorder) GetFoxxServiceScripts(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoxxServiceScripts", reflect.TypeOf((*MockClient)(nil).GetFoxxServiceScripts), ctx, dbName, mount)
}
// GetFoxxServiceSwagger mocks base method.
func (m *MockClient) GetFoxxServiceSwagger(ctx context.Context, dbName string, mount *string) (arangodb.SwaggerResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFoxxServiceSwagger", ctx, dbName, mount)
ret0, _ := ret[0].(arangodb.SwaggerResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetFoxxServiceSwagger indicates an expected call of GetFoxxServiceSwagger.
func (mr *MockClientMockRecorder) GetFoxxServiceSwagger(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFoxxServiceSwagger", reflect.TypeOf((*MockClient)(nil).GetFoxxServiceSwagger), ctx, dbName, mount)
}
// GetInstalledFoxxService mocks base method.
func (m *MockClient) GetInstalledFoxxService(ctx context.Context, dbName string, mount *string) (arangodb.FoxxServiceObject, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInstalledFoxxService", ctx, dbName, mount)
ret0, _ := ret[0].(arangodb.FoxxServiceObject)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetInstalledFoxxService indicates an expected call of GetInstalledFoxxService.
func (mr *MockClientMockRecorder) GetInstalledFoxxService(ctx, dbName, mount any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInstalledFoxxService", reflect.TypeOf((*MockClient)(nil).GetInstalledFoxxService), ctx, dbName, mount)
}
// GetInventory mocks base method.
func (m *MockClient) GetInventory(ctx context.Context, dbName string, params arangodb.InventoryQueryParams) (arangodb.InventoryResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInventory", ctx, dbName, params)
ret0, _ := ret[0].(arangodb.InventoryResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetInventory indicates an expected call of GetInventory.
func (mr *MockClientMockRecorder) GetInventory(ctx, dbName, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInventory", reflect.TypeOf((*MockClient)(nil).GetInventory), ctx, dbName, params)
}
// GetJWTSecrets mocks base method.
func (m *MockClient) GetJWTSecrets(ctx context.Context, dbName string) (arangodb.JWTSecretsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetJWTSecrets", ctx, dbName)
ret0, _ := ret[0].(arangodb.JWTSecretsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetJWTSecrets indicates an expected call of GetJWTSecrets.
func (mr *MockClientMockRecorder) GetJWTSecrets(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJWTSecrets", reflect.TypeOf((*MockClient)(nil).GetJWTSecrets), ctx, dbName)
}
// GetLicense mocks base method.
func (m *MockClient) GetLicense(ctx context.Context) (arangodb.License, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLicense", ctx)
ret0, _ := ret[0].(arangodb.License)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLicense indicates an expected call of GetLicense.
func (mr *MockClientMockRecorder) GetLicense(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLicense", reflect.TypeOf((*MockClient)(nil).GetLicense), ctx)
}
// GetLogLevels mocks base method.
func (m *MockClient) GetLogLevels(ctx context.Context, opts *arangodb.LogLevelsGetOptions) (arangodb.LogLevels, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLogLevels", ctx, opts)
ret0, _ := ret[0].(arangodb.LogLevels)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLogLevels indicates an expected call of GetLogLevels.
func (mr *MockClientMockRecorder) GetLogLevels(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLogLevels", reflect.TypeOf((*MockClient)(nil).GetLogLevels), ctx, opts)
}
// GetMetrics mocks base method.
func (m *MockClient) GetMetrics(ctx context.Context, dbName string, serverId *string) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMetrics", ctx, dbName, serverId)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMetrics indicates an expected call of GetMetrics.
func (mr *MockClientMockRecorder) GetMetrics(ctx, dbName, serverId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMetrics", reflect.TypeOf((*MockClient)(nil).GetMetrics), ctx, dbName, serverId)
}
// GetRecentAPICalls mocks base method.
func (m *MockClient) GetRecentAPICalls(ctx context.Context, dbName string) (arangodb.ApiCallsResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRecentAPICalls", ctx, dbName)
ret0, _ := ret[0].(arangodb.ApiCallsResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRecentAPICalls indicates an expected call of GetRecentAPICalls.
func (mr *MockClientMockRecorder) GetRecentAPICalls(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRecentAPICalls", reflect.TypeOf((*MockClient)(nil).GetRecentAPICalls), ctx, dbName)
}
// GetReplicationServerId mocks base method.
func (m *MockClient) GetReplicationServerId(ctx context.Context, dbName string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetReplicationServerId", ctx, dbName)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetReplicationServerId indicates an expected call of GetReplicationServerId.
func (mr *MockClientMockRecorder) GetReplicationServerId(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReplicationServerId", reflect.TypeOf((*MockClient)(nil).GetReplicationServerId), ctx, dbName)
}
// GetServerStatus mocks base method.
func (m *MockClient) GetServerStatus(ctx context.Context, dbName string) (arangodb.ServerStatusResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetServerStatus", ctx, dbName)
ret0, _ := ret[0].(arangodb.ServerStatusResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetServerStatus indicates an expected call of GetServerStatus.
func (mr *MockClientMockRecorder) GetServerStatus(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServerStatus", reflect.TypeOf((*MockClient)(nil).GetServerStatus), ctx, dbName)
}
// GetShardRevisionTree mocks base method.
func (m *MockClient) GetShardRevisionTree(ctx context.Context, dbName string, shardID arangodb.ShardID, batchId string) (json.RawMessage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetShardRevisionTree", ctx, dbName, shardID, batchId)
ret0, _ := ret[0].(json.RawMessage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetShardRevisionTree indicates an expected call of GetShardRevisionTree.
func (mr *MockClientMockRecorder) GetShardRevisionTree(ctx, dbName, shardID, batchId any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetShardRevisionTree", reflect.TypeOf((*MockClient)(nil).GetShardRevisionTree), ctx, dbName, shardID, batchId)
}
// GetStartupConfiguration mocks base method.
func (m *MockClient) GetStartupConfiguration(ctx context.Context) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStartupConfiguration", ctx)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStartupConfiguration indicates an expected call of GetStartupConfiguration.
func (mr *MockClientMockRecorder) GetStartupConfiguration(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStartupConfiguration", reflect.TypeOf((*MockClient)(nil).GetStartupConfiguration), ctx)
}
// GetStartupConfigurationDescription mocks base method.
func (m *MockClient) GetStartupConfigurationDescription(ctx context.Context) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStartupConfigurationDescription", ctx)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStartupConfigurationDescription indicates an expected call of GetStartupConfigurationDescription.
func (mr *MockClientMockRecorder) GetStartupConfigurationDescription(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStartupConfigurationDescription", reflect.TypeOf((*MockClient)(nil).GetStartupConfigurationDescription), ctx)
}
// GetStructuredLogSettings mocks base method.
func (m *MockClient) GetStructuredLogSettings(ctx context.Context) (arangodb.LogSettingsOptions, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetStructuredLogSettings", ctx)
ret0, _ := ret[0].(arangodb.LogSettingsOptions)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetStructuredLogSettings indicates an expected call of GetStructuredLogSettings.
func (mr *MockClientMockRecorder) GetStructuredLogSettings(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStructuredLogSettings", reflect.TypeOf((*MockClient)(nil).GetStructuredLogSettings), ctx)
}
// GetSystemTime mocks base method.
func (m *MockClient) GetSystemTime(ctx context.Context, dbName string) (float64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetSystemTime", ctx, dbName)
ret0, _ := ret[0].(float64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetSystemTime indicates an expected call of GetSystemTime.
func (mr *MockClientMockRecorder) GetSystemTime(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSystemTime", reflect.TypeOf((*MockClient)(nil).GetSystemTime), ctx, dbName)
}
// GetTLSData mocks base method.
func (m *MockClient) GetTLSData(ctx context.Context, dbName string) (arangodb.TLSDataResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetTLSData", ctx, dbName)
ret0, _ := ret[0].(arangodb.TLSDataResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetTLSData indicates an expected call of GetTLSData.
func (mr *MockClientMockRecorder) GetTLSData(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTLSData", reflect.TypeOf((*MockClient)(nil).GetTLSData), ctx, dbName)
}
// GetWALLastTick mocks base method.
func (m *MockClient) GetWALLastTick(ctx context.Context, dbName string) (arangodb.WALLastTickResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWALLastTick", ctx, dbName)
ret0, _ := ret[0].(arangodb.WALLastTickResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWALLastTick indicates an expected call of GetWALLastTick.
func (mr *MockClientMockRecorder) GetWALLastTick(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWALLastTick", reflect.TypeOf((*MockClient)(nil).GetWALLastTick), ctx, dbName)
}
// GetWALRange mocks base method.
func (m *MockClient) GetWALRange(ctx context.Context, dbName string) (arangodb.WALRangeResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWALRange", ctx, dbName)
ret0, _ := ret[0].(arangodb.WALRangeResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWALRange indicates an expected call of GetWALRange.
func (mr *MockClientMockRecorder) GetWALRange(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWALRange", reflect.TypeOf((*MockClient)(nil).GetWALRange), ctx, dbName)
}
// GetWALTail mocks base method.
func (m *MockClient) GetWALTail(ctx context.Context, dbName string, params *arangodb.WALTailOptions) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWALTail", ctx, dbName, params)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWALTail indicates an expected call of GetWALTail.
func (mr *MockClientMockRecorder) GetWALTail(ctx, dbName, params any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWALTail", reflect.TypeOf((*MockClient)(nil).GetWALTail), ctx, dbName, params)
}
// HandleAdminVersion mocks base method.
func (m *MockClient) HandleAdminVersion(ctx context.Context, opts *arangodb.GetVersionOptions) (arangodb.VersionInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HandleAdminVersion", ctx, opts)
ret0, _ := ret[0].(arangodb.VersionInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HandleAdminVersion indicates an expected call of HandleAdminVersion.
func (mr *MockClientMockRecorder) HandleAdminVersion(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleAdminVersion", reflect.TypeOf((*MockClient)(nil).HandleAdminVersion), ctx, opts)
}
// Head mocks base method.
func (m *MockClient) Head(ctx context.Context, output any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Head", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Head indicates an expected call of Head.
func (mr *MockClientMockRecorder) Head(ctx, output any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Head", reflect.TypeOf((*MockClient)(nil).Head), varargs...)
}
// Health mocks base method.
func (m *MockClient) Health(ctx context.Context) (arangodb.ClusterHealth, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health", ctx)
ret0, _ := ret[0].(arangodb.ClusterHealth)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Health indicates an expected call of Health.
func (mr *MockClientMockRecorder) Health(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockClient)(nil).Health), ctx)
}
// InstallFoxxService mocks base method.
func (m *MockClient) InstallFoxxService(ctx context.Context, dbName, zipFile string, options *arangodb.FoxxDeploymentOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InstallFoxxService", ctx, dbName, zipFile, options)
ret0, _ := ret[0].(error)
return ret0
}
// InstallFoxxService indicates an expected call of InstallFoxxService.
func (mr *MockClientMockRecorder) InstallFoxxService(ctx, dbName, zipFile, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstallFoxxService", reflect.TypeOf((*MockClient)(nil).InstallFoxxService), ctx, dbName, zipFile, options)
}
// IsCleanedOut mocks base method.
func (m *MockClient) IsCleanedOut(ctx context.Context, serverID arangodb.ServerID) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsCleanedOut", ctx, serverID)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IsCleanedOut indicates an expected call of IsCleanedOut.
func (mr *MockClientMockRecorder) IsCleanedOut(ctx, serverID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCleanedOut", reflect.TypeOf((*MockClient)(nil).IsCleanedOut), ctx, serverID)
}
// ListDocumentRevisionsInRange mocks base method.
func (m *MockClient) ListDocumentRevisionsInRange(ctx context.Context, dbName string, queryParams arangodb.RevisionQueryParams, opts [][2]string) ([][2]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListDocumentRevisionsInRange", ctx, dbName, queryParams, opts)
ret0, _ := ret[0].([][2]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListDocumentRevisionsInRange indicates an expected call of ListDocumentRevisionsInRange.
func (mr *MockClientMockRecorder) ListDocumentRevisionsInRange(ctx, dbName, queryParams, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDocumentRevisionsInRange", reflect.TypeOf((*MockClient)(nil).ListDocumentRevisionsInRange), ctx, dbName, queryParams, opts)
}
// ListInstalledFoxxServices mocks base method.
func (m *MockClient) ListInstalledFoxxServices(ctx context.Context, dbName string, excludeSystem *bool) ([]arangodb.FoxxServiceListItem, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListInstalledFoxxServices", ctx, dbName, excludeSystem)
ret0, _ := ret[0].([]arangodb.FoxxServiceListItem)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListInstalledFoxxServices indicates an expected call of ListInstalledFoxxServices.
func (mr *MockClientMockRecorder) ListInstalledFoxxServices(ctx, dbName, excludeSystem any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListInstalledFoxxServices", reflect.TypeOf((*MockClient)(nil).ListInstalledFoxxServices), ctx, dbName, excludeSystem)
}
// LoggerFirstTick mocks base method.
func (m *MockClient) LoggerFirstTick(ctx context.Context, dbName string) (arangodb.LoggerFirstTickResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoggerFirstTick", ctx, dbName)
ret0, _ := ret[0].(arangodb.LoggerFirstTickResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoggerFirstTick indicates an expected call of LoggerFirstTick.
func (mr *MockClientMockRecorder) LoggerFirstTick(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoggerFirstTick", reflect.TypeOf((*MockClient)(nil).LoggerFirstTick), ctx, dbName)
}
// LoggerState mocks base method.
func (m *MockClient) LoggerState(ctx context.Context, dbName string, DBserver *string) (arangodb.LoggerStateResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoggerState", ctx, dbName, DBserver)
ret0, _ := ret[0].(arangodb.LoggerStateResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoggerState indicates an expected call of LoggerState.
func (mr *MockClientMockRecorder) LoggerState(ctx, dbName, DBserver any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoggerState", reflect.TypeOf((*MockClient)(nil).LoggerState), ctx, dbName, DBserver)
}
// LoggerTickRange mocks base method.
func (m *MockClient) LoggerTickRange(ctx context.Context, dbName string) ([]arangodb.LoggerTickRangeResponseObj, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoggerTickRange", ctx, dbName)
ret0, _ := ret[0].([]arangodb.LoggerTickRangeResponseObj)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LoggerTickRange indicates an expected call of LoggerTickRange.
func (mr *MockClientMockRecorder) LoggerTickRange(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoggerTickRange", reflect.TypeOf((*MockClient)(nil).LoggerTickRange), ctx, dbName)
}
// Logs mocks base method.
func (m *MockClient) Logs(ctx context.Context, queryParams *arangodb.AdminLogEntriesOptions) (arangodb.AdminLogEntriesResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Logs", ctx, queryParams)
ret0, _ := ret[0].(arangodb.AdminLogEntriesResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Logs indicates an expected call of Logs.
func (mr *MockClientMockRecorder) Logs(ctx, queryParams any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logs", reflect.TypeOf((*MockClient)(nil).Logs), ctx, queryParams)
}
// MakeFollower mocks base method.
func (m *MockClient) MakeFollower(ctx context.Context, dbName string, opts arangodb.ApplierOptions) (arangodb.ApplierStateResp, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MakeFollower", ctx, dbName, opts)
ret0, _ := ret[0].(arangodb.ApplierStateResp)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MakeFollower indicates an expected call of MakeFollower.
func (mr *MockClientMockRecorder) MakeFollower(ctx, dbName, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MakeFollower", reflect.TypeOf((*MockClient)(nil).MakeFollower), ctx, dbName, opts)
}
// MoveShard mocks base method.
func (m *MockClient) MoveShard(ctx context.Context, col arangodb.Collection, shard arangodb.ShardID, fromServer, toServer arangodb.ServerID) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MoveShard", ctx, col, shard, fromServer, toServer)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MoveShard indicates an expected call of MoveShard.
func (mr *MockClientMockRecorder) MoveShard(ctx, col, shard, fromServer, toServer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveShard", reflect.TypeOf((*MockClient)(nil).MoveShard), ctx, col, shard, fromServer, toServer)
}
// NumberOfServers mocks base method.
func (m *MockClient) NumberOfServers(ctx context.Context) (arangodb.NumberOfServersResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NumberOfServers", ctx)
ret0, _ := ret[0].(arangodb.NumberOfServersResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NumberOfServers indicates an expected call of NumberOfServers.
func (mr *MockClientMockRecorder) NumberOfServers(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumberOfServers", reflect.TypeOf((*MockClient)(nil).NumberOfServers), ctx)
}
// Patch mocks base method.
func (m *MockClient) Patch(ctx context.Context, output, input any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output, input}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Patch", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockClientMockRecorder) Patch(ctx, output, input any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output, input}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockClient)(nil).Patch), varargs...)
}
// Post mocks base method.
func (m *MockClient) Post(ctx context.Context, output, input any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output, input}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Post", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Post indicates an expected call of Post.
func (mr *MockClientMockRecorder) Post(ctx, output, input any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output, input}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockClient)(nil).Post), varargs...)
}
// Put mocks base method.
func (m *MockClient) Put(ctx context.Context, output, input any, urlParts ...string) (connection.Response, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, output, input}
for _, a := range urlParts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Put", varargs...)
ret0, _ := ret[0].(connection.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Put indicates an expected call of Put.
func (mr *MockClientMockRecorder) Put(ctx, output, input any, urlParts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, output, input}, urlParts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockClient)(nil).Put), varargs...)
}
// RebuildShardRevisionTree mocks base method.
func (m *MockClient) RebuildShardRevisionTree(ctx context.Context, dbName string, shardID arangodb.ShardID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RebuildShardRevisionTree", ctx, dbName, shardID)
ret0, _ := ret[0].(error)
return ret0
}
// RebuildShardRevisionTree indicates an expected call of RebuildShardRevisionTree.
func (mr *MockClientMockRecorder) RebuildShardRevisionTree(ctx, dbName, shardID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RebuildShardRevisionTree", reflect.TypeOf((*MockClient)(nil).RebuildShardRevisionTree), ctx, dbName, shardID)
}
// ReloadJWTSecrets mocks base method.
func (m *MockClient) ReloadJWTSecrets(ctx context.Context) (arangodb.JWTSecretsResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReloadJWTSecrets", ctx)
ret0, _ := ret[0].(arangodb.JWTSecretsResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReloadJWTSecrets indicates an expected call of ReloadJWTSecrets.
func (mr *MockClientMockRecorder) ReloadJWTSecrets(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadJWTSecrets", reflect.TypeOf((*MockClient)(nil).ReloadJWTSecrets), ctx)
}
// ReloadRoutingTable mocks base method.
func (m *MockClient) ReloadRoutingTable(ctx context.Context, dbName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReloadRoutingTable", ctx, dbName)
ret0, _ := ret[0].(error)
return ret0
}
// ReloadRoutingTable indicates an expected call of ReloadRoutingTable.
func (mr *MockClientMockRecorder) ReloadRoutingTable(ctx, dbName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadRoutingTable", reflect.TypeOf((*MockClient)(nil).ReloadRoutingTable), ctx, dbName)
}
// ReloadTLSData mocks base method.
func (m *MockClient) ReloadTLSData(ctx context.Context) (arangodb.TLSDataResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReloadTLSData", ctx)
ret0, _ := ret[0].(arangodb.TLSDataResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReloadTLSData indicates an expected call of ReloadTLSData.
func (mr *MockClientMockRecorder) ReloadTLSData(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadTLSData", reflect.TypeOf((*MockClient)(nil).ReloadTLSData), ctx)
}
// RemoveServer mocks base method.
func (m *MockClient) RemoveServer(ctx context.Context, serverID arangodb.ServerID) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveServer", ctx, serverID)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveServer indicates an expected call of RemoveServer.
func (mr *MockClientMockRecorder) RemoveServer(ctx, serverID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveServer", reflect.TypeOf((*MockClient)(nil).RemoveServer), ctx, serverID)
}
// RemoveTask mocks base method.
func (m *MockClient) RemoveTask(ctx context.Context, databaseName, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveTask", ctx, databaseName, id)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveTask indicates an expected call of RemoveTask.
func (mr *MockClientMockRecorder) RemoveTask(ctx, databaseName, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTask", reflect.TypeOf((*MockClient)(nil).RemoveTask), ctx, databaseName, id)
}
// RemoveUser mocks base method.
func (m *MockClient) RemoveUser(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveUser", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveUser indicates an expected call of RemoveUser.
func (mr *MockClientMockRecorder) RemoveUser(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveUser", reflect.TypeOf((*MockClient)(nil).RemoveUser), ctx, name)
}
// ReplaceFoxxService mocks base method.
func (m *MockClient) ReplaceFoxxService(ctx context.Context, dbName, zipFile string, opts *arangodb.FoxxDeploymentOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceFoxxService", ctx, dbName, zipFile, opts)
ret0, _ := ret[0].(error)
return ret0
}
// ReplaceFoxxService indicates an expected call of ReplaceFoxxService.
func (mr *MockClientMockRecorder) ReplaceFoxxService(ctx, dbName, zipFile, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceFoxxService", reflect.TypeOf((*MockClient)(nil).ReplaceFoxxService), ctx, dbName, zipFile, opts)
}
// ReplaceFoxxServiceConfiguration mocks base method.
func (m *MockClient) ReplaceFoxxServiceConfiguration(ctx context.Context, dbName string, mount *string, opt map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceFoxxServiceConfiguration", ctx, dbName, mount, opt)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceFoxxServiceConfiguration indicates an expected call of ReplaceFoxxServiceConfiguration.
func (mr *MockClientMockRecorder) ReplaceFoxxServiceConfiguration(ctx, dbName, mount, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceFoxxServiceConfiguration", reflect.TypeOf((*MockClient)(nil).ReplaceFoxxServiceConfiguration), ctx, dbName, mount, opt)
}
// ReplaceFoxxServiceDependencies mocks base method.
func (m *MockClient) ReplaceFoxxServiceDependencies(ctx context.Context, dbName string, mount *string, opt map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceFoxxServiceDependencies", ctx, dbName, mount, opt)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceFoxxServiceDependencies indicates an expected call of ReplaceFoxxServiceDependencies.
func (mr *MockClientMockRecorder) ReplaceFoxxServiceDependencies(ctx, dbName, mount, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceFoxxServiceDependencies", reflect.TypeOf((*MockClient)(nil).ReplaceFoxxServiceDependencies), ctx, dbName, mount, opt)
}
// ReplaceUser mocks base method.
func (m *MockClient) ReplaceUser(ctx context.Context, name string, options *arangodb.UserOptions) (arangodb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReplaceUser", ctx, name, options)
ret0, _ := ret[0].(arangodb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReplaceUser indicates an expected call of ReplaceUser.
func (mr *MockClientMockRecorder) ReplaceUser(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceUser", reflect.TypeOf((*MockClient)(nil).ReplaceUser), ctx, name, options)
}
// ResignServer mocks base method.
func (m *MockClient) ResignServer(ctx context.Context, serverID arangodb.ServerID) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResignServer", ctx, serverID)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResignServer indicates an expected call of ResignServer.
func (mr *MockClientMockRecorder) ResignServer(ctx, serverID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResignServer", reflect.TypeOf((*MockClient)(nil).ResignServer), ctx, serverID)
}
// RotateEncryptionAtRestKey mocks base method.
func (m *MockClient) RotateEncryptionAtRestKey(ctx context.Context) ([]arangodb.EncryptionKey, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RotateEncryptionAtRestKey", ctx)
ret0, _ := ret[0].([]arangodb.EncryptionKey)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RotateEncryptionAtRestKey indicates an expected call of RotateEncryptionAtRestKey.
func (mr *MockClientMockRecorder) RotateEncryptionAtRestKey(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RotateEncryptionAtRestKey", reflect.TypeOf((*MockClient)(nil).RotateEncryptionAtRestKey), ctx)
}
// RunFoxxServiceScript mocks base method.
func (m *MockClient) RunFoxxServiceScript(ctx context.Context, dbName, name string, mount *string, body map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunFoxxServiceScript", ctx, dbName, name, mount, body)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunFoxxServiceScript indicates an expected call of RunFoxxServiceScript.
func (mr *MockClientMockRecorder) RunFoxxServiceScript(ctx, dbName, name, mount, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunFoxxServiceScript", reflect.TypeOf((*MockClient)(nil).RunFoxxServiceScript), ctx, dbName, name, mount, body)
}
// RunFoxxServiceTests mocks base method.
func (m *MockClient) RunFoxxServiceTests(ctx context.Context, dbName string, opt arangodb.FoxxTestOptions) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunFoxxServiceTests", ctx, dbName, opt)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunFoxxServiceTests indicates an expected call of RunFoxxServiceTests.
func (mr *MockClientMockRecorder) RunFoxxServiceTests(ctx, dbName, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunFoxxServiceTests", reflect.TypeOf((*MockClient)(nil).RunFoxxServiceTests), ctx, dbName, opt)
}
// ServerID mocks base method.
func (m *MockClient) ServerID(ctx context.Context) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerID", ctx)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServerID indicates an expected call of ServerID.
func (mr *MockClientMockRecorder) ServerID(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerID", reflect.TypeOf((*MockClient)(nil).ServerID), ctx)
}
// ServerMode mocks base method.
func (m *MockClient) ServerMode(ctx context.Context) (arangodb.ServerMode, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerMode", ctx)
ret0, _ := ret[0].(arangodb.ServerMode)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServerMode indicates an expected call of ServerMode.
func (mr *MockClientMockRecorder) ServerMode(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerMode", reflect.TypeOf((*MockClient)(nil).ServerMode), ctx)
}
// ServerRole mocks base method.
func (m *MockClient) ServerRole(ctx context.Context) (arangodb.ServerRole, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ServerRole", ctx)
ret0, _ := ret[0].(arangodb.ServerRole)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ServerRole indicates an expected call of ServerRole.
func (mr *MockClientMockRecorder) ServerRole(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ServerRole", reflect.TypeOf((*MockClient)(nil).ServerRole), ctx)
}
// SetClusterMaintenance mocks base method.
func (m *MockClient) SetClusterMaintenance(ctx context.Context, mode *string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetClusterMaintenance", ctx, mode)
ret0, _ := ret[0].(error)
return ret0
}
// SetClusterMaintenance indicates an expected call of SetClusterMaintenance.
func (mr *MockClientMockRecorder) SetClusterMaintenance(ctx, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetClusterMaintenance", reflect.TypeOf((*MockClient)(nil).SetClusterMaintenance), ctx, mode)
}
// SetDBServerMaintenance mocks base method.
func (m *MockClient) SetDBServerMaintenance(ctx context.Context, dbServer string, opts *arangodb.ClusterMaintenanceOpts) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDBServerMaintenance", ctx, dbServer, opts)
ret0, _ := ret[0].(error)
return ret0
}
// SetDBServerMaintenance indicates an expected call of SetDBServerMaintenance.
func (mr *MockClientMockRecorder) SetDBServerMaintenance(ctx, dbServer, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDBServerMaintenance", reflect.TypeOf((*MockClient)(nil).SetDBServerMaintenance), ctx, dbServer, opts)
}
// SetLicense mocks base method.
func (m *MockClient) SetLicense(ctx context.Context, license string, force bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLicense", ctx, license, force)
ret0, _ := ret[0].(error)
return ret0
}
// SetLicense indicates an expected call of SetLicense.
func (mr *MockClientMockRecorder) SetLicense(ctx, license, force any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLicense", reflect.TypeOf((*MockClient)(nil).SetLicense), ctx, license, force)
}
// SetLogLevels mocks base method.
func (m *MockClient) SetLogLevels(ctx context.Context, logLevels arangodb.LogLevels, opts *arangodb.LogLevelsSetOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetLogLevels", ctx, logLevels, opts)
ret0, _ := ret[0].(error)
return ret0
}
// SetLogLevels indicates an expected call of SetLogLevels.
func (mr *MockClientMockRecorder) SetLogLevels(ctx, logLevels, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLogLevels", reflect.TypeOf((*MockClient)(nil).SetLogLevels), ctx, logLevels, opts)
}
// SetServerMode mocks base method.
func (m *MockClient) SetServerMode(ctx context.Context, mode arangodb.ServerMode) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetServerMode", ctx, mode)
ret0, _ := ret[0].(error)
return ret0
}
// SetServerMode indicates an expected call of SetServerMode.
func (mr *MockClientMockRecorder) SetServerMode(ctx, mode any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetServerMode", reflect.TypeOf((*MockClient)(nil).SetServerMode), ctx, mode)
}
// StartReplicationSync mocks base method.
func (m *MockClient) StartReplicationSync(ctx context.Context, dbName string, opts arangodb.ReplicationSyncOptions) (arangodb.ReplicationSyncResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartReplicationSync", ctx, dbName, opts)
ret0, _ := ret[0].(arangodb.ReplicationSyncResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StartReplicationSync indicates an expected call of StartReplicationSync.
func (mr *MockClientMockRecorder) StartReplicationSync(ctx, dbName, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartReplicationSync", reflect.TypeOf((*MockClient)(nil).StartReplicationSync), ctx, dbName, opts)
}
// Task mocks base method.
func (m *MockClient) Task(ctx context.Context, databaseName, id string) (arangodb.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Task", ctx, databaseName, id)
ret0, _ := ret[0].(arangodb.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Task indicates an expected call of Task.
func (mr *MockClientMockRecorder) Task(ctx, databaseName, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Task", reflect.TypeOf((*MockClient)(nil).Task), ctx, databaseName, id)
}
// Tasks mocks base method.
func (m *MockClient) Tasks(ctx context.Context, databaseName string) ([]arangodb.Task, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Tasks", ctx, databaseName)
ret0, _ := ret[0].([]arangodb.Task)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Tasks indicates an expected call of Tasks.
func (mr *MockClientMockRecorder) Tasks(ctx, databaseName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tasks", reflect.TypeOf((*MockClient)(nil).Tasks), ctx, databaseName)
}
// TransferMonitor mocks base method.
func (m *MockClient) TransferMonitor(jobId string, transferType arangodb.TransferType) (arangodb.TransferMonitor, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TransferMonitor", jobId, transferType)
ret0, _ := ret[0].(arangodb.TransferMonitor)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// TransferMonitor indicates an expected call of TransferMonitor.
func (mr *MockClientMockRecorder) TransferMonitor(jobId, transferType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransferMonitor", reflect.TypeOf((*MockClient)(nil).TransferMonitor), jobId, transferType)
}
// UninstallFoxxService mocks base method.
func (m *MockClient) UninstallFoxxService(ctx context.Context, dbName string, options *arangodb.FoxxDeleteOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UninstallFoxxService", ctx, dbName, options)
ret0, _ := ret[0].(error)
return ret0
}
// UninstallFoxxService indicates an expected call of UninstallFoxxService.
func (mr *MockClientMockRecorder) UninstallFoxxService(ctx, dbName, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UninstallFoxxService", reflect.TypeOf((*MockClient)(nil).UninstallFoxxService), ctx, dbName, options)
}
// UpdateApplierConfig mocks base method.
func (m *MockClient) UpdateApplierConfig(ctx context.Context, dbName string, global *bool, opts arangodb.ApplierOptions) (arangodb.ApplierConfigResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateApplierConfig", ctx, dbName, global, opts)
ret0, _ := ret[0].(arangodb.ApplierConfigResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateApplierConfig indicates an expected call of UpdateApplierConfig.
func (mr *MockClientMockRecorder) UpdateApplierConfig(ctx, dbName, global, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateApplierConfig", reflect.TypeOf((*MockClient)(nil).UpdateApplierConfig), ctx, dbName, global, opts)
}
// UpdateFoxxServiceConfiguration mocks base method.
func (m *MockClient) UpdateFoxxServiceConfiguration(ctx context.Context, dbName string, mount *string, opt map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateFoxxServiceConfiguration", ctx, dbName, mount, opt)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateFoxxServiceConfiguration indicates an expected call of UpdateFoxxServiceConfiguration.
func (mr *MockClientMockRecorder) UpdateFoxxServiceConfiguration(ctx, dbName, mount, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFoxxServiceConfiguration", reflect.TypeOf((*MockClient)(nil).UpdateFoxxServiceConfiguration), ctx, dbName, mount, opt)
}
// UpdateFoxxServiceDependencies mocks base method.
func (m *MockClient) UpdateFoxxServiceDependencies(ctx context.Context, dbName string, mount *string, opt map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateFoxxServiceDependencies", ctx, dbName, mount, opt)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateFoxxServiceDependencies indicates an expected call of UpdateFoxxServiceDependencies.
func (mr *MockClientMockRecorder) UpdateFoxxServiceDependencies(ctx, dbName, mount, opt any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFoxxServiceDependencies", reflect.TypeOf((*MockClient)(nil).UpdateFoxxServiceDependencies), ctx, dbName, mount, opt)
}
// UpdateStructuredLogSettings mocks base method.
func (m *MockClient) UpdateStructuredLogSettings(ctx context.Context, opts *arangodb.LogSettingsOptions) (arangodb.LogSettingsOptions, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateStructuredLogSettings", ctx, opts)
ret0, _ := ret[0].(arangodb.LogSettingsOptions)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateStructuredLogSettings indicates an expected call of UpdateStructuredLogSettings.
func (mr *MockClientMockRecorder) UpdateStructuredLogSettings(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStructuredLogSettings", reflect.TypeOf((*MockClient)(nil).UpdateStructuredLogSettings), ctx, opts)
}
// UpdateUser mocks base method.
func (m *MockClient) UpdateUser(ctx context.Context, name string, options *arangodb.UserOptions) (arangodb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateUser", ctx, name, options)
ret0, _ := ret[0].(arangodb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateUser indicates an expected call of UpdateUser.
func (mr *MockClientMockRecorder) UpdateUser(ctx, name, options any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockClient)(nil).UpdateUser), ctx, name, options)
}
// UpgradeFoxxService mocks base method.
func (m *MockClient) UpgradeFoxxService(ctx context.Context, dbName, zipFile string, opts *arangodb.FoxxDeploymentOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpgradeFoxxService", ctx, dbName, zipFile, opts)
ret0, _ := ret[0].(error)
return ret0
}
// UpgradeFoxxService indicates an expected call of UpgradeFoxxService.
func (mr *MockClientMockRecorder) UpgradeFoxxService(ctx, dbName, zipFile, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpgradeFoxxService", reflect.TypeOf((*MockClient)(nil).UpgradeFoxxService), ctx, dbName, zipFile, opts)
}
// User mocks base method.
func (m *MockClient) User(ctx context.Context, name string) (arangodb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "User", ctx, name)
ret0, _ := ret[0].(arangodb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// User indicates an expected call of User.
func (mr *MockClientMockRecorder) User(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "User", reflect.TypeOf((*MockClient)(nil).User), ctx, name)
}
// UserExists mocks base method.
func (m *MockClient) UserExists(ctx context.Context, name string) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UserExists", ctx, name)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UserExists indicates an expected call of UserExists.
func (mr *MockClientMockRecorder) UserExists(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UserExists", reflect.TypeOf((*MockClient)(nil).UserExists), ctx, name)
}
// Users mocks base method.
func (m *MockClient) Users(ctx context.Context) ([]arangodb.User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Users", ctx)
ret0, _ := ret[0].([]arangodb.User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Users indicates an expected call of Users.
func (mr *MockClientMockRecorder) Users(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Users", reflect.TypeOf((*MockClient)(nil).Users), ctx)
}
// Version mocks base method.
func (m *MockClient) Version(ctx context.Context) (arangodb.VersionInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Version", ctx)
ret0, _ := ret[0].(arangodb.VersionInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Version indicates an expected call of Version.
func (mr *MockClientMockRecorder) Version(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Version", reflect.TypeOf((*MockClient)(nil).Version), ctx)
}
// VersionWithOptions mocks base method.
func (m *MockClient) VersionWithOptions(ctx context.Context, opts *arangodb.GetVersionOptions) (arangodb.VersionInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "VersionWithOptions", ctx, opts)
ret0, _ := ret[0].(arangodb.VersionInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// VersionWithOptions indicates an expected call of VersionWithOptions.
func (mr *MockClientMockRecorder) VersionWithOptions(ctx, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VersionWithOptions", reflect.TypeOf((*MockClient)(nil).VersionWithOptions), ctx, opts)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=arangodb
//
// Package arangodb is a generated GoMock package.
package arangodb
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=arangodb
//
// Package arangodb is a generated GoMock package.
package arangodb
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/arangodb/mock_user.go
================================================
package arangodb
import (
"context"
"github.com/arangodb/go-driver/v2/arangodb"
"go.uber.org/mock/gomock"
)
// MockUser implements the complete arangodb.user interface.
type MockUser struct {
ctrl *gomock.Controller
name string
active bool
}
func NewMockUser(ctrl *gomock.Controller) *MockUser {
return &MockUser{
ctrl: ctrl,
name: "testUser",
active: true,
}
}
func (m *MockUser) Name() string { return m.name }
func (m *MockUser) IsActive() bool { return m.active }
func (*MockUser) Extra(any) error { return nil }
func (*MockUser) AccessibleDatabases(context.Context) (map[string]arangodb.Grant, error) {
return nil, nil
}
func (*MockUser) AccessibleDatabasesFull(context.Context) (map[string]arangodb.DatabasePermissions, error) {
return nil, nil
}
func (*MockUser) GetDatabaseAccess(context.Context, string) (arangodb.Grant, error) {
return arangodb.GrantNone, nil
}
func (*MockUser) GetCollectionAccess(context.Context, string, string) (arangodb.Grant, error) {
return arangodb.GrantNone, nil
}
func (*MockUser) SetDatabaseAccess(context.Context, string, arangodb.Grant) error {
return nil
}
func (*MockUser) SetCollectionAccess(context.Context, string, string, arangodb.Grant) error {
return nil
}
func (*MockUser) RemoveDatabaseAccess(context.Context, string) error {
return nil
}
func (*MockUser) RemoveCollectionAccess(context.Context, string, string) error {
return nil
}
================================================
FILE: pkg/gofr/datasource/cassandra/cassandra.go
================================================
package cassandra
import (
"context"
"errors"
"fmt"
"reflect"
"time"
"github.com/gocql/gocql"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const (
LoggedBatch = iota
UnloggedBatch
CounterBatch
)
type Config struct {
Hosts string
Keyspace string
Port int
Username string
Password string
}
type cassandra struct {
clusterConfig clusterConfig
session session
query query
batches map[string]batch
}
type Client struct {
config *Config
cassandra *cassandra
logger Logger
metrics Metrics
tracer trace.Tracer
}
var errStatusDown = errors.New("status down")
// New initializes Cassandra driver with the provided configuration.
// The Connect method must be called to establish a connection to Cassandra.
// Usage:
//
// client := New(config)
// client.UseLogger(loggerInstance)
// client.UseMetrics(metricsInstance)
// client.Connect()
func New(conf Config) *Client {
cass := &cassandra{clusterConfig: newClusterConfig(&conf)}
return &Client{config: &conf, cassandra: cass}
}
// Connect establishes a connection to Cassandra and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
c.logger.Debugf("connecting to Cassandra at %v on port %v to keyspace %v", c.config.Hosts, c.config.Port, c.config.Keyspace)
sess, err := c.cassandra.clusterConfig.createSession()
if err != nil {
c.logger.Error("error connecting to Cassandra: ", err)
return
}
cassandraBucktes := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_cassandra_stats", "Response time of CASSANDRA queries in microseconds.", cassandraBucktes...)
c.logger.Logf("connected to '%s' keyspace at host '%s' and port '%d'", c.config.Keyspace, c.config.Hosts, c.config.Port)
c.cassandra.session = sess
}
// UseLogger sets the logger for the Cassandra client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the Cassandra client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for Cassandra client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// Query is the original method without context, preserved for backward compatibility.
// It internally delegates to [Client.QueryWithCtx] using context.Background() as the default context.
func (c *Client) Query(dest any, stmt string, values ...any) error {
return c.QueryWithCtx(context.Background(), dest, stmt, values...)
}
func (c *Client) Exec(stmt string, values ...any) error {
return c.ExecWithCtx(context.Background(), stmt, values)
}
func (c *Client) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
return c.ExecCASWithCtx(context.Background(), dest, stmt, values)
}
func (c *Client) NewBatch(name string, batchType int) error {
return c.NewBatchWithCtx(context.Background(), name, batchType)
}
// QueryWithCtx executes a CQL query in the Cassandra database and returns the result.
//
//nolint:exhaustive // We just want to take care of slice and struct in this case.
func (c *Client) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
span := c.addTrace(ctx, "query", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "QueryWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "query", span)
rvo := reflect.ValueOf(dest)
if rvo.Kind() != reflect.Ptr {
c.logger.Error("we did not get a pointer. data is not settable.")
return errDestinationIsNotPointer
}
rv := rvo.Elem()
iter := c.cassandra.session.query(stmt, values...).iter()
switch rv.Kind() {
case reflect.Slice:
numRows := iter.numRows()
for numRows > 0 {
val := reflect.New(rv.Type().Elem())
if rv.Type().Elem().Kind() == reflect.Struct {
c.rowsToStruct(iter, val)
} else {
_ = iter.scan(val.Interface())
}
rv = reflect.Append(rv, val.Elem())
numRows--
}
if rvo.Elem().CanSet() {
rvo.Elem().Set(rv)
}
case reflect.Struct:
c.rowsToStruct(iter, rv)
default:
c.logger.Debugf("a pointer to %v was not expected.", rv.Kind().String())
return errUnexpectedPointer{target: rv.Kind().String()}
}
return nil
}
func (c *Client) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
span := c.addTrace(ctx, "exec", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "ExecWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "exec", span)
return c.cassandra.session.query(stmt, values...).exec()
}
// ExecCASWithCtx executes a CQL query in the Cassandra database and returns the true if the query is applied.
//
//nolint:exhaustive // We just want to take care of slice and struct in this case.
func (c *Client) ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error) {
var (
applied bool
err error
)
span := c.addTrace(ctx, "exec-cas", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "ExecCASWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "exec-cas", span)
rvo := reflect.ValueOf(dest)
if rvo.Kind() != reflect.Ptr {
c.logger.Debugf("we did not get a pointer. data is not settable.")
return false, errDestinationIsNotPointer
}
rv := rvo.Elem()
q := c.cassandra.session.query(stmt, values...)
switch rv.Kind() {
case reflect.Struct:
applied, err = c.rowsToStructCAS(q, rv)
case reflect.Slice:
c.logger.Debugf("a slice of %v was not expected.", reflect.SliceOf(reflect.TypeOf(dest)).String())
return false, errUnexpectedSlice{target: reflect.SliceOf(reflect.TypeOf(dest)).String()}
case reflect.Map:
c.logger.Debugf("a map was not expected.")
return false, errUnexpectedMap
default:
applied, err = q.scanCAS(rv.Interface())
}
return applied, err
}
func (c *Client) NewBatchWithCtx(_ context.Context, name string, batchType int) error {
switch batchType {
case LoggedBatch, UnloggedBatch, CounterBatch:
if len(c.cassandra.batches) == 0 {
c.cassandra.batches = make(map[string]batch)
}
c.cassandra.batches[name] = c.cassandra.session.newBatch(gocql.BatchType(batchType))
return nil
default:
return errUnsupportedBatchType
}
}
func (c *Client) rowsToStruct(iter iterator, vo reflect.Value) {
v := vo
if vo.Kind() == reflect.Ptr {
v = vo.Elem()
}
columns := c.getColumnsFromColumnsInfo(iter.columns())
fieldNameIndex := c.getFieldNameIndex(v)
fields := c.getFields(columns, fieldNameIndex, v)
_ = iter.scan(fields...)
if vo.CanSet() {
vo.Set(v)
}
}
func (c *Client) rowsToStructCAS(query query, vo reflect.Value) (bool, error) {
v := vo
if vo.Kind() == reflect.Ptr {
v = vo.Elem()
}
row := make(map[string]any)
applied, err := query.mapScanCAS(row)
if err != nil {
return false, err
}
fieldNameIndex := c.getFieldNameIndex(v)
for col, value := range row {
if i, ok := fieldNameIndex[col]; ok {
field := v.Field(i)
if reflect.TypeOf(value) == field.Type() {
field.Set(reflect.ValueOf(value))
}
}
}
if vo.CanSet() {
vo.Set(v)
}
return applied, nil
}
func (*Client) getFields(columns []string, fieldNameIndex map[string]int, v reflect.Value) []any {
fields := make([]any, 0)
for _, column := range columns {
if i, ok := fieldNameIndex[column]; ok {
fields = append(fields, v.Field(i).Addr().Interface())
} else {
var i any
fields = append(fields, &i)
}
}
return fields
}
func (*Client) getFieldNameIndex(v reflect.Value) map[string]int {
fieldNameIndex := map[string]int{}
for i := 0; i < v.Type().NumField(); i++ {
var name string
f := v.Type().Field(i)
tag := f.Tag.Get("db")
if tag != "" {
name = tag
} else {
name = toSnakeCase(f.Name)
}
fieldNameIndex[name] = i
}
return fieldNameIndex
}
func (*Client) getColumnsFromColumnsInfo(columns []gocql.ColumnInfo) []string {
cols := make([]string, 0)
for _, column := range columns {
cols = append(cols, column.Name)
}
return cols
}
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("cassandra.%v.duration", method), duration))
}
c.metrics.RecordHistogram(context.Background(), "app_cassandra_stats", float64(duration), "hostname", c.config.Hosts,
"keyspace", c.config.Keyspace)
c.cassandra.query = nil
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// HealthCheck checks the health of the Cassandra.
func (c *Client) HealthCheck(context.Context) (any, error) {
const (
statusDown = "DOWN"
statusUp = "UP"
)
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.config.Hosts
h.Details["keyspace"] = c.config.Keyspace
if c.cassandra.session == nil {
h.Status = statusDown
h.Details["message"] = "cassandra not connected"
return &h, errStatusDown
}
err := c.cassandra.session.query("SELECT now() FROM system.local").exec()
if err != nil {
h.Status = statusDown
h.Details["message"] = err.Error()
return &h, errStatusDown
}
h.Status = statusUp
return &h, nil
}
func (c *Client) addTrace(ctx context.Context, method, query string) trace.Span {
if c.tracer != nil {
_, span := c.tracer.Start(ctx, fmt.Sprintf("cassandra-%v", method))
span.SetAttributes(
attribute.String("cassandra.query", query),
attribute.String("cassandra.keyspace", c.config.Keyspace),
)
return span
}
return nil
}
================================================
FILE: pkg/gofr/datasource/cassandra/cassandra_batch.go
================================================
package cassandra
import (
"context"
"time"
)
func (c *Client) BatchQuery(name, stmt string, values ...any) error {
return c.BatchQueryWithCtx(context.Background(), name, stmt, values...)
}
func (c *Client) ExecuteBatch(name string) error {
return c.ExecuteBatchWithCtx(context.Background(), name)
}
func (c *Client) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
return c.ExecuteBatchCASWithCtx(context.Background(), name, dest)
}
func (c *Client) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
span := c.addTrace(ctx, "batch-query", stmt)
defer c.sendOperationStats(&QueryLog{
Operation: "BatchQueryWithCtx",
Query: stmt,
Keyspace: c.config.Keyspace,
}, time.Now(), "batch-query", span)
b, ok := c.cassandra.batches[name]
if !ok {
return errBatchNotInitialized
}
b.Query(stmt, values...)
return nil
}
func (c *Client) ExecuteBatchWithCtx(ctx context.Context, name string) error {
span := c.addTrace(ctx, "execute-batch", "batch")
defer c.sendOperationStats(&QueryLog{
Operation: "ExecuteBatchWithCtx",
Query: "batch",
Keyspace: c.config.Keyspace,
}, time.Now(), "execute-batch", span)
b, ok := c.cassandra.batches[name]
if !ok {
return errBatchNotInitialized
}
return c.cassandra.session.executeBatch(b)
}
func (c *Client) ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error) {
span := c.addTrace(ctx, "execute-batch-cas", "batch")
defer c.sendOperationStats(&QueryLog{
Operation: "ExecuteBatchCASWithCtx",
Query: "batch",
Keyspace: c.config.Keyspace,
}, time.Now(), "execute-batch-cas", span)
b, ok := c.cassandra.batches[name]
if !ok {
return false, errBatchNotInitialized
}
return c.cassandra.session.executeBatchCAS(b, dest...)
}
================================================
FILE: pkg/gofr/datasource/cassandra/cassandra_batch_test.go
================================================
package cassandra
import (
"testing"
"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
func Test_BatchQuery(t *testing.T) {
client, mockDeps := initTest(t)
const stmt = "INSERT INTO users (id, name) VALUES(?, ?)"
values := []any{1, "Test"}
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"batch is initialized", func() {
mockDeps.mockBatch.EXPECT().Query(stmt, values...)
}, nil},
{"batch is not initialized", func() {
client.cassandra.batches = nil
}, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
err := client.BatchQuery(mockBatchName, stmt, values...)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_ExecuteBatch(t *testing.T) {
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"execute batch success", func() {
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(nil).Times(1)
}, nil},
{"execute batch failure", func() {
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(errMock).Times(1)
}, errMock},
{"batch not initialized", func() {
client.cassandra.batches = nil
}, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
err := client.ExecuteBatch(mockBatchName)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_newBatch(t *testing.T) {
cassSession := &cassandraSession{session: &gocql.Session{}}
testCases := []struct {
desc string
batchType gocql.BatchType
}{
{"create logged batch", gocql.LoggedBatch},
{"create unlogged batch", gocql.UnloggedBatch},
{"create counter batch", gocql.CounterBatch},
}
for i, tc := range testCases {
b := cassSession.newBatch(tc.batchType)
assert.NotNil(t, b, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_cassandraBatch_Query(t *testing.T) {
c := &cassandraBatch{batch: &gocql.Batch{}}
c.Query("test query")
assert.Equalf(t, 1, c.batch.Size(), "Test Failed")
}
func Test_cassandraBatch_getBatch(t *testing.T) {
c := &cassandraBatch{batch: &gocql.Batch{}}
assert.NotNil(t, c.getBatch(), "Test Failed")
}
func Test_ExecuteBatchCAS(t *testing.T) {
client, mockDeps := initTest(t)
type testStruct struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStructSlice := make([]testStruct, 0)
testCases := []struct {
desc string
dest any
mockCall func()
expRes any
expErr error
}{
{"success case: struct slice", &mockStructSlice, func() {
mockDeps.mockSession.EXPECT().executeBatchCAS(mockDeps.mockBatch, gomock.Any()).Return(true, nil).Times(1)
}, &mockStructSlice, nil},
{"failure case: executeBatchCAS returns error", &mockStructSlice, func() {
mockDeps.mockSession.EXPECT().executeBatchCAS(mockDeps.mockBatch, gomock.Any()).Return(false, assert.AnError).Times(1)
}, &mockStructSlice, assert.AnError},
{"failure case: batch not initialized", &mockStructSlice, func() {
client.cassandra.batches = nil
}, &mockStructSlice, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
applied, err := client.ExecuteBatchCAS(mockBatchName, tc.dest)
assert.Equalf(t, tc.expRes, tc.dest, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, applied, tc.expErr == nil, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/datasource/cassandra/cassandra_test.go
================================================
package cassandra
import (
"context"
"errors"
"testing"
"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
const mockBatchName = "mockBatch"
var (
errConnFail = errors.New("connection failure")
errMock = errors.New("test error")
)
type mockDependencies struct {
mockSession *Mocksession
mockQuery *Mockquery
mockBatch *Mockbatch
mockIter *Mockiterator
}
func initTest(t *testing.T) (*Client, *mockDependencies) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockSession := NewMocksession(ctrl)
mockQuery := NewMockquery(ctrl)
mockBatch := NewMockbatch(ctrl)
mockIter := NewMockiterator(ctrl)
config := Config{
Hosts: "host1",
Port: 9042,
Keyspace: "test_keyspace",
}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-cassandra"))
client.cassandra.session = mockSession
client.cassandra.batches = map[string]batch{mockBatchName: mockBatch}
mockMetrics.EXPECT().RecordHistogram(gomock.AssignableToTypeOf(context.Background()), "app_cassandra_stats",
gomock.AssignableToTypeOf(float64(0)), "hostname", client.config.Hosts, "keyspace", client.config.Keyspace).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Error("we did not get a pointer. data is not settable.").AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return client, &mockDependencies{mockSession: mockSession, mockQuery: mockQuery, mockBatch: mockBatch, mockIter: mockIter}
}
func Test_Connect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockClusterConfig := NewMockclusterConfig(ctrl)
config := Config{
Hosts: "host1",
Port: 9042,
Keyspace: "test_keyspace",
}
cassandraBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
mockLogger.EXPECT().Debugf("connecting to Cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace")
testCases := []struct {
desc string
mockCall func()
expSession session
}{
{"successful connection", func() {
mockClusterConfig.EXPECT().createSession().Return(&cassandraSession{}, nil).Times(1)
mockMetrics.EXPECT().NewHistogram("app_cassandra_stats", "Response time of CASSANDRA queries in microseconds.",
cassandraBuckets).Times(1)
mockLogger.EXPECT().Debugf("connecting to Cassandra at %v on port %v to keyspace %v", "host1", 9042, "test_keyspace")
mockLogger.EXPECT().Logf("connected to '%s' keyspace at host '%s' and port '%d'", "test_keyspace", "host1", 9042)
}, &cassandraSession{}},
{"connection failure", func() {
mockClusterConfig.EXPECT().createSession().Return(nil, errConnFail).Times(1)
mockLogger.EXPECT().Error("error connecting to Cassandra: ")
}, nil},
}
for i, tc := range testCases {
tc.mockCall()
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.cassandra.clusterConfig = mockClusterConfig
client.Connect()
assert.Equal(t, tc.expSession, client.cassandra.session, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_Query(t *testing.T) {
const query = "SELECT id, name FROM users"
type users struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStructSlice := make([]users, 0)
mockIntSlice := make([]int, 0)
mockStruct := users{}
mockInt := 0
client, mockDeps := initTest(t)
testCases := []struct {
desc string
dest any
mockCall func()
expRes any
expErr error
}{
{"success case: struct slice", &mockStructSlice, func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().iter().Return(mockDeps.mockIter).Times(1)
mockDeps.mockIter.EXPECT().numRows().Return(1).Times(1)
mockDeps.mockIter.EXPECT().columns().Return([]gocql.ColumnInfo{{Name: "id"}, {Name: "name"}}).Times(1)
mockDeps.mockIter.EXPECT().scan(gomock.Any()).Times(1)
}, &mockStructSlice, nil},
{"success case: int slice", &mockIntSlice, func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().iter().Return(mockDeps.mockIter).Times(1)
mockDeps.mockIter.EXPECT().numRows().Return(1).Times(1)
mockDeps.mockIter.EXPECT().scan(gomock.Any()).Times(1)
}, &mockIntSlice, nil},
{"success case: struct", &mockStruct, func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().iter().Return(mockDeps.mockIter).Times(1)
mockDeps.mockIter.EXPECT().columns().Return([]gocql.ColumnInfo{{Name: "id"}, {Name: "name"}}).Times(1)
mockDeps.mockIter.EXPECT().scan(gomock.Any()).Times(1)
}, &mockStruct, nil},
{"failure case: dest is not pointer", mockStructSlice, func() {}, mockStructSlice,
errDestinationIsNotPointer},
{"failure case: dest is int", &mockInt, func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().iter().Return(mockDeps.mockIter).Times(1)
}, &mockInt, errUnexpectedPointer{target: "int"}},
}
for i, tc := range testCases {
tc.mockCall()
err := client.Query(tc.dest, query)
assert.Equalf(t, tc.expRes, tc.dest, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_Exec(t *testing.T) {
const query = "INSERT INTO users (id, name) VALUES(1, 'Test')"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"success case", func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().exec().Return(nil).Times(1)
}, nil},
{"failure case", func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().exec().Return(errMock).Times(1)
}, errMock},
}
for i, tc := range testCases {
tc.mockCall()
err := client.Exec(query)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_ExecCAS(t *testing.T) {
const query = "INSERT INTO users (id, name) VALUES(1, 'Test') IF NOT EXISTS"
type users struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStruct := users{}
mockInt := 0
client, mockDeps := initTest(t)
testCases := []struct {
desc string
dest any
mockCall func()
expApplied bool
expErr error
}{
{"success case: struct dest, applied true", &mockStruct, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().mapScanCAS(gomock.AssignableToTypeOf(map[string]any{})).Return(true, nil).Times(1)
}, true, nil},
{"success case: int dest, applied true", &mockInt, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().scanCAS(gomock.Any()).Return(true, nil).Times(1)
}, true, nil},
{"failure case: struct dest, error", &mockStruct, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().mapScanCAS(gomock.AssignableToTypeOf(map[string]any{})).Return(false, errMock).Times(1)
}, false, errMock},
{"failure case: int dest, error", &mockInt, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().scanCAS(gomock.Any()).Return(false, errMock).Times(1)
}, false, errMock},
{"failure case: dest is not pointer", mockInt, func() {}, false, errDestinationIsNotPointer},
{"failure case: dest is slice", &[]int{}, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
}, false, errUnexpectedSlice{target: "[]*[]int"}},
{"failure case: dest is map", &map[string]any{}, func() {
mockDeps.mockSession.EXPECT().query(query, nil).Return(mockDeps.mockQuery).Times(1)
}, false, errUnexpectedMap},
}
for i, tc := range testCases {
tc.mockCall()
applied, err := client.ExecCAS(tc.dest, query)
assert.Equalf(t, tc.expApplied, applied, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_NewBatch(t *testing.T) {
const batchName = "testBatch"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
batchType int
mockCall func()
expErr error
}{
{"valid log type", LoggedBatch, func() {
mockDeps.mockSession.EXPECT().newBatch(gocql.BatchType(LoggedBatch)).Return(&cassandraBatch{}).Times(1)
}, nil},
{"valid log type, empty batches", LoggedBatch, func() {
client.cassandra.batches = nil
mockDeps.mockSession.EXPECT().newBatch(gocql.BatchType(LoggedBatch)).Return(&cassandraBatch{}).Times(1)
}, nil},
{"invalid log type", -1, func() {}, errUnsupportedBatchType},
}
for i, tc := range testCases {
tc.mockCall()
err := client.NewBatch(batchName, tc.batchType)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
if tc.expErr != nil {
_, ok := client.cassandra.batches[batchName]
assert.Truef(t, ok, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
}
func Test_HealthCheck(t *testing.T) {
const query = "SELECT now() FROM system.local"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expHealth *Health
err error
}{
{"success case", func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().exec().Return(nil).Times(1)
}, &Health{
Status: "UP",
Details: map[string]any{"host": client.config.Hosts, "keyspace": client.config.Keyspace},
}, nil},
{"failure case: exec error", func() {
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().exec().Return(errMock).Times(1)
}, &Health{
Status: "DOWN",
Details: map[string]any{"host": client.config.Hosts, "keyspace": client.config.Keyspace,
"message": errMock.Error()},
}, errStatusDown},
{"failure case: cassandra not initializes", func() {
client.cassandra.session = nil
mockDeps.mockSession.EXPECT().query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().exec().Return(nil).Times(1)
}, &Health{
Status: "DOWN",
Details: map[string]any{"host": client.config.Hosts, "keyspace": client.config.Keyspace,
"message": "cassandra not connected"},
}, errStatusDown},
}
for i, tc := range testCases {
tc.mockCall()
health, err := client.HealthCheck(context.Background())
assert.Equal(t, tc.err, err)
assert.Equalf(t, tc.expHealth, health, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_CreateSession_Error(t *testing.T) {
c := newClusterConfig(&Config{})
sess, err := c.createSession()
assert.Nil(t, sess, "Test Failed: should return error without creating session")
require.Error(t, err, "Test Failed: should return error without creating session")
}
func Test_cassandraSession_Query(t *testing.T) {
c := &cassandraSession{session: &gocql.Session{}}
q := c.query("sample query")
assert.NotNil(t, q, "Test Failed")
assert.IsType(t, &cassandraQuery{}, q, "Test Failed")
}
================================================
FILE: pkg/gofr/datasource/cassandra/errors.go
================================================
package cassandra
import (
"errors"
"fmt"
)
var (
errDestinationIsNotPointer = errors.New("destination is not pointer")
errUnexpectedMap = errors.New("a map was not expected")
errUnsupportedBatchType = errors.New("batch type not supported")
errBatchNotInitialized = errors.New("batch not initialized")
)
type errUnexpectedPointer struct {
target string
}
func (d errUnexpectedPointer) Error() string {
return fmt.Sprintf("a pointer to %v was not expected.", d.target)
}
type errUnexpectedSlice struct {
target string
}
func (d errUnexpectedSlice) Error() string {
return fmt.Sprintf("a slice of %v was not expected.", d.target)
}
================================================
FILE: pkg/gofr/datasource/cassandra/errors_test.go
================================================
package cassandra
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_DestinationIsNotPointer_Error(t *testing.T) {
err := errDestinationIsNotPointer
require.Equal(t, err, errDestinationIsNotPointer)
}
func Test_UnexpectedPointer_Error(t *testing.T) {
expected := "a pointer to int was not expected."
err := errUnexpectedPointer{target: "int"}
require.ErrorContains(t, err, expected)
}
func Test_UnexpectedSlice_Error(t *testing.T) {
expected := "a slice of int was not expected."
err := errUnexpectedSlice{target: "int"}
require.ErrorContains(t, err, expected)
}
func Test_UnexpectedMap_Error(t *testing.T) {
err := errUnexpectedMap
require.ErrorIs(t, err, errUnexpectedMap)
}
================================================
FILE: pkg/gofr/datasource/cassandra/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/cassandra
go 1.25.0
require (
github.com/gocql/gocql v1.7.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/cassandra/go.sum
================================================
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/cassandra/interfaces.go
================================================
package cassandra
import (
"github.com/gocql/gocql"
)
//go:generate mockgen -source=interfaces.go -destination=mock_interfaces.go -package=cassandra
// All interfaces is designed to be mockable for unit testing purposes, allowing you to control the behavior of Cassandra
// interactions during tests.
// clusterConfig defines methods for interacting with a Cassandra clusterConfig.
type clusterConfig interface {
createSession() (session, error)
}
// session defines methods for interacting with a Cassandra session.
type session interface {
query(stmt string, values ...any) query
newBatch(batchtype gocql.BatchType) batch
executeBatch(batch batch) error
executeBatchCAS(b batch, dest ...any) (bool, error)
}
// query defines methods for interacting with a Cassandra query.
type query interface {
exec() error
iter() iterator
mapScanCAS(dest map[string]any) (applied bool, err error)
scanCAS(dest ...any) (applied bool, err error)
}
// batch defines methods for interacting with a Cassandra batch.
type batch interface {
Query(stmt string, args ...any)
getBatch() *gocql.Batch
}
// iterator defines methods for interacting with a Cassandra iterator.
type iterator interface {
columns() []gocql.ColumnInfo
scan(dest ...any) bool
numRows() int
}
================================================
FILE: pkg/gofr/datasource/cassandra/internal.go
================================================
package cassandra
import (
"regexp"
"strings"
"github.com/gocql/gocql"
)
// cassandraIterator implements iterator interface.
type cassandraIterator struct {
iter *gocql.Iter
}
// Columns gets the column information.
// This method wraps the `Columns` method of the underlying `iter` object.
func (c *cassandraIterator) columns() []gocql.ColumnInfo {
return c.iter.Columns()
}
// Scan gets the next row from the Cassandra iterator and fills in the provided arguments.
// This method wraps the `Scan` method of the underlying `iter` object.
func (c *cassandraIterator) scan(dest ...any) bool {
return c.iter.Scan(dest...)
}
// numRows returns a number of rows.
// This method wraps the `NumRows` method of the underlying `iter` object.
func (c *cassandraIterator) numRows() int {
return c.iter.NumRows()
}
// cassandraQuery implements query interface.
type cassandraQuery struct {
query *gocql.Query
}
// exec performs a Cassandra's Query Exec.
// This method wraps the `Exec` method of the underlying `query` object.
func (c *cassandraQuery) exec() error {
return c.query.Exec()
}
// iter returns a Cassandra iterator.
// This method wraps the `Iter` method of the underlying `query` object.
func (c *cassandraQuery) iter() iterator {
iter := cassandraIterator{iter: c.query.Iter()}
return &iter
}
// mapScanCAS checks a Cassandra query with an IF clause and scans the existing data into map[string]any (if any).
// This method wraps the `MapScanCAS` method of the underlying `query` object.
func (c *cassandraQuery) mapScanCAS(dest map[string]any) (applied bool, err error) {
return c.query.MapScanCAS(dest)
}
// scanCAS checks a Cassandra query with an IF clause and scans the existing data (if any).
// This method wraps the `ScanCAS` method of the underlying `query` object.
func (c *cassandraQuery) scanCAS(dest ...any) (applied bool, err error) {
return c.query.ScanCAS(dest)
}
// cassandraClusterConfig implements clusterConfig interface.
type cassandraClusterConfig struct {
clusterConfig *gocql.ClusterConfig
}
func newClusterConfig(config *Config) clusterConfig {
var c cassandraClusterConfig
config.Hosts = strings.TrimSuffix(strings.TrimSpace(config.Hosts), ",")
hosts := strings.Split(config.Hosts, ",")
c.clusterConfig = gocql.NewCluster(hosts...)
c.clusterConfig.Keyspace = config.Keyspace
c.clusterConfig.Port = config.Port
c.clusterConfig.Authenticator = gocql.PasswordAuthenticator{Username: config.Username, Password: config.Password}
return &c
}
// createSession creates a Cassandra session based on the provided configuration.
// This method wraps the `CreateSession` method of the underlying `clusterConfig` object.
// It creates a new Cassandra session using the configuration options specified in `c.clusterConfig`.
//
// Returns:
// - A `session` object representing the established Cassandra connection, or `nil` if an error occurred.
// - An `error` object if there was a problem creating the session, or `nil` if successful.
func (c *cassandraClusterConfig) createSession() (session, error) {
sess, err := c.clusterConfig.CreateSession()
if err != nil {
return nil, err
}
return &cassandraSession{session: sess}, nil
}
// cassandraSession implements session interface.
type cassandraSession struct {
session *gocql.Session
}
// query creates a Cassandra query.
// This method wraps the `Query` method of the underlying `session` object.
func (c *cassandraSession) query(stmt string, values ...any) query {
return &cassandraQuery{query: c.session.Query(stmt, values...)}
}
func (c *cassandraSession) newBatch(batchType gocql.BatchType) batch {
return &cassandraBatch{batch: c.session.NewBatch(batchType)}
}
// executeBatch executes a batch operation and returns nil if successful otherwise an error is returned describing the failure.
// This method wraps the `ExecuteBatch` method of the underlying `session` object.
func (c *cassandraSession) executeBatch(b batch) error {
gocqlBatch := b.getBatch()
return c.session.ExecuteBatch(gocqlBatch)
}
// executeBatchCAS executes a batch operation and returns true if successful.
// This method wraps the `executeBatchCAS` method of the underlying `session` object.
func (c *cassandraSession) executeBatchCAS(b batch, dest ...any) (bool, error) {
gocqlBatch := b.getBatch()
applied, _, err := c.session.ExecuteBatchCAS(gocqlBatch, dest...)
return applied, err
}
// cassandraBatch implements batch interface.
type cassandraBatch struct {
batch *gocql.Batch
}
// Query adds the query to the batch operation.
// This method wraps the `Query` method of underlying `batch` object.
func (c *cassandraBatch) Query(stmt string, args ...any) {
c.batch.Query(stmt, args...)
}
// getBatch returns the underlying `gocql.Batch`.
func (c *cassandraBatch) getBatch() *gocql.Batch {
return c.batch
}
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
func toSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}
================================================
FILE: pkg/gofr/datasource/cassandra/logger.go
================================================
package cassandra
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type QueryLog struct {
Operation string `json:"operation"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Keyspace string `json:"keyspace,omitempty"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s \u001B[38;5;8m%-32s\u001B[0m\n",
clean(ql.Operation), "CASS", ql.Duration, clean(ql.Keyspace), clean(ql.Query))
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/cassandra/logger_test.go
================================================
package cassandra
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := QueryLog{
Query: "sample query",
Duration: 12345,
}
expected := "sample query"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
func Test_Clean(t *testing.T) {
testCases := []struct {
desc string
input string
expected string
}{
{"multiple spaces", " multiple spaces ", "multiple spaces"},
{"leading and trailing", "leading and trailing ", "leading and trailing"},
{"mixed white spaces", " mixed\twhite\nspaces", "mixed white spaces"},
{"single word", "singleword", "singleword"},
{"empty string", "", ""},
{"empty string with spaces", " ", ""},
}
for i, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := clean(tc.input)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
================================================
FILE: pkg/gofr/datasource/cassandra/metrics.go
================================================
package cassandra
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/cassandra/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces.go
//
// Generated by this command:
//
// mockgen -source=interfaces.go -destination=mock_interfaces.go -package=cassandra
//
package cassandra
import (
reflect "reflect"
gocql "github.com/gocql/gocql"
gomock "go.uber.org/mock/gomock"
)
// MockclusterConfig is a mock of clusterConfig interface.
type MockclusterConfig struct {
ctrl *gomock.Controller
recorder *MockclusterConfigMockRecorder
}
// MockclusterConfigMockRecorder is the mock recorder for MockclusterConfig.
type MockclusterConfigMockRecorder struct {
mock *MockclusterConfig
}
// NewMockclusterConfig creates a new mock instance.
func NewMockclusterConfig(ctrl *gomock.Controller) *MockclusterConfig {
mock := &MockclusterConfig{ctrl: ctrl}
mock.recorder = &MockclusterConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockclusterConfig) EXPECT() *MockclusterConfigMockRecorder {
return m.recorder
}
// createSession mocks base method.
func (m *MockclusterConfig) createSession() (session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "createSession")
ret0, _ := ret[0].(session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// createSession indicates an expected call of createSession.
func (mr *MockclusterConfigMockRecorder) createSession() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createSession", reflect.TypeOf((*MockclusterConfig)(nil).createSession))
}
// Mocksession is a mock of session interface.
type Mocksession struct {
ctrl *gomock.Controller
recorder *MocksessionMockRecorder
}
// MocksessionMockRecorder is the mock recorder for Mocksession.
type MocksessionMockRecorder struct {
mock *Mocksession
}
// NewMocksession creates a new mock instance.
func NewMocksession(ctrl *gomock.Controller) *Mocksession {
mock := &Mocksession{ctrl: ctrl}
mock.recorder = &MocksessionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mocksession) EXPECT() *MocksessionMockRecorder {
return m.recorder
}
// executeBatch mocks base method.
func (m *Mocksession) executeBatch(batch batch) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "executeBatch", batch)
ret0, _ := ret[0].(error)
return ret0
}
// executeBatch indicates an expected call of executeBatch.
func (mr *MocksessionMockRecorder) executeBatch(batch any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeBatch", reflect.TypeOf((*Mocksession)(nil).executeBatch), batch)
}
// executeBatchCAS mocks base method.
func (m *Mocksession) executeBatchCAS(b batch, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{b}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "executeBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// executeBatchCAS indicates an expected call of executeBatchCAS.
func (mr *MocksessionMockRecorder) executeBatchCAS(b any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{b}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeBatchCAS", reflect.TypeOf((*Mocksession)(nil).executeBatchCAS), varargs...)
}
// newBatch mocks base method.
func (m *Mocksession) newBatch(batchtype gocql.BatchType) batch {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "newBatch", batchtype)
ret0, _ := ret[0].(batch)
return ret0
}
// newBatch indicates an expected call of newBatch.
func (mr *MocksessionMockRecorder) newBatch(batchtype any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "newBatch", reflect.TypeOf((*Mocksession)(nil).newBatch), batchtype)
}
// query mocks base method.
func (m *Mocksession) query(stmt string, values ...any) query {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "query", varargs...)
ret0, _ := ret[0].(query)
return ret0
}
// query indicates an expected call of query.
func (mr *MocksessionMockRecorder) query(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "query", reflect.TypeOf((*Mocksession)(nil).query), varargs...)
}
// Mockquery is a mock of query interface.
type Mockquery struct {
ctrl *gomock.Controller
recorder *MockqueryMockRecorder
}
// MockqueryMockRecorder is the mock recorder for Mockquery.
type MockqueryMockRecorder struct {
mock *Mockquery
}
// NewMockquery creates a new mock instance.
func NewMockquery(ctrl *gomock.Controller) *Mockquery {
mock := &Mockquery{ctrl: ctrl}
mock.recorder = &MockqueryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockquery) EXPECT() *MockqueryMockRecorder {
return m.recorder
}
// exec mocks base method.
func (m *Mockquery) exec() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "exec")
ret0, _ := ret[0].(error)
return ret0
}
// exec indicates an expected call of exec.
func (mr *MockqueryMockRecorder) exec() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "exec", reflect.TypeOf((*Mockquery)(nil).exec))
}
// iter mocks base method.
func (m *Mockquery) iter() iterator {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "iter")
ret0, _ := ret[0].(iterator)
return ret0
}
// iter indicates an expected call of iter.
func (mr *MockqueryMockRecorder) iter() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "iter", reflect.TypeOf((*Mockquery)(nil).iter))
}
// mapScanCAS mocks base method.
func (m *Mockquery) mapScanCAS(dest map[string]any) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "mapScanCAS", dest)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// mapScanCAS indicates an expected call of mapScanCAS.
func (mr *MockqueryMockRecorder) mapScanCAS(dest any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "mapScanCAS", reflect.TypeOf((*Mockquery)(nil).mapScanCAS), dest)
}
// scanCAS mocks base method.
func (m *Mockquery) scanCAS(dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "scanCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// scanCAS indicates an expected call of scanCAS.
func (mr *MockqueryMockRecorder) scanCAS(dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "scanCAS", reflect.TypeOf((*Mockquery)(nil).scanCAS), dest...)
}
// Mockbatch is a mock of batch interface.
type Mockbatch struct {
ctrl *gomock.Controller
recorder *MockbatchMockRecorder
}
// MockbatchMockRecorder is the mock recorder for Mockbatch.
type MockbatchMockRecorder struct {
mock *Mockbatch
}
// NewMockbatch creates a new mock instance.
func NewMockbatch(ctrl *gomock.Controller) *Mockbatch {
mock := &Mockbatch{ctrl: ctrl}
mock.recorder = &MockbatchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockbatch) EXPECT() *MockbatchMockRecorder {
return m.recorder
}
// Query mocks base method.
func (m *Mockbatch) Query(stmt string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Query", varargs...)
}
// Query indicates an expected call of Query.
func (mr *MockbatchMockRecorder) Query(stmt any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*Mockbatch)(nil).Query), varargs...)
}
// getBatch mocks base method.
func (m *Mockbatch) getBatch() *gocql.Batch {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "getBatch")
ret0, _ := ret[0].(*gocql.Batch)
return ret0
}
// getBatch indicates an expected call of getBatch.
func (mr *MockbatchMockRecorder) getBatch() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getBatch", reflect.TypeOf((*Mockbatch)(nil).getBatch))
}
// Mockiterator is a mock of iterator interface.
type Mockiterator struct {
ctrl *gomock.Controller
recorder *MockiteratorMockRecorder
}
// MockiteratorMockRecorder is the mock recorder for Mockiterator.
type MockiteratorMockRecorder struct {
mock *Mockiterator
}
// NewMockiterator creates a new mock instance.
func NewMockiterator(ctrl *gomock.Controller) *Mockiterator {
mock := &Mockiterator{ctrl: ctrl}
mock.recorder = &MockiteratorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockiterator) EXPECT() *MockiteratorMockRecorder {
return m.recorder
}
// columns mocks base method.
func (m *Mockiterator) columns() []gocql.ColumnInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "columns")
ret0, _ := ret[0].([]gocql.ColumnInfo)
return ret0
}
// columns indicates an expected call of columns.
func (mr *MockiteratorMockRecorder) columns() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "columns", reflect.TypeOf((*Mockiterator)(nil).columns))
}
// numRows mocks base method.
func (m *Mockiterator) numRows() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "numRows")
ret0, _ := ret[0].(int)
return ret0
}
// numRows indicates an expected call of numRows.
func (mr *MockiteratorMockRecorder) numRows() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "numRows", reflect.TypeOf((*Mockiterator)(nil).numRows))
}
// scan mocks base method.
func (m *Mockiterator) scan(dest ...any) bool {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "scan", varargs...)
ret0, _ := ret[0].(bool)
return ret0
}
// scan indicates an expected call of scan.
func (mr *MockiteratorMockRecorder) scan(dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "scan", reflect.TypeOf((*Mockiterator)(nil).scan), dest...)
}
================================================
FILE: pkg/gofr/datasource/cassandra/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger_old.go -package=cassandra
//
package cassandra
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/cassandra/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=cassandra
//
package cassandra
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/clickhouse/clickhouse.go
================================================
// Package clickhouse provides a client for interacting with ClickHouse databases,
// supporting query execution, asynchronous inserts, and observability integration
// through logging, metrics, and tracing.
//
// It is designed to be used with the GoFr framework and allows configuration
// of connection parameters, observability tools, and health checks.
package clickhouse
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/ClickHouse/clickhouse-go/v2"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type Config struct {
Hosts string // Comma-separated list of ClickHouse server addresses.
Username string // Username used for authentication.
Password string // Password used for authentication.
Database string // Name of the database to connect to.
}
// Client is a ClickHouse client implementation that wraps a Conn interface.
// It provides methods for executing queries, performing inserts, and collecting metrics.
type Client struct {
conn Conn
config Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
var errStatusDown = errors.New("status down")
// New initializes ClickHouse client with the provided configuration.
// Metrics, Logger has to be initialized before calling the Connect method.
// Usage:
//
// client.UseLogger(Logger())
// client.UseMetrics(Metrics())
//
// client.Connect()
func New(config Config) *Client {
return &Client{config: config}
}
// UseLogger sets the logger for the ClickHouse client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the ClickHouse client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for ClickHouse client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Connect establishes a connection to ClickHouse and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
var err error
c.logger.Debugf("connecting to Clickhouse db at %v to database %v", c.config.Hosts, c.config.Database)
clickHouseBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_clickhouse_stats", "Response time of Clickhouse queries in microseconds.", clickHouseBuckets...)
c.metrics.NewGauge("app_clickhouse_open_connections", "Number of open Clickhouse connections.")
c.metrics.NewGauge("app_clickhouse_idle_connections", "Number of idle Clickhouse connections.")
addresses := strings.Split(c.config.Hosts, ",")
ctx := context.Background()
c.conn, err = clickhouse.Open(&clickhouse.Options{
Addr: addresses,
Auth: clickhouse.Auth{
Database: c.config.Database,
Username: c.config.Username,
Password: c.config.Password,
},
})
if err != nil {
c.logger.Errorf("error while connecting to Clickhouse %v", err)
return
}
if err = c.conn.Ping(ctx); err != nil {
c.logger.Errorf("ping failed with error %v", err)
} else {
c.logger.Logf("successfully connected to ClickhouseDB")
}
go pushDBMetrics(c.conn, c.metrics)
}
func pushDBMetrics(conn Conn, metrics Metrics) {
const frequency = 10
for {
if conn != nil {
stats := conn.Stats()
metrics.SetGauge("app_clickhouse_open_connections", float64(stats.Open))
metrics.SetGauge("app_clickhouse_idle_connections", float64(stats.Idle))
time.Sleep(frequency * time.Second)
}
}
}
// Exec should be used for DDL and simple statements.
// It should not be used for larger inserts or query iterations.
func (c *Client) Exec(ctx context.Context, query string, args ...any) error {
tracedCtx, span := c.addTrace(ctx, "exec", query)
err := c.conn.Exec(tracedCtx, query, args...)
defer c.sendOperationStats(time.Now(), "Exec", query, "exec", span, args...)
return err
}
// Select method allows a set of response rows to be marshaled into a slice of structs with a single invocation..
// DB column names should be defined in the struct in `ch` tag.
// Example Usages:
//
// type User struct {
// Id string `ch:"id"`
// Name string `ch:"name"`
// Age string `ch:"age"`
// }
//
// var user []User
//
// err = ctx.Clickhouse.Select(ctx, &user, "SELECT * FROM users") .
func (c *Client) Select(ctx context.Context, dest any, query string, args ...any) error {
tracedCtx, span := c.addTrace(ctx, "select", query)
err := c.conn.Select(tracedCtx, dest, query, args...)
defer c.sendOperationStats(time.Now(), "Select", query, "select", span, args...)
return err
}
// AsyncInsert allows the user to specify whether the client should wait for the server to complete the insert or
// respond once the data has been received.
func (c *Client) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
tracedCtx, span := c.addTrace(ctx, "async-insert", query)
err := c.conn.AsyncInsert(tracedCtx, query, wait, args...)
defer c.sendOperationStats(time.Now(), "AsyncInsert", query, "async-insert", span, args...)
return err
}
// sendOperationStats records the duration of a database operation and logs the query context.
// It also attaches metrics and trace attributes if enabled.
func (c *Client) sendOperationStats(start time.Time, methodType, query string, method string,
span trace.Span, args ...any) {
duration := time.Since(start).Microseconds()
c.logger.Debug(&Log{
Type: methodType,
Query: query,
Duration: duration,
Args: args,
})
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("clickhouse.%v.duration", method), duration))
}
c.metrics.RecordHistogram(context.Background(), "app_clickhouse_stats", float64(duration), "hosts", c.config.Hosts,
"database", c.config.Database, "type", getOperationType(query))
}
// getOperationType extracts the operation type (e.g., SELECT, INSERT) from a query.
func getOperationType(query string) string {
query = strings.TrimSpace(query)
words := strings.Split(query, " ")
return strings.ToUpper(words[0])
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// HealthCheck checks the health of the MongoDB client by pinging the database.
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.config.Hosts
h.Details["database"] = c.config.Database
err := c.conn.Ping(ctx)
if err != nil {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
return &h, nil
}
// addTrace starts a new trace span for the given operation and query.
func (c *Client) addTrace(ctx context.Context, method, query string) (context.Context, trace.Span) {
if c.tracer != nil {
contextWithTrace, span := c.tracer.Start(ctx, fmt.Sprintf("clickhouse-%v", method))
span.SetAttributes(
attribute.String("clickhouse.query", query),
)
return contextWithTrace, span
}
return ctx, nil
}
================================================
FILE: pkg/gofr/datasource/clickhouse/clickhouse_test.go
================================================
package clickhouse
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func getClickHouseTestConnection(t *testing.T) (*MockConn, *MockMetrics, *MockLogger, Client) {
t.Helper()
ctrl := gomock.NewController(t)
mockConn := NewMockConn(ctrl)
mockMetric := NewMockMetrics(ctrl)
mockLogger := NewMockLogger(ctrl)
c := Client{conn: mockConn, config: Config{
Hosts: "localhost",
Username: "user",
Password: "pass",
Database: "test",
}, logger: mockLogger, metrics: mockMetric}
return mockConn, mockMetric, mockLogger, c
}
func Test_ClickHouse_ConnectAndMetricRegistrationAndPingFailure(t *testing.T) {
_, mockMetric, mockLogger, _ := getClickHouseTestConnection(t)
cl := New(Config{
Hosts: "localhost:8000",
Username: "user",
Password: "pass",
Database: "test",
})
cl.UseLogger(mockLogger)
cl.UseMetrics(mockMetric)
mockMetric.EXPECT().NewHistogram("app_clickhouse_stats", "Response time of Clickhouse queries in microseconds.", gomock.Any())
mockMetric.EXPECT().NewGauge("app_clickhouse_open_connections", "Number of open Clickhouse connections.")
mockMetric.EXPECT().NewGauge("app_clickhouse_idle_connections", "Number of idle Clickhouse connections.")
mockMetric.EXPECT().SetGauge("app_clickhouse_open_connections", gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge("app_clickhouse_idle_connections", gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf("connecting to Clickhouse db at %v to database %v", "localhost:8000", "test")
mockLogger.EXPECT().Errorf("ping failed with error %v", gomock.Any())
cl.Connect()
time.Sleep(100 * time.Millisecond)
assert.True(t, mockLogger.ctrl.Satisfied())
assert.True(t, mockMetric.ctrl.Satisfied())
}
func Test_ClickHouse_HealthUP(t *testing.T) {
mockConn, _, _, c := getClickHouseTestConnection(t)
mockConn.EXPECT().Ping(gomock.Any()).Return(nil)
resp, _ := c.HealthCheck(context.Background())
assert.Contains(t, fmt.Sprint(resp), "UP")
}
func Test_ClickHouse_HealthDOWN(t *testing.T) {
mockConn, _, _, c := getClickHouseTestConnection(t)
mockConn.EXPECT().Ping(gomock.Any()).Return(sql.ErrConnDone)
resp, err := c.HealthCheck(context.Background())
require.ErrorIs(t, err, errStatusDown)
assert.Contains(t, fmt.Sprint(resp), "DOWN")
}
func Test_ClickHouse_Exec(t *testing.T) {
mockConn, mockMetric, mockLogger, c := getClickHouseTestConnection(t)
ctx := context.Background()
mockConn.EXPECT().Exec(ctx, "INSERT INTO users (id, name, age) VALUES (?, ?, ?)",
"8f165e2d-feef-416c-95f6-913ce3172e15", "gofr", "10").Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetric.EXPECT().RecordHistogram(ctx, "app_clickhouse_stats", float64(0), "hosts", c.config.Hosts,
"database", c.config.Database, "type", "INSERT")
err := c.Exec(ctx, "INSERT INTO users (id, name, age) VALUES (?, ?, ?)", "8f165e2d-feef-416c-95f6-913ce3172e15", "gofr", "10")
require.NoError(t, err)
}
func Test_ClickHouse_Select(t *testing.T) {
mockConn, mockMetric, mockLogger, c := getClickHouseTestConnection(t)
type User struct {
ID string `ch:"id"`
Name string `ch:"name"`
Age string `ch:"age"`
}
ctx := context.Background()
var user []User
mockConn.EXPECT().Select(ctx, &user, "SELECT * FROM users").Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetric.EXPECT().RecordHistogram(ctx, "app_clickhouse_stats", float64(0), "hosts", c.config.Hosts,
"database", c.config.Database, "type", "SELECT")
err := c.Select(ctx, &user, "SELECT * FROM users")
require.NoError(t, err)
}
func Test_ClickHouse_AsyncInsert(t *testing.T) {
mockConn, mockMetric, mockLogger, c := getClickHouseTestConnection(t)
ctx := context.Background()
mockConn.EXPECT().AsyncInsert(ctx, "INSERT INTO users (id, name, age) VALUES (?, ?, ?)", true,
"8f165e2d-feef-416c-95f6-913ce3172e15", "user", "10").Return(nil)
mockMetric.EXPECT().RecordHistogram(ctx, "app_clickhouse_stats", float64(0), "hosts", c.config.Hosts,
"database", c.config.Database, "type", "INSERT")
mockLogger.EXPECT().Debug(gomock.Any())
err := c.AsyncInsert(ctx, "INSERT INTO users (id, name, age) VALUES (?, ?, ?)", true,
"8f165e2d-feef-416c-95f6-913ce3172e15", "user", "10")
require.NoError(t, err)
}
================================================
FILE: pkg/gofr/datasource/clickhouse/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/clickhouse
go 1.25.0
require (
github.com/ClickHouse/clickhouse-go/v2 v2.43.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/ClickHouse/ch-go v0.71.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/paulmach/orb v0.12.0 // indirect
github.com/pierrec/lz4/v4 v4.1.25 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/segmentio/asm v1.2.1 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sys v0.40.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/clickhouse/go.sum
================================================
github.com/ClickHouse/ch-go v0.71.0 h1:bUdZ/EZj/LcVHsMqaRUP2holqygrPWQKeMjc6nZoyRM=
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
github.com/ClickHouse/clickhouse-go/v2 v2.43.0 h1:fUR05TrF1GyvLDa/mAQjkx7KbgwdLRffs2n9O3WobtE=
github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.2/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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/paulmach/orb v0.12.0 h1:z+zOwjmG3MyEEqzv92UN49Lg1JFYx0L9GpGKNVDKk1s=
github.com/paulmach/orb v0.12.0/go.mod h1:5mULz1xQfs3bmQm63QEJA6lNGujuRafwA5S/EnuLaLU=
github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY=
github.com/pierrec/lz4/v4 v4.1.25 h1:kocOqRffaIbU5djlIBr7Wh+cx82C0vtFb0fOurZHqD0=
github.com/pierrec/lz4/v4 v4.1.25/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/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/sys v0.0.0-20190215142949-d0b11bdaac8a/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-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-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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/clickhouse/interface.go
================================================
// Package clickhouse provides functionalities for interacting with a Clickhouse database.
//
// It contains the Conn interface for executing queries, retrieving data,
// managing asynchronous inserts, and obtaining connection statistics.
package clickhouse
import (
"context"
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
)
// Conn defines the interface for interacting with a ClickHouse database.
//
// It abstracts the underlying database operations, allowing clients to perform
// queries, execute statements, perform asynchronous inserts, and retrieve
// connection statistics. This interface is suitable for use in application logic
// and for mocking in tests.
type Conn interface {
// Select executes a query that returns rows and scans the result into the provided destination.
//
// The destination must be a pointer to a slice of structs or another compatible type
// supported by the ClickHouse driver.
//
// ctx controls the lifetime of the query.
//
// Returns an error if the query execution or scanning fails.
Select(ctx context.Context, dest any, query string, args ...any) error
// Exec executes a query that does not return rows, such as INSERT or UPDATE.
//
// ctx controls the lifetime of the query.
//
// Returns an error if the execution fails.
Exec(ctx context.Context, query string, args ...any) error
// AsyncInsert performs an asynchronous INSERT operation.
//
// If wait is true, the method waits for the insert operation to complete before returning.
// If false, the insert is queued and the method returns immediately.
//
// ctx controls the lifetime of the operation.
//
// Returns an error if the insert operation fails.
AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error
// Ping verifies the connection to the database is still alive.
//
// ctx controls the timeout for the ping operation.
//
// Returns an error if the connection is unhealthy or the ping fails.
Ping(context.Context) error
// Stats returns internal statistics for the ClickHouse driver connection,
// such as open and idle connections.
//
// The returned value is driver.Stats as provided by the ClickHouse driver library.
Stats() driver.Stats
}
================================================
FILE: pkg/gofr/datasource/clickhouse/logger.go
================================================
package clickhouse
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debugf(pattern string, args ...any)
Debug(args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Type string `json:"type"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Args []any `json:"args,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n",
l.Type, "CHDB", l.Duration, clean(l.Query))
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/clickhouse/logger_test.go
================================================
package clickhouse
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoggingDataPresent(t *testing.T) {
queryLog := Log{
Type: "SELECT",
Query: "SELECT * FROM users",
Duration: 12345,
}
expected := "SELECT"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
================================================
FILE: pkg/gofr/datasource/clickhouse/metrics.go
================================================
package clickhouse
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
NewGauge(name, desc string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/clickhouse/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=clickhouse
//
package clickhouse
import (
context "context"
reflect "reflect"
driver "github.com/ClickHouse/clickhouse-go/v2/lib/driver"
gomock "go.uber.org/mock/gomock"
)
// MockConn is a mock of Conn interface.
type MockConn struct {
ctrl *gomock.Controller
recorder *MockConnMockRecorder
}
// MockConnMockRecorder is the mock recorder for MockConn.
type MockConnMockRecorder struct {
mock *MockConn
}
// NewMockConn creates a new mock instance.
func NewMockConn(ctrl *gomock.Controller) *MockConn {
mock := &MockConn{ctrl: ctrl}
mock.recorder = &MockConnMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConn) EXPECT() *MockConnMockRecorder {
return m.recorder
}
// AsyncInsert mocks base method.
func (m *MockConn) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query, wait}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AsyncInsert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AsyncInsert indicates an expected call of AsyncInsert.
func (mr *MockConnMockRecorder) AsyncInsert(ctx, query, wait any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query, wait}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncInsert", reflect.TypeOf((*MockConn)(nil).AsyncInsert), varargs...)
}
// Exec mocks base method.
func (m *MockConn) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockConnMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockConn)(nil).Exec), varargs...)
}
// Ping mocks base method.
func (m *MockConn) Ping(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping.
func (mr *MockConnMockRecorder) Ping(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockConn)(nil).Ping), arg0)
}
// Select mocks base method.
func (m *MockConn) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockConnMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockConn)(nil).Select), varargs...)
}
// Stats mocks base method.
func (m *MockConn) Stats() driver.Stats {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stats")
ret0, _ := ret[0].(driver.Stats)
return ret0
}
// Stats indicates an expected call of Stats.
func (mr *MockConnMockRecorder) Stats() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockConn)(nil).Stats))
}
================================================
FILE: pkg/gofr/datasource/clickhouse/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=clickhouse
//
// Package clickhouse is a generated GoMock package.
package clickhouse
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/clickhouse/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=clickhouse
//
package clickhouse
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, desc)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, desc)
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/datasource/couchbase/couchbase.go
================================================
package couchbase
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/couchbase/gocb/v2"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// Error variables for the couchbase package.
var (
errStatusDown = errors.New("status down")
errMissingField = errors.New("missing required field in config")
errWrongResultType = errors.New("result must be *gocb.MutationResult or **gocb.MutationResult")
errBucketNotInitialized = errors.New("couchbase bucket is not initialized")
errClustertNotInitialized = errors.New("couchbase cluster is not initialized")
errFailedToUnmarshalN1QL = errors.New("failed to unmarshal N1QL results into target")
errFailedToUnmarshalAnalytics = errors.New("failed to unmarshal analytics results into target")
)
const defaultTimeout = 5 * time.Second
// Client represents a Couchbase client that interacts with a Couchbase cluster.
type Client struct {
cluster clusterProvider
bucket bucketProvider
config *Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
// Collection represents a handle to a Couchbase collection.
type Collection struct {
collection collectionProvider
client *Client
}
// Scope represents a handle to a Couchbase scope.
type Scope struct {
scope scopeProvider
client *Client
}
// Config holds the configuration parameters for connecting to a Couchbase cluster.
type Config struct {
Host string
User string
Password string
Bucket string
URI string
ConnectionTimeout time.Duration
}
// Health represents the health status of the Couchbase connection.
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// New creates a new Couchbase client with the provided configuration.
func New(c *Config) *Client {
return &Client{config: c}
}
// UseLogger sets the logger for the Couchbase client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics collector for the Couchbase client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the Couchbase client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// sendOperationStats sends statistics about a Couchbase operation.
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
c.metrics.RecordHistogram(context.Background(), "app_couchbase_stats", float64(duration), "hostname", c.config.Host,
"bucket", c.config.Bucket, "type", method)
}
// Connect establishes a connection to the Couchbase cluster.
func (c *Client) Connect() {
uri, err := c.generateCouchbaseURI()
if err != nil {
c.logger.Errorf("error generating Couchbase URI: %v", err)
return
}
c.logger.Debugf("connecting to Couchbase at %v to bucket %v", c.config.Host, c.config.Bucket)
if err := c.establishConnection(uri); err != nil {
c.logger.Errorf("error while connecting to Couchbase, err:%v", err)
return
}
if err := c.waitForClusterReady(); err != nil {
c.logger.Errorf("could not connect to Couchbase at %v due to err: %v", c.config.Host, err)
return
}
c.bucket = c.cluster.Bucket(c.config.Bucket)
if err := c.waitForBucketReady(); err != nil {
c.logger.Errorf("could not connect to bucket %v at %v due to err: %v", c.config.Bucket, c.config.Host, err)
return
}
couchbaseBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_couchbase_stats", "Response time of Couchbase queries in milliseconds.", couchbaseBuckets...)
c.logger.Logf("connected to Couchbase at %v to bucket %v", c.config.Host, c.config.Bucket)
}
// generateCouchbaseURI generates the Couchbase connection URI.
func (c *Client) generateCouchbaseURI() (string, error) {
if c.config.URI != "" {
return c.config.URI, nil
}
if c.config.Host == "" {
return "", fmt.Errorf("%w: host is empty", errMissingField)
}
return fmt.Sprintf("couchbase://%s", c.config.Host), nil
}
// establishConnection establishes a connection to the Couchbase cluster.
func (c *Client) establishConnection(uri string) error {
cluster, err := gocb.Connect(uri, gocb.ClusterOptions{
Authenticator: gocb.PasswordAuthenticator{
Username: c.config.User,
Password: c.config.Password,
},
})
if err != nil {
return err
}
c.cluster = &clusterWrapper{cluster}
return nil
}
// waitForClusterReady waits for the Couchbase cluster to be ready.
func (c *Client) waitForClusterReady() error {
timeout := c.getTimeout()
return c.cluster.WaitUntilReady(timeout, nil)
}
// waitForBucketReady waits for the Couchbase bucket to be ready.
func (c *Client) waitForBucketReady() error {
timeout := c.getTimeout()
return c.bucket.WaitUntilReady(timeout, nil)
}
// getTimeout returns the connection timeout.
func (c *Client) getTimeout() time.Duration {
if c.config.ConnectionTimeout == 0 {
return defaultTimeout
}
return c.config.ConnectionTimeout
}
// HealthCheck performs a health check on the Couchbase connection.
func (c *Client) HealthCheck(_ context.Context) (any, error) {
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.config.URI
h.Details["bucket"] = c.config.Bucket
_, err := c.cluster.Ping(nil)
if err != nil {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
return &h, nil
}
// defaultCollection returns a handle for the default collection.
func (c *Client) defaultCollection() *Collection {
if c.bucket == nil {
c.logger.Error("bucket not initialized")
return &Collection{client: c}
}
return &Collection{
collection: c.bucket.DefaultCollection(),
client: c,
}
}
// scope returns a handle for a specific scope.
func (c *Client) scope(name string) *Scope {
if c.bucket == nil {
c.logger.Error("bucket not initialized")
return &Scope{client: c}
}
return &Scope{
scope: c.bucket.Scope(name),
client: c,
}
}
// mutationOperation performs a mutation operation on the collection.
func (c *Collection) mutationOperation(ctx context.Context, opName, key string, document, result any,
op func(tracerCtx context.Context) (*gocb.MutationResult, error),
) error {
if c.collection == nil {
return errBucketNotInitialized
}
tracerCtx, span := c.client.addTrace(ctx, opName, key)
startTime := time.Now()
mr, err := op(tracerCtx)
// Finish span with error status
c.client.finishSpan(span, err)
defer c.client.sendOperationStats(&QueryLog{Query: opName, Key: key, Parameters: document}, startTime, opName)
if err != nil {
return fmt.Errorf("failed to %s document with key %s: %w", opName, key, err)
}
switch r := result.(type) {
case *gocb.MutationResult:
*r = *mr
case **gocb.MutationResult:
*r = mr
case nil:
default:
return errWrongResultType
}
return nil
}
// Upsert performs an upsert operation on the collection.
func (c *Collection) Upsert(ctx context.Context, key string, document, result any) error {
return c.mutationOperation(ctx, "Upsert", key, document, result, func(tracerCtx context.Context) (*gocb.MutationResult, error) {
return c.collection.Upsert(key, document, &gocb.UpsertOptions{Context: tracerCtx})
})
}
// Insert inserts a new document in the collection.
func (c *Collection) Insert(ctx context.Context, key string, document, result any) error {
return c.mutationOperation(ctx, "Insert", key, document, result, func(tracerCtx context.Context) (*gocb.MutationResult, error) {
return c.collection.Insert(key, document, &gocb.InsertOptions{Context: tracerCtx})
})
}
// Remove removes a document from the collection.
func (c *Collection) Remove(ctx context.Context, key string) error {
if c.collection == nil {
return errBucketNotInitialized
}
tracerCtx, span := c.client.addTrace(ctx, "Remove", key)
startTime := time.Now()
_, err := c.collection.Remove(key, &gocb.RemoveOptions{Context: tracerCtx})
// Finish span with error status
c.client.finishSpan(span, err)
defer c.client.sendOperationStats(&QueryLog{Query: "Remove", Key: key}, startTime, "Remove")
if err != nil {
return fmt.Errorf("failed to remove document with key %s: %w", key, err)
}
return nil
}
// executeTracedQuery executes a traced query.
func (c *Client) executeTracedQuery(
ctx context.Context,
statement string,
params map[string]any,
result any,
operation string,
queryType string,
queryFn func(tracerCtx context.Context) (resultProvider, error),
) error {
if c.cluster == nil {
return errClustertNotInitialized
}
tracerCtx, span := c.addTrace(ctx, operation, statement)
// Add query parameters as span attributes if they exist
if len(params) > 0 && c.tracer != nil {
// Only add a count of parameters to avoid sensitive data leakage
span.SetAttributes(attribute.Int("db.couchbase.parameter_count", len(params)))
}
startTime := time.Now()
err := executeQuery(func() (resultProvider, error) {
return queryFn(tracerCtx)
}, queryType, result)
// Finish span with error status
c.finishSpan(span, err)
defer c.sendOperationStats(&QueryLog{Query: operation, Statement: statement, Parameters: params}, startTime, operation)
if err != nil {
c.logger.Errorf("%s query failed: %v", queryType, err)
}
return err
}
// Query executes a N1QL query against the Couchbase cluster.
func (c *Client) Query(ctx context.Context, statement string, params map[string]any, result any) error {
queryFn := func(tracerCtx context.Context) (resultProvider, error) {
opts := &gocb.QueryOptions{Context: tracerCtx}
if params != nil {
opts.NamedParameters = params
}
return c.cluster.Query(statement, opts)
}
return c.executeTracedQuery(ctx, statement, params, result, "N1QLQuery", "N1QL", queryFn)
}
// AnalyticsQuery executes an Analytics query against the Couchbase Analytics service.
func (c *Client) AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error {
queryFn := func(tracerCtx context.Context) (resultProvider, error) {
opts := &gocb.AnalyticsOptions{Context: tracerCtx}
if params != nil {
opts.NamedParameters = params
}
return c.cluster.AnalyticsQuery(statement, opts)
}
return c.executeTracedQuery(ctx, statement, params, result, "AnalyticsQuery", "Analytics", queryFn)
}
// executeQuery executes a query and processes the results.
func executeQuery(queryFn func() (resultProvider, error), queryType string, result any) error {
rows, err := queryFn()
if err != nil {
return fmt.Errorf("%s query failed: %w", queryType, err)
}
defer rows.Close()
var tempResults []map[string]any
for rows.Next() {
var row map[string]any
if err = rows.Row(&row); err != nil {
return fmt.Errorf("failed to unmarshal %s query row into map: %w", queryType, err)
}
tempResults = append(tempResults, row)
}
if err = rows.Err(); err != nil {
return fmt.Errorf("%s query iteration error: %w", queryType, err)
}
data, err := json.Marshal(tempResults)
if err != nil {
return fmt.Errorf("failed to marshal %s results: %w", queryType, err)
}
if err := json.Unmarshal(data, result); err != nil {
switch queryType {
case "N1QL":
return fmt.Errorf("%w: %w", errFailedToUnmarshalN1QL, err)
case "Analytics":
return fmt.Errorf("%w: %w", errFailedToUnmarshalAnalytics, err)
}
}
return nil
}
// RunTransaction executes a transaction.
func (c *Client) RunTransaction(ctx context.Context, logic func(any) error) (any, error) {
if c.cluster == nil {
return nil, errClustertNotInitialized
}
_, span := c.addTrace(ctx, "RunTransaction", "transaction")
defer span.End()
startTime := time.Now()
// Wrap the generic logic function to match the expected signature
wrappedLogic := func(t *gocb.TransactionAttemptContext) error {
return logic(t)
}
// gocb transactions are not directly context-aware in the Run method signature in the same way as other operations.
// The context is passed down to operations within the transaction lambda.
result, err := c.cluster.Transactions().Run(wrappedLogic, nil)
defer c.sendOperationStats(&QueryLog{Query: "RunTransaction"}, startTime, "RunTransaction")
if err != nil {
c.logger.Errorf("Transaction failed: %v", err)
}
return result, err
}
// Get performs a get operation on the default collection.
func (c *Client) Get(ctx context.Context, key string, result any) error {
return c.defaultCollection().Get(ctx, key, result)
}
// Insert inserts a new document in the default collection.
func (c *Client) Insert(ctx context.Context, key string, document, result any) error {
return c.defaultCollection().Insert(ctx, key, document, result)
}
// Upsert performs an upsert operation on the default collection.
func (c *Client) Upsert(ctx context.Context, key string, document, result any) error {
return c.defaultCollection().Upsert(ctx, key, document, result)
}
// Remove performs a remove operation on the default collection.
func (c *Client) Remove(ctx context.Context, key string) error {
return c.defaultCollection().Remove(ctx, key)
}
// Close closes the connection to the Couchbase cluster.
func (c *Client) Close(opts any) error {
if c.cluster != nil {
return c.cluster.Close(opts.(*gocb.ClusterCloseOptions))
}
return nil
}
// addTrace adds a trace to the context.
func (c *Client) addTrace(ctx context.Context, method, statement string) (context.Context, trace.Span) {
if c.tracer == nil {
// Return a no-op span when tracer is not available
return ctx, trace.SpanFromContext(ctx)
}
// Set the span attributes following OpenTelemetry semantic conventions
attrs := []attribute.KeyValue{
attribute.String("db.system", "couchbase"),
attribute.String("db.operation", method),
attribute.String("db.name", c.config.Bucket),
attribute.String("server.address", c.config.Host),
}
// Add statement/key information based on the operation
if statement != "" {
if method == "Get" || method == "Insert" || method == "Upsert" || method == "Remove" {
attrs = append(attrs, attribute.String("db.couchbase.document_key", statement))
} else {
attrs = append(attrs, attribute.String("db.statement", statement))
}
}
// Create a new span with proper naming
spanName := fmt.Sprintf("couchbase.%s", strings.ToLower(method))
ctx, span := c.tracer.Start(ctx, spanName, trace.WithAttributes(attrs...))
return ctx, span
}
// finishSpan finishes a trace span.
func (*Client) finishSpan(span trace.Span, err error) {
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
} else {
span.SetStatus(codes.Ok, "")
}
span.End()
}
// Get performs a get operation on the collection.
func (c *Collection) Get(ctx context.Context, key string, result any) error {
if c.collection == nil {
return errBucketNotInitialized
}
tracerCtx, span := c.client.addTrace(ctx, "Get", key)
startTime := time.Now()
res, err := c.collection.Get(key, &gocb.GetOptions{Context: tracerCtx})
// Finish span with error status
c.client.finishSpan(span, err)
defer c.client.sendOperationStats(&QueryLog{Query: "Get", Key: key}, startTime, "Get")
if err != nil {
c.client.logger.Errorf("failed to get document with key %s: %v", key, err)
return fmt.Errorf("failed to get document with key %s: %w", key, err)
}
if err = res.Content(result); err != nil {
return fmt.Errorf("failed to unmarshal document content for key %s: %w", key, err)
}
return nil
}
================================================
FILE: pkg/gofr/datasource/couchbase/couchbase_test.go
================================================
package couchbase
import (
"encoding/json"
"errors"
"testing"
"time"
"github.com/couchbase/gocb/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
)
var (
errMockTransaction = errors.New("transaction failed")
errLogic = errors.New("logic error")
)
type testMocks struct {
logger *MockLogger
metrics *MockMetrics
cluster *MockclusterProvider
bucket *MockbucketProvider
transactions *MocktransactionsProvider
collection *MockcollectionProvider
getResult *MockgetResultProvider
scope *MockscopeProvider
queryResult *MockresultProvider
}
func newTestMocks(t *testing.T) *testMocks {
t.Helper()
ctrl := gomock.NewController(t)
return &testMocks{
logger: NewMockLogger(ctrl),
metrics: NewMockMetrics(ctrl),
cluster: NewMockclusterProvider(ctrl),
bucket: NewMockbucketProvider(ctrl),
transactions: NewMocktransactionsProvider(ctrl),
collection: NewMockcollectionProvider(ctrl),
getResult: NewMockgetResultProvider(ctrl),
scope: NewMockscopeProvider(ctrl),
queryResult: NewMockresultProvider(ctrl),
}
}
func TestClient_New(t *testing.T) {
client := New(&Config{
Host: "localhost",
User: "Administrator",
Password: "password",
Bucket: "gofr",
ConnectionTimeout: time.Second * 5,
URI: "",
})
require.NotNil(t, client)
}
func TestClient_Upsert(t *testing.T) {
tests := []struct {
name string
key string
document any
result any
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: upsert document with *gocb.MutationResult",
key: "test-key",
document: map[string]string{"key": "value"},
result: &gocb.MutationResult{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Upsert("test-key", gomock.Any(), gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "success: upsert document with **gocb.MutationResult",
key: "test-key",
document: map[string]string{"key": "value"},
result: func() **gocb.MutationResult { var res *gocb.MutationResult; return &res }(),
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Upsert("test-key", gomock.Any(), gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: from collection.Upsert",
key: "test-key",
document: map[string]string{"key": "value"},
result: &gocb.MutationResult{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Upsert("test-key", gomock.Any(), gomock.Any()).Return(nil, gocb.ErrDocumentExists),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDocumentExists,
},
{
name: "error: wrong result type",
key: "test-key",
document: map[string]string{"key": "value"},
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Upsert("test-key", gomock.Any(), gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: errWrongResultType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.Upsert(t.Context(), tt.key, tt.document, tt.result)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestClient_Insert(t *testing.T) {
tests := []struct {
name string
key string
document any
result any
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: insert document with *gocb.MutationResult",
key: "test-key",
document: map[string]string{"key": "value"},
result: &gocb.MutationResult{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Insert("test-key", gomock.Any(), gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: from collection.Insert",
key: "test-key",
document: map[string]string{"key": "value"},
result: &gocb.MutationResult{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Insert("test-key", gomock.Any(), gomock.Any()).Return(nil, gocb.ErrDocumentExists),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDocumentExists,
},
{
name: "error: wrong result type",
key: "test-key",
document: map[string]string{"key": "value"},
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Insert("test-key", gomock.Any(), gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()),
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes(),
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: errWrongResultType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.Insert(t.Context(), tt.key, tt.document, tt.result)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestClient_Get(t *testing.T) {
tests := []struct {
name string
key string
result any
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: get document",
key: "test-key",
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection)
mocks.collection.EXPECT().Get("test-key", gomock.Any()).Return(mocks.getResult, nil)
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.getResult.EXPECT().Content(gomock.Any()).Return(nil)
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: from collection.Get",
key: "test-key",
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Get("test-key", gomock.Any()).Return(nil, gocb.ErrDocumentNotFound),
mocks.logger.EXPECT().Debug(gomock.Any()),
)
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any())
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDocumentNotFound,
},
{
name: "error: from getResult.Content",
key: "test-key",
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Get("test-key", gomock.Any()).Return(mocks.getResult, nil),
mocks.getResult.EXPECT().Content(gomock.Any()).Return(gocb.ErrDecodingFailure),
)
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDecodingFailure,
},
{
name: "error: bucket not initialized",
key: "test-key",
result: nil,
setup: func(mocks *testMocks) *Client {
mocks.logger.EXPECT().Error("bucket not initialized")
client := &Client{bucket: nil}
client.UseLogger(mocks.logger)
return client
},
wantErr: errBucketNotInitialized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.Get(t.Context(), tt.key, tt.result)
assert.ErrorIs(t, err, tt.wantErr)
})
}
}
func TestClient_Remove(t *testing.T) {
tests := []struct {
name string
key string
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: remove document",
key: "test-key",
setup: func(mocks *testMocks) *Client {
gomock.InOrder(
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Remove("test-key", gomock.Any()).Return(&gocb.MutationResult{}, nil),
mocks.logger.EXPECT().Debug(gomock.Any()),
)
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: from collection.Remove",
key: "test-key",
setup: func(mocks *testMocks) *Client {
gomock.InOrder(
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection),
mocks.collection.EXPECT().Remove("test-key", gomock.Any()).Return(nil, gocb.ErrDocumentNotFound),
mocks.logger.EXPECT().Debug(gomock.Any()),
)
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, bucket: mocks.bucket, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDocumentNotFound,
},
{
name: "error: bucket not initialized",
key: "test-key",
setup: func(mocks *testMocks) *Client {
mocks.logger.EXPECT().Error("bucket not initialized")
client := &Client{bucket: nil}
client.UseLogger(mocks.logger)
return client
},
wantErr: errBucketNotInitialized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.Remove(t.Context(), tt.key)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else {
require.NoError(t, err)
}
})
}
}
func TestClient_DefaultCollection(t *testing.T) {
tests := []struct {
name string
setup func(mocks *testMocks) *Client
wantCollection *Collection
}{
{
name: "success: default collection returned",
setup: func(mocks *testMocks) *Client {
mocks.bucket.EXPECT().DefaultCollection().Return(mocks.collection)
return &Client{
bucket: mocks.bucket,
logger: mocks.logger,
metrics: mocks.metrics,
tracer: noop.NewTracerProvider().Tracer("test"),
}
},
wantCollection: &Collection{
collection: NewMockcollectionProvider(gomock.NewController(t)),
},
},
{
name: "error: bucket not initialized",
setup: func(mocks *testMocks) *Client {
mocks.logger.EXPECT().Error("bucket not initialized")
return &Client{
bucket: nil,
logger: mocks.logger,
}
},
wantCollection: &Collection{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
got := client.defaultCollection()
// We cannot directly compare the collection, so we check for nil and non-nil cases.
if tt.wantCollection == nil {
assert.Nil(t, got)
} else {
assert.NotNil(t, got)
}
})
}
}
func TestClient_Scope(t *testing.T) {
tests := []struct {
name string
scopeName string
setup func(mocks *testMocks) *Client
wantScope *Scope
}{
{
name: "success: scope returned",
scopeName: "test-scope",
setup: func(mocks *testMocks) *Client {
mocks.bucket.EXPECT().Scope("test-scope").Return(mocks.scope)
return &Client{
bucket: mocks.bucket,
logger: mocks.logger,
metrics: mocks.metrics,
tracer: noop.NewTracerProvider().Tracer("test"),
}
},
wantScope: &Scope{
scope: NewMockscopeProvider(gomock.NewController(t)),
},
},
{
name: "error: bucket not initialized",
scopeName: "test-scope",
setup: func(mocks *testMocks) *Client {
mocks.logger.EXPECT().Error("bucket not initialized")
return &Client{
bucket: nil,
logger: mocks.logger,
}
},
wantScope: &Scope{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
got := client.scope(tt.scopeName)
if tt.wantScope == nil {
assert.Nil(t, got)
} else {
assert.NotNil(t, got)
}
})
}
}
func TestClient_RunTransaction(t *testing.T) {
tests := []struct {
name string
setup func(mocks *testMocks) *Client
logic func(any) error
wantErr error
}{
{
name: "success: transaction runs",
setup: func(mocks *testMocks) *Client {
mocks.cluster.EXPECT().Transactions().Return(mocks.transactions)
mocks.transactions.EXPECT().Run(gomock.Any(), gomock.Any()).Return(&gocb.TransactionResult{}, nil)
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
bucket: mocks.bucket,
cluster: mocks.cluster,
logger: mocks.logger,
metrics: mocks.metrics,
config: &Config{Bucket: "bucket"},
tracer: noop.NewTracerProvider().Tracer("test"),
}
},
logic: func(any) error {
return nil
},
},
{
name: "error: cluster not initialized",
setup: func(*testMocks) *Client {
return &Client{cluster: nil}
},
logic: func(any) error {
return nil
},
wantErr: errClustertNotInitialized,
},
{
name: "error: transaction fails",
setup: func(mocks *testMocks) *Client {
mocks.cluster.EXPECT().Transactions().Return(mocks.transactions)
mocks.transactions.EXPECT().Run(gomock.Any(), gomock.Any()).Return(nil, errMockTransaction)
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster,
config: &Config{Bucket: "bucket"},
logger: mocks.logger,
metrics: mocks.metrics,
tracer: noop.NewTracerProvider().Tracer("test"),
}
},
logic: func(any) error {
return errLogic
},
wantErr: errMockTransaction,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
_, err := client.RunTransaction(t.Context(), tt.logic)
if tt.wantErr != nil {
assert.ErrorContains(t, err, tt.wantErr.Error())
} else {
require.NoError(t, err)
}
})
}
}
func TestClient_Query(t *testing.T) {
tests := []struct {
name string
statement string
params map[string]any
result any
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: N1QL query",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &[]map[string]any{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.cluster.EXPECT().Query(gomock.Any(), gomock.Any()).Return(mocks.queryResult, nil),
mocks.queryResult.EXPECT().Next().Return(true),
mocks.queryResult.EXPECT().Row(gomock.Any()).DoAndReturn(func(arg any) error {
data := `{"id": "1", "name": "test"}`
return json.Unmarshal([]byte(data), arg)
}),
mocks.queryResult.EXPECT().Next().Return(false),
mocks.queryResult.EXPECT().Err().Return(nil),
mocks.queryResult.EXPECT().Close().Return(nil))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: from cluster.Query",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &[]map[string]any{},
setup: func(mocks *testMocks) *Client {
mocks.cluster.EXPECT().Query(gomock.Any(), gomock.Any()).Return(nil, gocb.ErrPlanningFailure)
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrPlanningFailure,
},
{
name: "error: failed to unmarshal N1QL results into target",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.cluster.EXPECT().Query(gomock.Any(), gomock.Any()).Return(mocks.queryResult, nil),
mocks.queryResult.EXPECT().Next().Return(true),
mocks.queryResult.EXPECT().Row(gomock.Any()).DoAndReturn(func(arg any) error {
data := `{"id": "1", "name": "test"}`
return json.Unmarshal([]byte(data), arg)
}),
mocks.queryResult.EXPECT().Next().Return(false),
mocks.queryResult.EXPECT().Err().Return(nil),
mocks.queryResult.EXPECT().Close().Return(nil))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: errFailedToUnmarshalN1QL,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.Query(t.Context(), tt.statement, tt.params, tt.result)
if tt.wantErr != nil {
assert.ErrorContains(t, err, tt.wantErr.Error())
} else {
require.NoError(t, err)
}
})
}
}
func TestClient_UseLogger(t *testing.T) {
ctrl := gomock.NewController(t)
logger := NewMockLogger(ctrl)
client := New(&Config{})
client.UseLogger(logger)
assert.Equal(t, logger, client.logger)
}
func TestClient_UseMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
client := New(&Config{})
client.UseMetrics(metrics)
assert.Equal(t, metrics, client.metrics)
}
func TestClient_UseTracer(t *testing.T) {
client := New(&Config{})
provider := noop.NewTracerProvider()
tracer := provider.Tracer("test")
client.UseTracer(tracer)
assert.Equal(t, tracer, client.tracer)
}
func TestClient_Close(t *testing.T) {
mocks := newTestMocks(t)
mocks.cluster.EXPECT().Close(&gocb.ClusterCloseOptions{}).Return(nil)
client := &Client{
cluster: mocks.cluster,
}
err := client.Close(&gocb.ClusterCloseOptions{})
require.NoError(t, err)
}
func TestClient_HealthCheck(t *testing.T) {
tests := []struct {
name string
setupMocks func(mocks *testMocks)
wantStatus string
wantErr error
}{
{
name: "success: cluster is up",
setupMocks: func(mocks *testMocks) {
mocks.cluster.EXPECT().Ping(nil).Return(&gocb.PingResult{}, nil)
},
wantStatus: "UP",
wantErr: nil,
},
{
name: "error: cluster is down",
setupMocks: func(mocks *testMocks) {
mocks.cluster.EXPECT().Ping(nil).Return(nil, gocb.ErrUnambiguousTimeout)
},
wantStatus: "DOWN",
wantErr: errStatusDown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
tt.setupMocks(mocks)
client := &Client{
cluster: mocks.cluster,
config: &Config{
Host: "localhost",
Bucket: "gofr",
},
}
health, err := client.HealthCheck(t.Context())
require.ErrorIs(t, err, tt.wantErr)
assert.Equal(t, tt.wantStatus, health.(*Health).Status)
})
}
}
func Test_generateCouchbaseURI(t *testing.T) {
tests := []struct {
name string
config *Config
want string
wantErr bool
}{
{
name: "success: with host",
config: &Config{
Host: "localhost",
},
want: "couchbase://localhost",
wantErr: false,
},
{
name: "success: with URI",
config: &Config{
URI: "couchbase://remotehost",
},
want: "couchbase://remotehost",
wantErr: false,
},
{
name: "error: missing host and URI",
config: &Config{},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{config: tt.config}
got, err := c.generateCouchbaseURI()
if (err != nil) != tt.wantErr {
t.Errorf("generateCouchbaseURI() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("generateCouchbaseURI() = %v, want %v", got, tt.want)
}
})
}
}
func TestClient_AnalyticsQuery(t *testing.T) {
tests := []struct {
name string
statement string
params map[string]any
result any
setup func(mocks *testMocks) *Client
wantErr error
}{
{
name: "success: Analytics query",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &[]map[string]any{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.cluster.EXPECT().AnalyticsQuery(gomock.Any(), gomock.Any()).Return(mocks.queryResult, nil),
mocks.queryResult.EXPECT().Next().Return(true),
mocks.queryResult.EXPECT().Row(gomock.Any()).DoAndReturn(func(arg any) error {
data := `{"id": "1", "name": "test_analytics"}`
return json.Unmarshal([]byte(data), arg)
}),
mocks.queryResult.EXPECT().Next().Return(false),
mocks.queryResult.EXPECT().Err().Return(nil),
mocks.queryResult.EXPECT().Close().Return(nil))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
},
{
name: "error: failed to unmarshal Analytics query row",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &[]map[string]any{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.cluster.EXPECT().AnalyticsQuery(gomock.Any(), gomock.Any()).Return(mocks.queryResult, nil),
mocks.queryResult.EXPECT().Next().Return(true),
mocks.queryResult.EXPECT().Row(gomock.Any()).Return(gocb.ErrDecodingFailure),
mocks.queryResult.EXPECT().Close().Return(nil))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: gocb.ErrDecodingFailure,
},
{
name: "error: failed to unmarshal analytics results into target",
statement: "SELECT * FROM `bucket`",
params: nil,
result: &struct{}{},
setup: func(mocks *testMocks) *Client {
gomock.InOrder(mocks.cluster.EXPECT().AnalyticsQuery(gomock.Any(), gomock.Any()).Return(mocks.queryResult, nil),
mocks.queryResult.EXPECT().Next().Return(true),
mocks.queryResult.EXPECT().Row(gomock.Any()).DoAndReturn(func(arg any) error {
data := `{"id": "1", "name": "test_analytics"}`
return json.Unmarshal([]byte(data), arg)
}),
mocks.queryResult.EXPECT().Next().Return(false),
mocks.queryResult.EXPECT().Err().Return(nil),
mocks.queryResult.EXPECT().Close().Return(nil))
mocks.metrics.EXPECT().RecordHistogram(gomock.Any(), "app_couchbase_stats", gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Debug(gomock.Any())
mocks.logger.EXPECT().Logf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mocks.logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return &Client{
cluster: mocks.cluster, config: &Config{}, logger: mocks.logger, metrics: mocks.metrics,
}
},
wantErr: errFailedToUnmarshalAnalytics,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mocks := newTestMocks(t)
client := tt.setup(mocks)
err := client.AnalyticsQuery(t.Context(), tt.statement, tt.params, tt.result)
if tt.wantErr != nil {
assert.ErrorContains(t, err, tt.wantErr.Error())
} else {
require.NoError(t, err)
}
})
}
}
================================================
FILE: pkg/gofr/datasource/couchbase/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/couchbase
go 1.25.0
require (
github.com/couchbase/gocb/v2 v2.12.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/couchbase/gocbcore/v10 v10.9.0 // indirect
github.com/couchbase/gocbcoreps v0.1.5-0.20260107140814-1c3a03f888f8 // indirect
github.com/couchbase/goprotostellar v1.0.5 // indirect
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/couchbase/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/couchbase/gocb/v2 v2.12.0 h1:IIIhOLJJHXHJ5Y876tgmhG9osmOaDPuepycJyJKj/14=
github.com/couchbase/gocb/v2 v2.12.0/go.mod h1:MVrScUfHQI+/wIg5BJZd2LefgW+0sn9FfK2x89mW10Y=
github.com/couchbase/gocbcore/v10 v10.9.0 h1:+O1ZF9/BZN2wE8qrPUwatR4BsXcffdIOZ8Lj/0tY3s4=
github.com/couchbase/gocbcore/v10 v10.9.0/go.mod h1:OWKfU9R5Nm5V3QZBtfdZl5qCfgxtxTqOgXiNr4pn9/c=
github.com/couchbase/gocbcoreps v0.1.5-0.20260107140814-1c3a03f888f8 h1:WwGhY3TYn2INQo88yzEhUMYFlgjRInA1dgfEa3UhAxw=
github.com/couchbase/gocbcoreps v0.1.5-0.20260107140814-1c3a03f888f8/go.mod h1:AUR8DPPmvM+uMkb+Q01Y0mMXINdEY/jUL/qE+kPJ67s=
github.com/couchbase/goprotostellar v1.0.5 h1:pmR4H87zbYymIdTR1owyUZsfQ7NupkfCuNLW4FIPBhE=
github.com/couchbase/goprotostellar v1.0.5/go.mod h1:X58ot5FRqlBTBkwG/oI4klunpu4MApjGktheqeRWQw0=
github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8 h1:MQfvw4BiLTuyR69FuA5Kex+tXUeLkH+/ucJfVL1/hkM=
github.com/couchbaselabs/gocaves/client v0.0.0-20250107114554-f96479220ae8/go.mod h1:AVekAZwIY2stsJOMWLAS/0uA/+qdp7pjO8EHnl61QkY=
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0 h1:HU9DlAYYWR69jQnLN6cpg0fh0hxW/8d5hnglCXXjW78=
github.com/couchbaselabs/gocbconnstr/v2 v2.0.0/go.mod h1:o7T431UOfFVHDNvMBUmUxpHnhivwv7BziUao/nMl81E=
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/couchbase/interfaces.go
================================================
package couchbase
import (
"context"
"time"
gocb "github.com/couchbase/gocb/v2"
)
type Couchbase interface {
Get(ctx context.Context, key string, result any) error
Insert(ctx context.Context, key string, document, result any) error
Upsert(ctx context.Context, key string, document any, result any) error
Remove(ctx context.Context, key string) error
Query(ctx context.Context, statement string, params map[string]any, result any) error
AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error
RunTransaction(ctx context.Context, logic func(attempt *gocb.TransactionAttemptContext) error) (*gocb.TransactionResult, error)
Close(opts any) error
}
// clusterProvider is an interface that abstracts the gocb.Cluster for easier testing.
type clusterProvider interface {
Bucket(bucketName string) bucketProvider
Query(statement string, opts *gocb.QueryOptions) (resultProvider, error)
AnalyticsQuery(statement string, opts *gocb.AnalyticsOptions) (resultProvider, error)
WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error
Ping(opts *gocb.PingOptions) (*gocb.PingResult, error)
Close(opts *gocb.ClusterCloseOptions) error
Transactions() transactionsProvider
}
// resultProvider is an interface that abstracts gocb.QueryResult and gocb.AnalyticsResult for easier testing.
type resultProvider interface {
Next() bool
Row(value any) error
Err() error
Close() error
}
// bucketProvider is an interface that abstracts the gocb.Bucket for easier testing.
type bucketProvider interface {
Collection(collectionName string) collectionProvider
DefaultCollection() collectionProvider
WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error
Scope(name string) scopeProvider
}
// collectionProvider is an interface that abstracts the gocb.Collection for easier testing.
type collectionProvider interface {
Upsert(key string, value any, opts *gocb.UpsertOptions) (*gocb.MutationResult, error)
Insert(key string, value any, opts *gocb.InsertOptions) (*gocb.MutationResult, error)
Get(key string, opts *gocb.GetOptions) (getResultProvider, error)
Remove(key string, opts *gocb.RemoveOptions) (*gocb.MutationResult, error)
}
type getResultProvider interface {
Content(value any) error
}
// scopeProvider is an interface that abstracts the gocb.Scope for easier testing.
type scopeProvider interface {
Collection(name string) collectionProvider
}
// transactionsProvider is an interface that abstracts the gocb.Transactions for easier testing.
type transactionsProvider interface {
Run(logic func(*gocb.TransactionAttemptContext) error, opts *gocb.TransactionOptions) (*gocb.TransactionResult, error)
}
================================================
FILE: pkg/gofr/datasource/couchbase/logger.go
================================================
package couchbase
import (
"fmt"
"io"
"regexp"
"strings"
)
// Logger is an interface for logging messages.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
// QueryLog represents a log entry for a Couchbase query.
type QueryLog struct {
Query string `json:"query"`
Duration int64 `json:"duration"`
Key string `json:"key,omitempty"`
Statement string `json:"statement,omitempty"`
Parameters any `json:"parameters,omitempty"`
}
// PrettyPrint prints the query log in a human-readable format.
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
if ql.Key == "" && ql.Statement == "" {
return
}
if ql.Parameters == nil {
ql.Parameters = ""
}
query := ql.Query
if query == "" {
query = ql.Statement
}
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;207m%-6s\u001B[0m %8d\u001B[38;5;8m\b 5s\u001B[0m %s\n",
clean(query), "COUCHBASE", ql.Duration,
clean(strings.Join([]string{ql.Key + " " + fmt.Sprint(ql.Parameters)}, " ")))
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/couchbase/metrics.go
================================================
package couchbase
import "context"
// Metrics is an interface for collecting metrics.
type Metrics interface {
// NewHistogram creates a new histogram metric.
NewHistogram(name, desc string, buckets ...float64)
// RecordHistogram records a value in a histogram metric.
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/couchbase/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces.go
//
// Generated by this command:
//
// mockgen -source=interfaces.go -destination=mock_interfaces.go -package=couchbase
//
// Package couchbase is a generated GoMock package.
package couchbase
import (
context "context"
reflect "reflect"
time "time"
gocb "github.com/couchbase/gocb/v2"
gomock "go.uber.org/mock/gomock"
)
// MockCouchbase is a mock of Couchbase interface.
type MockCouchbase struct {
ctrl *gomock.Controller
recorder *MockCouchbaseMockRecorder
isgomock struct{}
}
// MockCouchbaseMockRecorder is the mock recorder for MockCouchbase.
type MockCouchbaseMockRecorder struct {
mock *MockCouchbase
}
// NewMockCouchbase creates a new mock instance.
func NewMockCouchbase(ctrl *gomock.Controller) *MockCouchbase {
mock := &MockCouchbase{ctrl: ctrl}
mock.recorder = &MockCouchbaseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCouchbase) EXPECT() *MockCouchbaseMockRecorder {
return m.recorder
}
// AnalyticsQuery mocks base method.
func (m *MockCouchbase) AnalyticsQuery(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AnalyticsQuery", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// AnalyticsQuery indicates an expected call of AnalyticsQuery.
func (mr *MockCouchbaseMockRecorder) AnalyticsQuery(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnalyticsQuery", reflect.TypeOf((*MockCouchbase)(nil).AnalyticsQuery), ctx, statement, params, result)
}
// Close mocks base method.
func (m *MockCouchbase) Close(opts any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close", opts)
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockCouchbaseMockRecorder) Close(opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockCouchbase)(nil).Close), opts)
}
// Get mocks base method.
func (m *MockCouchbase) Get(ctx context.Context, key string, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key, result)
ret0, _ := ret[0].(error)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockCouchbaseMockRecorder) Get(ctx, key, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCouchbase)(nil).Get), ctx, key, result)
}
// Insert mocks base method.
func (m *MockCouchbase) Insert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Insert indicates an expected call of Insert.
func (mr *MockCouchbaseMockRecorder) Insert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockCouchbase)(nil).Insert), ctx, key, document, result)
}
// Query mocks base method.
func (m *MockCouchbase) Query(ctx context.Context, statement string, params map[string]any, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, statement, params, result)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockCouchbaseMockRecorder) Query(ctx, statement, params, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockCouchbase)(nil).Query), ctx, statement, params, result)
}
// Remove mocks base method.
func (m *MockCouchbase) Remove(ctx context.Context, key string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", ctx, key)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockCouchbaseMockRecorder) Remove(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockCouchbase)(nil).Remove), ctx, key)
}
// RunTransaction mocks base method.
func (m *MockCouchbase) RunTransaction(ctx context.Context, logic func(*gocb.TransactionAttemptContext) error) (*gocb.TransactionResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RunTransaction", ctx, logic)
ret0, _ := ret[0].(*gocb.TransactionResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RunTransaction indicates an expected call of RunTransaction.
func (mr *MockCouchbaseMockRecorder) RunTransaction(ctx, logic any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RunTransaction", reflect.TypeOf((*MockCouchbase)(nil).RunTransaction), ctx, logic)
}
// Upsert mocks base method.
func (m *MockCouchbase) Upsert(ctx context.Context, key string, document, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upsert", ctx, key, document, result)
ret0, _ := ret[0].(error)
return ret0
}
// Upsert indicates an expected call of Upsert.
func (mr *MockCouchbaseMockRecorder) Upsert(ctx, key, document, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockCouchbase)(nil).Upsert), ctx, key, document, result)
}
// MockclusterProvider is a mock of clusterProvider interface.
type MockclusterProvider struct {
ctrl *gomock.Controller
recorder *MockclusterProviderMockRecorder
isgomock struct{}
}
// MockclusterProviderMockRecorder is the mock recorder for MockclusterProvider.
type MockclusterProviderMockRecorder struct {
mock *MockclusterProvider
}
// NewMockclusterProvider creates a new mock instance.
func NewMockclusterProvider(ctrl *gomock.Controller) *MockclusterProvider {
mock := &MockclusterProvider{ctrl: ctrl}
mock.recorder = &MockclusterProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockclusterProvider) EXPECT() *MockclusterProviderMockRecorder {
return m.recorder
}
// AnalyticsQuery mocks base method.
func (m *MockclusterProvider) AnalyticsQuery(statement string, opts *gocb.AnalyticsOptions) (resultProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AnalyticsQuery", statement, opts)
ret0, _ := ret[0].(resultProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AnalyticsQuery indicates an expected call of AnalyticsQuery.
func (mr *MockclusterProviderMockRecorder) AnalyticsQuery(statement, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnalyticsQuery", reflect.TypeOf((*MockclusterProvider)(nil).AnalyticsQuery), statement, opts)
}
// Bucket mocks base method.
func (m *MockclusterProvider) Bucket(bucketName string) bucketProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bucket", bucketName)
ret0, _ := ret[0].(bucketProvider)
return ret0
}
// Bucket indicates an expected call of Bucket.
func (mr *MockclusterProviderMockRecorder) Bucket(bucketName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bucket", reflect.TypeOf((*MockclusterProvider)(nil).Bucket), bucketName)
}
// Close mocks base method.
func (m *MockclusterProvider) Close(opts *gocb.ClusterCloseOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close", opts)
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockclusterProviderMockRecorder) Close(opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockclusterProvider)(nil).Close), opts)
}
// Ping mocks base method.
func (m *MockclusterProvider) Ping(opts *gocb.PingOptions) (*gocb.PingResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", opts)
ret0, _ := ret[0].(*gocb.PingResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ping indicates an expected call of Ping.
func (mr *MockclusterProviderMockRecorder) Ping(opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockclusterProvider)(nil).Ping), opts)
}
// Query mocks base method.
func (m *MockclusterProvider) Query(statement string, opts *gocb.QueryOptions) (resultProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", statement, opts)
ret0, _ := ret[0].(resultProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockclusterProviderMockRecorder) Query(statement, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockclusterProvider)(nil).Query), statement, opts)
}
// Transactions mocks base method.
func (m *MockclusterProvider) Transactions() transactionsProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Transactions")
ret0, _ := ret[0].(transactionsProvider)
return ret0
}
// Transactions indicates an expected call of Transactions.
func (mr *MockclusterProviderMockRecorder) Transactions() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockclusterProvider)(nil).Transactions))
}
// WaitUntilReady mocks base method.
func (m *MockclusterProvider) WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WaitUntilReady", timeout, opts)
ret0, _ := ret[0].(error)
return ret0
}
// WaitUntilReady indicates an expected call of WaitUntilReady.
func (mr *MockclusterProviderMockRecorder) WaitUntilReady(timeout, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilReady", reflect.TypeOf((*MockclusterProvider)(nil).WaitUntilReady), timeout, opts)
}
// MockresultProvider is a mock of resultProvider interface.
type MockresultProvider struct {
ctrl *gomock.Controller
recorder *MockresultProviderMockRecorder
isgomock struct{}
}
// MockresultProviderMockRecorder is the mock recorder for MockresultProvider.
type MockresultProviderMockRecorder struct {
mock *MockresultProvider
}
// NewMockresultProvider creates a new mock instance.
func NewMockresultProvider(ctrl *gomock.Controller) *MockresultProvider {
mock := &MockresultProvider{ctrl: ctrl}
mock.recorder = &MockresultProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockresultProvider) EXPECT() *MockresultProviderMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockresultProvider) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockresultProviderMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockresultProvider)(nil).Close))
}
// Err mocks base method.
func (m *MockresultProvider) Err() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(error)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockresultProviderMockRecorder) Err() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockresultProvider)(nil).Err))
}
// Next mocks base method.
func (m *MockresultProvider) Next() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Next")
ret0, _ := ret[0].(bool)
return ret0
}
// Next indicates an expected call of Next.
func (mr *MockresultProviderMockRecorder) Next() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockresultProvider)(nil).Next))
}
// Row mocks base method.
func (m *MockresultProvider) Row(value any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Row", value)
ret0, _ := ret[0].(error)
return ret0
}
// Row indicates an expected call of Row.
func (mr *MockresultProviderMockRecorder) Row(value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Row", reflect.TypeOf((*MockresultProvider)(nil).Row), value)
}
// MockbucketProvider is a mock of bucketProvider interface.
type MockbucketProvider struct {
ctrl *gomock.Controller
recorder *MockbucketProviderMockRecorder
isgomock struct{}
}
// MockbucketProviderMockRecorder is the mock recorder for MockbucketProvider.
type MockbucketProviderMockRecorder struct {
mock *MockbucketProvider
}
// NewMockbucketProvider creates a new mock instance.
func NewMockbucketProvider(ctrl *gomock.Controller) *MockbucketProvider {
mock := &MockbucketProvider{ctrl: ctrl}
mock.recorder = &MockbucketProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockbucketProvider) EXPECT() *MockbucketProviderMockRecorder {
return m.recorder
}
// Collection mocks base method.
func (m *MockbucketProvider) Collection(collectionName string) collectionProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Collection", collectionName)
ret0, _ := ret[0].(collectionProvider)
return ret0
}
// Collection indicates an expected call of Collection.
func (mr *MockbucketProviderMockRecorder) Collection(collectionName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collection", reflect.TypeOf((*MockbucketProvider)(nil).Collection), collectionName)
}
// DefaultCollection mocks base method.
func (m *MockbucketProvider) DefaultCollection() collectionProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DefaultCollection")
ret0, _ := ret[0].(collectionProvider)
return ret0
}
// DefaultCollection indicates an expected call of DefaultCollection.
func (mr *MockbucketProviderMockRecorder) DefaultCollection() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DefaultCollection", reflect.TypeOf((*MockbucketProvider)(nil).DefaultCollection))
}
// Scope mocks base method.
func (m *MockbucketProvider) Scope(name string) scopeProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Scope", name)
ret0, _ := ret[0].(scopeProvider)
return ret0
}
// Scope indicates an expected call of Scope.
func (mr *MockbucketProviderMockRecorder) Scope(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scope", reflect.TypeOf((*MockbucketProvider)(nil).Scope), name)
}
// WaitUntilReady mocks base method.
func (m *MockbucketProvider) WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WaitUntilReady", timeout, opts)
ret0, _ := ret[0].(error)
return ret0
}
// WaitUntilReady indicates an expected call of WaitUntilReady.
func (mr *MockbucketProviderMockRecorder) WaitUntilReady(timeout, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilReady", reflect.TypeOf((*MockbucketProvider)(nil).WaitUntilReady), timeout, opts)
}
// MockcollectionProvider is a mock of collectionProvider interface.
type MockcollectionProvider struct {
ctrl *gomock.Controller
recorder *MockcollectionProviderMockRecorder
isgomock struct{}
}
// MockcollectionProviderMockRecorder is the mock recorder for MockcollectionProvider.
type MockcollectionProviderMockRecorder struct {
mock *MockcollectionProvider
}
// NewMockcollectionProvider creates a new mock instance.
func NewMockcollectionProvider(ctrl *gomock.Controller) *MockcollectionProvider {
mock := &MockcollectionProvider{ctrl: ctrl}
mock.recorder = &MockcollectionProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockcollectionProvider) EXPECT() *MockcollectionProviderMockRecorder {
return m.recorder
}
// Get mocks base method.
func (m *MockcollectionProvider) Get(key string, opts *gocb.GetOptions) (getResultProvider, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key, opts)
ret0, _ := ret[0].(getResultProvider)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockcollectionProviderMockRecorder) Get(key, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockcollectionProvider)(nil).Get), key, opts)
}
// Insert mocks base method.
func (m *MockcollectionProvider) Insert(key string, value any, opts *gocb.InsertOptions) (*gocb.MutationResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Insert", key, value, opts)
ret0, _ := ret[0].(*gocb.MutationResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Insert indicates an expected call of Insert.
func (mr *MockcollectionProviderMockRecorder) Insert(key, value, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Insert", reflect.TypeOf((*MockcollectionProvider)(nil).Insert), key, value, opts)
}
// Remove mocks base method.
func (m *MockcollectionProvider) Remove(key string, opts *gocb.RemoveOptions) (*gocb.MutationResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", key, opts)
ret0, _ := ret[0].(*gocb.MutationResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Remove indicates an expected call of Remove.
func (mr *MockcollectionProviderMockRecorder) Remove(key, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockcollectionProvider)(nil).Remove), key, opts)
}
// Upsert mocks base method.
func (m *MockcollectionProvider) Upsert(key string, value any, opts *gocb.UpsertOptions) (*gocb.MutationResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upsert", key, value, opts)
ret0, _ := ret[0].(*gocb.MutationResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Upsert indicates an expected call of Upsert.
func (mr *MockcollectionProviderMockRecorder) Upsert(key, value, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upsert", reflect.TypeOf((*MockcollectionProvider)(nil).Upsert), key, value, opts)
}
// MockgetResultProvider is a mock of getResultProvider interface.
type MockgetResultProvider struct {
ctrl *gomock.Controller
recorder *MockgetResultProviderMockRecorder
isgomock struct{}
}
// MockgetResultProviderMockRecorder is the mock recorder for MockgetResultProvider.
type MockgetResultProviderMockRecorder struct {
mock *MockgetResultProvider
}
// NewMockgetResultProvider creates a new mock instance.
func NewMockgetResultProvider(ctrl *gomock.Controller) *MockgetResultProvider {
mock := &MockgetResultProvider{ctrl: ctrl}
mock.recorder = &MockgetResultProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockgetResultProvider) EXPECT() *MockgetResultProviderMockRecorder {
return m.recorder
}
// Content mocks base method.
func (m *MockgetResultProvider) Content(value any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Content", value)
ret0, _ := ret[0].(error)
return ret0
}
// Content indicates an expected call of Content.
func (mr *MockgetResultProviderMockRecorder) Content(value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Content", reflect.TypeOf((*MockgetResultProvider)(nil).Content), value)
}
// MockscopeProvider is a mock of scopeProvider interface.
type MockscopeProvider struct {
ctrl *gomock.Controller
recorder *MockscopeProviderMockRecorder
isgomock struct{}
}
// MockscopeProviderMockRecorder is the mock recorder for MockscopeProvider.
type MockscopeProviderMockRecorder struct {
mock *MockscopeProvider
}
// NewMockscopeProvider creates a new mock instance.
func NewMockscopeProvider(ctrl *gomock.Controller) *MockscopeProvider {
mock := &MockscopeProvider{ctrl: ctrl}
mock.recorder = &MockscopeProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockscopeProvider) EXPECT() *MockscopeProviderMockRecorder {
return m.recorder
}
// Collection mocks base method.
func (m *MockscopeProvider) Collection(name string) collectionProvider {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Collection", name)
ret0, _ := ret[0].(collectionProvider)
return ret0
}
// Collection indicates an expected call of Collection.
func (mr *MockscopeProviderMockRecorder) Collection(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Collection", reflect.TypeOf((*MockscopeProvider)(nil).Collection), name)
}
// MocktransactionsProvider is a mock of transactionsProvider interface.
type MocktransactionsProvider struct {
ctrl *gomock.Controller
recorder *MocktransactionsProviderMockRecorder
isgomock struct{}
}
// MocktransactionsProviderMockRecorder is the mock recorder for MocktransactionsProvider.
type MocktransactionsProviderMockRecorder struct {
mock *MocktransactionsProvider
}
// NewMocktransactionsProvider creates a new mock instance.
func NewMocktransactionsProvider(ctrl *gomock.Controller) *MocktransactionsProvider {
mock := &MocktransactionsProvider{ctrl: ctrl}
mock.recorder = &MocktransactionsProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MocktransactionsProvider) EXPECT() *MocktransactionsProviderMockRecorder {
return m.recorder
}
// Run mocks base method.
func (m *MocktransactionsProvider) Run(logic func(*gocb.TransactionAttemptContext) error, opts *gocb.TransactionOptions) (*gocb.TransactionResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Run", logic, opts)
ret0, _ := ret[0].(*gocb.TransactionResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Run indicates an expected call of Run.
func (mr *MocktransactionsProviderMockRecorder) Run(logic, opts any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MocktransactionsProvider)(nil).Run), logic, opts)
}
================================================
FILE: pkg/gofr/datasource/couchbase/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=couchbase
//
// Package couchbase is a generated GoMock package.
package couchbase
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/couchbase/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/gofr/datasource/couchbase/metrics.go
//
// Generated by this command:
//
// mockgen -source=pkg/gofr/datasource/couchbase/metrics.go -destination=pkg/gofr/datasource/couchbase/mock_metrics.go -package=couchbase
//
// Package couchbase is a generated GoMock package.
package couchbase
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/couchbase/wrappers.go
================================================
package couchbase
import (
"time"
"github.com/couchbase/gocb/v2"
)
// analyticsResultWrapper is a wrapper around gocb.AnalyticsResult to implement the analyticsResultProvider interface.
type analyticsResultWrapper struct {
*gocb.AnalyticsResult
}
// clusterWrapper is a wrapper around gocb.Cluster to implement the clusterProvider interface.
type clusterWrapper struct {
*gocb.Cluster
}
// collectionWrapper is a wrapper around gocb.Collection to implement the collectionProvider interface.
type collectionWrapper struct {
*gocb.Collection
}
// bucketWrapper is a wrapper around gocb.Bucket to implement the bucketProvider interface.
type bucketWrapper struct {
*gocb.Bucket
}
// queryResultWrapper is a wrapper around gocb.QueryResult to implement the queryResultProvider interface.
type queryResultWrapper struct {
*gocb.QueryResult
}
// scopeWrapper is a wrapper around gocb.Scope to implement the scopeProvider interface.
type scopeWrapper struct {
*gocb.Scope
}
// transactionsWrapper is a wrapper around gocb.Transactions to implement the transactionsProvider interface.
type transactionsWrapper struct {
*gocb.Transactions
}
type getResultWrapper struct {
*gocb.GetResult
}
// Bucket returns a bucketProvider for the specified bucket name.
func (cw *clusterWrapper) Bucket(bucketName string) bucketProvider {
return &bucketWrapper{cw.Cluster.Bucket(bucketName)}
}
// Query executes a N1QL query against the Couchbase cluster.
func (cw *clusterWrapper) Query(statement string, opts *gocb.QueryOptions) (resultProvider, error) {
res, err := cw.Cluster.Query(statement, opts)
if err != nil {
return nil, err
}
return &queryResultWrapper{res}, nil
}
// AnalyticsQuery executes an Analytics query against the Couchbase Analytics service.
func (cw *clusterWrapper) AnalyticsQuery(statement string, opts *gocb.AnalyticsOptions) (resultProvider, error) {
res, err := cw.Cluster.AnalyticsQuery(statement, opts)
if err != nil {
return nil, err
}
return &analyticsResultWrapper{res}, nil
}
func (cw *clusterWrapper) Close(opts *gocb.ClusterCloseOptions) error {
return cw.Cluster.Close(opts)
}
func (cw *clusterWrapper) Ping(opts *gocb.PingOptions) (*gocb.PingResult, error) {
return cw.Cluster.Ping(opts)
}
// WaitUntilReady waits until the cluster is ready.
func (cw *clusterWrapper) WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error {
return cw.Cluster.WaitUntilReady(timeout, opts)
}
func (cw *clusterWrapper) Transactions() transactionsProvider {
return &transactionsWrapper{cw.Cluster.Transactions()}
}
// Collection returns a collectionProvider for the specified collection name.
func (bw *bucketWrapper) Collection(name string) collectionProvider {
return &collectionWrapper{bw.Bucket.Collection(name)}
}
// DefaultCollection returns the default collectionProvider for the bucket.
func (bw *bucketWrapper) DefaultCollection() collectionProvider {
return &collectionWrapper{bw.Bucket.DefaultCollection()}
}
func (bw *bucketWrapper) Scope(name string) scopeProvider {
return &scopeWrapper{bw.Bucket.Scope(name)}
}
func (bw *bucketWrapper) WaitUntilReady(timeout time.Duration, opts *gocb.WaitUntilReadyOptions) error {
return bw.Bucket.WaitUntilReady(timeout, opts)
}
// Next returns true if there are more rows to be retrieved.
func (qrw *queryResultWrapper) Next() bool {
return qrw.QueryResult.Next()
}
// Row unmarshals the current row into the value pointed to by the result parameter.
func (qrw *queryResultWrapper) Row(value any) error {
return qrw.QueryResult.Row(value)
}
// Err returns the error, if any, that occurred during the rows iteration.
func (qrw *queryResultWrapper) Err() error {
return qrw.QueryResult.Err()
}
// Close closes the query result.
func (qrw *queryResultWrapper) Close() error {
return qrw.QueryResult.Close()
}
// Next returns true if there are more rows to be retrieved.
func (arw *analyticsResultWrapper) Next() bool {
return arw.AnalyticsResult.Next()
}
// Row unmarshals the current row into the value pointed to by the result parameter.
func (arw *analyticsResultWrapper) Row(value any) error {
return arw.AnalyticsResult.Row(value)
}
// Err returns the error, if any, that occurred during the rows iteration.
func (arw *analyticsResultWrapper) Err() error {
return arw.AnalyticsResult.Err()
}
// Close closes the analytics result.
func (arw *analyticsResultWrapper) Close() error {
return arw.AnalyticsResult.Close()
}
func (sw *scopeWrapper) Collection(name string) collectionProvider {
return &collectionWrapper{sw.Scope.Collection(name)}
}
func (tw *transactionsWrapper) Run(
logic func(*gocb.TransactionAttemptContext) error, opts *gocb.TransactionOptions,
) (*gocb.TransactionResult, error) {
return tw.Transactions.Run(logic, opts)
}
func (grw *getResultWrapper) Content(value any) error {
return grw.GetResult.Content(value)
}
// Upsert performs an upsert operation on the collection.
func (cw *collectionWrapper) Upsert(key string, value any, opts *gocb.UpsertOptions) (*gocb.MutationResult, error) {
return cw.Collection.Upsert(key, value, opts)
}
// Get performs a get operation on the collection.
func (cw *collectionWrapper) Get(key string, opts *gocb.GetOptions) (getResultProvider, error) {
res, err := cw.Collection.Get(key, opts)
if err != nil {
return nil, err
}
return &getResultWrapper{res}, nil
}
// Remove performs a remove operation on the collection.
func (cw *collectionWrapper) Remove(key string, opts *gocb.RemoveOptions) (*gocb.MutationResult, error) {
return cw.Collection.Remove(key, opts)
}
func (cw *collectionWrapper) Insert(key string, value any, opts *gocb.InsertOptions) (*gocb.MutationResult, error) {
return cw.Collection.Insert(key, value, opts)
}
================================================
FILE: pkg/gofr/datasource/datasource.go
================================================
/*
Package datasource contains all the supported data sources in GoFr.
A datasource refers to any component that provides access to data — such as databases or message queues.
GoFr comes with built-in support for SQL and Redis data sources out of the box.
*/
package datasource
import "gofr.dev/pkg/gofr/config"
type Datasource interface {
Register(config config.Config)
}
// Question is: is container aware exactly "Redis" is there or some opaque datasource. in the later case, how do we
// retrieve from context?
================================================
FILE: pkg/gofr/datasource/dbresolver/circuit_breaker.go
================================================
package dbresolver
import (
"sync/atomic"
"time"
)
type circuitBreakerState int32
const (
circuitStateClosed circuitBreakerState = 0
circuitStateOpen circuitBreakerState = 1
circuitStateHalfOpen circuitBreakerState = 2
)
// Circuit breaker for replica health with atomic operations.
type circuitBreaker struct {
failures atomic.Int32
lastFailure atomic.Pointer[time.Time]
state atomic.Pointer[circuitBreakerState]
maxFailures int32
timeout time.Duration
}
func newCircuitBreaker(maxFailures int32, timeout time.Duration) *circuitBreaker {
cb := &circuitBreaker{
maxFailures: maxFailures,
timeout: timeout,
}
// Initialize state to closed
initialState := circuitStateClosed
cb.state.Store(&initialState)
return cb
}
func (cb *circuitBreaker) allowRequest() bool {
state := cb.state.Load()
if *state != circuitStateOpen {
return true
}
lastFailurePtr := cb.lastFailure.Load()
if lastFailurePtr == nil {
return true
}
if time.Since(*lastFailurePtr) <= cb.timeout {
return false
}
// Try to transition from open to half-open
openState := circuitStateOpen
halfOpenState := circuitStateHalfOpen
return cb.state.CompareAndSwap(&openState, &halfOpenState)
}
func (cb *circuitBreaker) recordSuccess() {
cb.failures.Store(0)
// Reset lastFailure to nil to indicate no recent failures
cb.lastFailure.Store(nil)
closedState := circuitStateClosed
cb.state.Store(&closedState)
}
func (cb *circuitBreaker) recordFailure() {
failures := cb.failures.Add(1)
now := time.Now()
cb.lastFailure.Store(&now)
if failures >= cb.maxFailures {
openState := circuitStateOpen
cb.state.Store(&openState)
}
}
================================================
FILE: pkg/gofr/datasource/dbresolver/factory.go
================================================
package dbresolver
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofrSQL "gofr.dev/pkg/gofr/datasource/sql"
gofrHTTP "gofr.dev/pkg/gofr/http"
)
const (
minIdleReplicaDefault = 2
maxIdleReplicaCapDefault = 10
minOpenReplicaDefault = 5
maxOpenReplicaCapDefault = 20
expectedHostPortParts = 2
)
var (
errPrimaryNil = errors.New("primary SQL connection is nil")
errInvalidReplicaHostFormat = errors.New("invalid replica host format (expected host:port)")
errDBNameRequired = errors.New("DB_NAME is required")
errEmptyCredentials = errors.New("replica has empty credentials")
errInvalidPort = errors.New("invalid port for replica")
errAllReplicasFailed = errors.New("all replicas failed to connect")
)
// Config holds resolver configuration.
type Config struct {
Strategy StrategyType
ReadFallback bool
MaxFailures uint32
TimeoutSec uint32
PrimaryRoutes []string
Replicas []ReplicaCredential
}
// ReplicaCredential stores credentials for a single replica.
type ReplicaCredential struct {
Host string `json:"host"` // Format: "hostname:port" (e.g., "localhost:3307").
User string `json:"user"` // Database username.
Password string `json:"password"` // Database password (supports special characters).
}
// ResolverProvider implements container.DBResolverProvider interface.
type ResolverProvider struct {
resolver container.DB
logger any
metrics any
tracer trace.Tracer
cfg Config
app *gofr.App
}
// NewDBResolverProvider creates a new ResolverProvider.
func NewDBResolverProvider(app *gofr.App, cfg *Config) *ResolverProvider {
return &ResolverProvider{
app: app,
cfg: *cfg,
}
}
// UseLogger sets the logger.
func (p *ResolverProvider) UseLogger(logger any) {
p.logger = logger
}
// UseMetrics sets the metrics.
func (p *ResolverProvider) UseMetrics(metrics any) {
p.metrics = metrics
}
// UseTracer sets the tracer.
func (p *ResolverProvider) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
p.tracer = t
}
}
// Connect initializes the resolver.
func (p *ResolverProvider) Connect() {
// Get primary from app.
primary := p.app.GetSQL()
if primary == nil {
if logger, ok := p.logger.(Logger); ok {
logger.Error(errPrimaryNil)
}
return
}
// Convert logger and metrics to proper types.
logger, loggerOk := p.logger.(Logger)
metrics, metricsOk := p.metrics.(Metrics)
if !loggerOk || !metricsOk {
if loggerOk {
logger.Errorf("Invalid logger or metrics type")
}
return
}
replicas := p.createAndValidateReplicas(logger, metrics)
if replicas == nil {
return
}
// Build primary routes map.
primaryRoutesMap := make(map[string]bool)
for _, route := range p.cfg.PrimaryRoutes {
primaryRoutesMap[route] = true
}
strategy := getStrategy(p.cfg.Strategy)
// Create resolver - single place!
p.resolver = NewResolver(
primary,
replicas,
logger,
metrics,
WithStrategy(strategy),
WithFallback(p.cfg.ReadFallback),
WithPrimaryRoutes(primaryRoutesMap),
)
logger.Logf("DB Resolver initialized with %d replicas", len(replicas))
}
func (p *ResolverProvider) createAndValidateReplicas(logger Logger, metrics Metrics) []container.DB {
// Pass Config to connectReplicas
replicas, err := connectReplicas(&p.cfg, p.app.Config, logger, metrics)
if err != nil {
logger.Errorf("Failed to create replicas: %v", err)
return nil
}
if len(replicas) == 0 {
logger.Warn("No replicas configured - all operations will use primary")
p.resolver = p.app.GetSQL()
return nil
}
return replicas
}
// getStrategy returns the configured strategy.
func getStrategy(strategyType StrategyType) Strategy {
switch strategyType {
case StrategyRandom:
return NewRandomStrategy()
case StrategyRoundRobin:
return NewRoundRobinStrategy()
default:
return NewRoundRobinStrategy()
}
}
// GetResolver returns the underlying resolver.
func (p *ResolverProvider) GetResolver() container.DB {
return p.resolver
}
// InitDBResolver - Complete initialization with middleware.
func InitDBResolver(app *gofr.App, cfg *Config) error {
provider := NewDBResolverProvider(app, cfg)
// Add middleware to inject HTTP context.
app.UseMiddleware(createHTTPMiddleware())
// Add resolver (calls Connect() internally).
app.AddDBResolver(provider)
return nil
}
// createHTTPMiddleware injects HTTP method/path into context.
func createHTTPMiddleware() gofrHTTP.Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = WithHTTPMethod(ctx, r.Method)
ctx = WithRequestPath(ctx, r.URL.Path)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
// connectReplicas creates replica connections from Config.Replicas.
func connectReplicas(cfg *Config, appCfg config.Config, logger Logger, metrics Metrics) ([]container.DB, error) {
if len(cfg.Replicas) == 0 {
return nil, nil
}
replicas := make([]container.DB, 0, len(cfg.Replicas))
var failedReplicas []string
for i, cred := range cfg.Replicas {
if err := validateReplicaCredentials(cred, i); err != nil {
return nil, err
}
host, port, err := parseReplicaHost(cred.Host, i)
if err != nil {
return nil, err
}
replica, err := createReplicaConnection(appCfg, host, port, cred.User, cred.Password, logger, metrics)
if err != nil {
logger.Warnf("Failed to connect to replica #%d (%s): %v", i+1, cred.Host, err)
failedReplicas = append(failedReplicas, cred.Host)
continue // Skip failed replica instead of failing completely
}
replicas = append(replicas, replica)
logger.Logf("Created DB replica connection to %s", cred.Host)
}
if len(replicas) == 0 {
return nil, fmt.Errorf("%w (%d total)", errAllReplicasFailed, len(cfg.Replicas))
}
if len(failedReplicas) > 0 {
logger.Warnf("%d/%d replicas failed: %v", len(failedReplicas), len(cfg.Replicas), failedReplicas)
}
return replicas, nil
}
// validateReplicaCredentials checks if all required fields are present.
func validateReplicaCredentials(cred ReplicaCredential, index int) error {
if cred.Host == "" || cred.User == "" || cred.Password == "" {
return fmt.Errorf("%w at index %d", errEmptyCredentials, index)
}
return nil
}
// parseReplicaHost splits host:port and validates the format.
func parseReplicaHost(host string, index int) (validatedHost, validatedPort string, err error) {
parts := strings.Split(host, ":")
if len(parts) != expectedHostPortParts {
return "", "", fmt.Errorf("%w at index %d: %s", errInvalidReplicaHostFormat, index, host)
}
hostname := strings.TrimSpace(parts[0])
port := strings.TrimSpace(parts[1])
// Validate port is numeric
if _, err := strconv.Atoi(port); err != nil {
return "", "", fmt.Errorf("%w at index %d: %s", errInvalidPort, index, port)
}
return hostname, port, nil
}
// createReplicaConnection creates a single replica database connection.
func createReplicaConnection(cfg config.Config, host, port, user, password string, logger Logger, metrics Metrics) (container.DB, error) {
dbName := cfg.Get("DB_NAME")
if dbName == "" {
return nil, errDBNameRequired
}
replicaCfg := &replicaConfig{
base: cfg,
host: host,
port: port,
user: user,
password: password,
}
db := gofrSQL.NewSQL(replicaCfg, logger, metrics)
return db, nil
}
// replicaConfig wraps the main config and overrides specific values.
type replicaConfig struct {
base config.Config
host string
port string
user string
password string
}
func (r *replicaConfig) Get(key string) string {
switch key {
case "DB_HOST":
return r.host
case "DB_PORT":
return r.port
case "DB_USER":
return r.user
case "DB_PASSWORD":
return r.password
case "DB_MAX_IDLE_CONNECTIONS":
return optimizedIdleConnections(r.base)
case "DB_MAX_OPEN_CONNECTIONS":
return optimizedOpenConnections(r.base)
default:
return r.base.Get(key)
}
}
func (r *replicaConfig) GetOrDefault(key, defaultValue string) string {
val := r.Get(key)
if val == "" {
return defaultValue
}
return val
}
func getReplicaConfigInt(cfg config.Config, key string, fallback int) int {
valStr := cfg.Get(key)
if valStr == "" {
return fallback
}
val, err := strconv.Atoi(valStr)
if err != nil {
return fallback
}
return val
}
func optimizedConnections(cfg config.Config, key string, minDefault, maxDefault, defaultVal, multiplier int) string {
val := getReplicaConfigInt(cfg, key, defaultVal)
if val <= 0 {
return strconv.Itoa(defaultVal)
}
optimized := val * multiplier
if optimized < minDefault {
optimized = minDefault
}
if optimized > maxDefault {
optimized = maxDefault
}
return strconv.Itoa(optimized)
}
func optimizedIdleConnections(cfg config.Config) string {
// preserves previous behavior: read replica idle config, clamp to min/max
return optimizedConnections(
cfg,
"DB_REPLICA_MAX_IDLE_CONNECTIONS",
minIdleReplicaDefault,
maxIdleReplicaCapDefault,
minIdleReplicaDefault,
1,
)
}
func optimizedOpenConnections(cfg config.Config) string {
// preserves previous behavior: read replica open config, clamp to min/max
return optimizedConnections(
cfg,
"DB_REPLICA_MAX_OPEN_CONNECTIONS",
minOpenReplicaDefault,
maxOpenReplicaCapDefault,
minOpenReplicaDefault,
1,
)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/factory_test.go
================================================
package dbresolver
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr"
"gofr.dev/pkg/gofr/config"
)
func TestNewDBResolverProvider(t *testing.T) {
app := &gofr.App{}
cfg := Config{
Strategy: StrategyRoundRobin,
ReadFallback: true,
}
provider := NewDBResolverProvider(app, &cfg)
require.NotNil(t, provider)
assert.Equal(t, app, provider.app)
assert.Equal(t, cfg, provider.cfg)
}
func TestResolverProvider_GetResolver(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockDB := NewMockDB(ctrl)
provider := &ResolverProvider{
resolver: mockDB,
}
resolver := provider.GetResolver()
assert.Equal(t, mockDB, resolver)
}
func TestCreateStrategy_RoundRobin(t *testing.T) {
strategy := getStrategy(StrategyRoundRobin)
assert.NotNil(t, strategy)
assert.IsType(t, &RoundRobinStrategy{}, strategy)
}
func TestCreateStrategy_Random(t *testing.T) {
strategy := getStrategy(StrategyRandom)
assert.NotNil(t, strategy)
assert.IsType(t, &RandomStrategy{}, strategy)
}
func TestCreateStrategy_Default(t *testing.T) {
strategy := getStrategy(StrategyType("unknown"))
assert.NotNil(t, strategy)
assert.IsType(t, &RoundRobinStrategy{}, strategy)
}
func TestReplicaConfig_Get(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
replicaCfg := &replicaConfig{
base: mockConfig,
host: "localhost",
port: "3307",
user: "replica_user",
password: "replica_pass",
}
assert.Equal(t, "localhost", replicaCfg.Get("DB_HOST"))
assert.Equal(t, "3307", replicaCfg.Get("DB_PORT"))
assert.Equal(t, "replica_user", replicaCfg.Get("DB_USER"))
assert.Equal(t, "replica_pass", replicaCfg.Get("DB_PASSWORD"))
assert.Equal(t, "testdb", replicaCfg.Get("DB_NAME"))
}
func TestReplicaConfig_GetOrDefault(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{})
replicaCfg := &replicaConfig{
base: mockConfig,
}
result := replicaCfg.GetOrDefault("EMPTY_KEY", "default_value")
assert.Equal(t, "default_value", result)
}
func TestReplicaConfig_GetMaxIdleConnections(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_IDLE_CONNECTIONS": "5",
})
replicaCfg := &replicaConfig{
base: mockConfig,
}
result := replicaCfg.Get("DB_MAX_IDLE_CONNECTIONS")
assert.Equal(t, "5", result)
}
func TestReplicaConfig_GetMaxOpenConnections(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_OPEN_CONNECTIONS": "10",
})
replicaCfg := &replicaConfig{
base: mockConfig,
}
result := replicaCfg.Get("DB_MAX_OPEN_CONNECTIONS")
assert.Equal(t, "10", result)
}
func TestGetReplicaConfigInt_ValidValue(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"TEST_KEY": "42",
})
result := getReplicaConfigInt(mockConfig, "TEST_KEY", 10)
assert.Equal(t, 42, result)
}
func TestGetReplicaConfigInt_EmptyValue(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{})
result := getReplicaConfigInt(mockConfig, "MISSING_KEY", 10)
assert.Equal(t, 10, result)
}
func TestGetReplicaConfigInt_InvalidValue(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"TEST_KEY": "invalid",
})
result := getReplicaConfigInt(mockConfig, "TEST_KEY", 10)
assert.Equal(t, 10, result)
}
func TestOptimizedIdleConnections(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_IDLE_CONNECTIONS": "5",
})
result := optimizedIdleConnections(mockConfig)
assert.Equal(t, "5", result)
}
func TestOptimizedIdleConnections_BelowMin(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_IDLE_CONNECTIONS": "1",
})
result := optimizedIdleConnections(mockConfig)
assert.Equal(t, "2", result)
}
func TestOptimizedIdleConnections_AboveMax(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_IDLE_CONNECTIONS": "15",
})
result := optimizedIdleConnections(mockConfig)
assert.Equal(t, "10", result)
}
func TestOptimizedOpenConnections(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_OPEN_CONNECTIONS": "8",
})
result := optimizedOpenConnections(mockConfig)
assert.Equal(t, "8", result)
}
func TestOptimizedOpenConnections_BelowMin(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_OPEN_CONNECTIONS": "2",
})
result := optimizedOpenConnections(mockConfig)
assert.Equal(t, "5", result)
}
func TestOptimizedOpenConnections_AboveMax(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_REPLICA_MAX_OPEN_CONNECTIONS": "25",
})
result := optimizedOpenConnections(mockConfig)
assert.Equal(t, "20", result)
}
func TestCreateReplicaConnection_NoDBName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
replica, err := createReplicaConnection(mockConfig, "host", "3306", "user", "pass", mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, replica)
assert.ErrorIs(t, err, errDBNameRequired)
}
func TestResolverProvider_UseTracer(t *testing.T) {
provider := &ResolverProvider{}
tracer := noop.NewTracerProvider().Tracer("test")
provider.UseTracer(tracer)
assert.Equal(t, tracer, provider.tracer)
}
func TestResolverProvider_UseTracer_InvalidType(t *testing.T) {
provider := &ResolverProvider{}
provider.UseTracer("invalid")
assert.Nil(t, provider.tracer)
}
func TestResolverProvider_Connect_InvalidLogger(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{})
app := gofr.New()
app.Config = mockConfig
provider := &ResolverProvider{
app: app,
logger: "invalid-logger", // Wrong type
}
provider.Connect()
assert.Nil(t, provider.resolver)
}
func TestResolverProvider_Connect_InvalidMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockConfig := config.NewMockConfig(map[string]string{})
app := gofr.New()
app.Config = mockConfig
mockLogger.EXPECT().Errorf("Invalid logger or metrics type").Times(1)
provider := &ResolverProvider{
app: app,
logger: mockLogger,
metrics: "invalid-metrics", // Wrong type
}
provider.Connect()
assert.Nil(t, provider.resolver)
}
func TestResolverProvider_UseLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
provider := &ResolverProvider{}
provider.UseLogger(mockLogger)
assert.Equal(t, mockLogger, provider.logger)
}
func TestResolverProvider_UseMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
provider := &ResolverProvider{}
provider.UseMetrics(mockMetrics)
assert.Equal(t, mockMetrics, provider.metrics)
}
func TestResolverProvider_GetResolver_Nil(t *testing.T) {
provider := &ResolverProvider{}
resolver := provider.GetResolver()
assert.Nil(t, resolver)
}
func TestCreateHTTPMiddleware(t *testing.T) {
middleware := createHTTPMiddleware()
assert.NotNil(t, middleware)
handlerCalled := false
handler := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
handlerCalled = true
method := r.Context().Value(contextKeyHTTPMethod)
path := r.Context().Value(contextKeyRequestPath)
assert.Equal(t, "GET", method)
assert.Equal(t, "/test/path", path)
})
wrappedHandler := middleware(handler)
req := httptest.NewRequest(http.MethodGet, "/test/path", http.NoBody)
w := httptest.NewRecorder()
wrappedHandler.ServeHTTP(w, req)
assert.True(t, handlerCalled)
}
func TestCircuitBreaker_AllowRequest_StateOpen_WithinTimeout(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Set state to open
openState := circuitStateOpen
cb.state.Store(&openState)
// Record a failure to set lastFailure
now := time.Now()
cb.lastFailure.Store(&now)
// Should not allow request (within timeout)
result := cb.allowRequest()
assert.False(t, result)
assert.Equal(t, circuitStateOpen, *cb.state.Load())
}
func TestCircuitBreaker_AllowRequest_StateOpen_NilLastFailure(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Set state to open but no last failure
openState := circuitStateOpen
cb.state.Store(&openState)
cb.lastFailure.Store(nil)
// Should allow request when lastFailure is nil
result := cb.allowRequest()
assert.True(t, result)
}
func TestCircuitBreaker_RecordFailure_OpensCircuit(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Record failures up to threshold
cb.recordFailure() // 1
assert.Equal(t, circuitStateClosed, *cb.state.Load())
cb.recordFailure() // 2
assert.Equal(t, circuitStateClosed, *cb.state.Load())
cb.recordFailure() // 3 - should open circuit
assert.Equal(t, circuitStateOpen, *cb.state.Load())
assert.Equal(t, int32(3), cb.failures.Load())
}
func TestCircuitBreaker_RecordFailure_LastFailureUpdated(t *testing.T) {
cb := newCircuitBreaker(5, 50*time.Millisecond)
before := time.Now()
cb.recordFailure()
after := time.Now()
lastFailurePtr := cb.lastFailure.Load()
require.NotNil(t, lastFailurePtr)
assert.True(t, lastFailurePtr.After(before) || lastFailurePtr.Equal(before))
assert.True(t, lastFailurePtr.Before(after) || lastFailurePtr.Equal(after))
}
func TestCircuitBreaker_AllowRequest_HalfOpenState(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Set state to half-open
halfOpenState := circuitStateHalfOpen
cb.state.Store(&halfOpenState)
// Should allow request in half-open state
result := cb.allowRequest()
assert.True(t, result)
}
func TestCircuitBreaker_RecordSuccess_ResetsCircuit(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Record some failures
cb.recordFailure()
cb.recordFailure()
// Record success
cb.recordSuccess()
// Should reset everything
assert.Equal(t, int32(0), cb.failures.Load())
assert.Nil(t, cb.lastFailure.Load())
assert.Equal(t, circuitStateClosed, *cb.state.Load())
}
func TestCircuitBreaker_AllowRequest_StateOpen_AfterTimeout(t *testing.T) {
cb := newCircuitBreaker(3, 30*time.Millisecond)
// Trigger circuit to open by recording failures
cb.recordFailure()
cb.recordFailure()
cb.recordFailure()
// Verify it's open
assert.Equal(t, circuitStateOpen, *cb.state.Load())
// Capture the CURRENT state pointer (the one set by recordFailure)
currentStatePtr := cb.state.Load()
// Simulate timeout by setting lastFailure in the past
pastTime := time.Now().Add(-50 * time.Millisecond)
cb.lastFailure.Store(&pastTime)
// Timeout has passed - CompareAndSwap will work because we're using the same pointer
halfOpenState := circuitStateHalfOpen
swapped := cb.state.CompareAndSwap(currentStatePtr, &halfOpenState)
assert.True(t, swapped)
// Now verify allowRequest works correctly
result := cb.allowRequest()
assert.True(t, result)
assert.Equal(t, circuitStateHalfOpen, *cb.state.Load())
}
func TestCircuitBreaker_StateTransitions(t *testing.T) {
cb := newCircuitBreaker(2, 30*time.Millisecond)
// Start closed
assert.Equal(t, circuitStateClosed, *cb.state.Load())
// Record failures to open
cb.recordFailure()
cb.recordFailure()
assert.Equal(t, circuitStateOpen, *cb.state.Load())
// Capture current state pointer
currentStatePtr := cb.state.Load()
// Simulate timeout by setting lastFailure in the past
pastTime := time.Now().Add(-50 * time.Millisecond)
cb.lastFailure.Store(&pastTime)
// Manually transition to half-open using the captured pointer
halfOpenState := circuitStateHalfOpen
swapped := cb.state.CompareAndSwap(currentStatePtr, &halfOpenState)
assert.True(t, swapped)
// Verify state changed
assert.Equal(t, circuitStateHalfOpen, *cb.state.Load())
// Record success to close
cb.recordSuccess()
assert.Equal(t, circuitStateClosed, *cb.state.Load())
}
func TestCircuitBreaker_AllowRequest_ClosedState(t *testing.T) {
cb := newCircuitBreaker(3, 50*time.Millisecond)
// Should allow request in closed state
result := cb.allowRequest()
assert.True(t, result)
assert.Equal(t, circuitStateClosed, *cb.state.Load())
}
func TestCircuitBreaker_MultipleFailuresUnderThreshold(t *testing.T) {
cb := newCircuitBreaker(5, 50*time.Millisecond)
// Record failures under threshold
cb.recordFailure()
cb.recordFailure()
cb.recordFailure()
// Should still be closed
assert.Equal(t, circuitStateClosed, *cb.state.Load())
assert.Equal(t, int32(3), cb.failures.Load())
}
func TestNewCircuitBreaker(t *testing.T) {
maxFailures := int32(5)
timeout := 50 * time.Millisecond
cb := newCircuitBreaker(maxFailures, timeout)
require.NotNil(t, cb)
assert.Equal(t, maxFailures, cb.maxFailures)
assert.Equal(t, timeout, cb.timeout)
assert.Equal(t, circuitStateClosed, *cb.state.Load())
assert.Equal(t, int32(0), cb.failures.Load())
}
func TestCircuitBreaker_ConcurrentFailures(t *testing.T) {
cb := newCircuitBreaker(10, 50*time.Millisecond)
// Record multiple failures concurrently
done := make(chan bool, 5)
for i := 0; i < 5; i++ {
go func() {
cb.recordFailure()
done <- true
}()
}
for i := 0; i < 5; i++ {
<-done
}
// Verify failures were recorded
failures := cb.failures.Load()
assert.Equal(t, int32(5), failures)
assert.Equal(t, circuitStateClosed, *cb.state.Load())
}
func TestConnectReplicas_EmptyConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
cfg := Config{
Replicas: []ReplicaCredential{}, // Empty replicas
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
assert.Nil(t, replicas)
}
func TestConnectReplicas_ValidSingleReplica(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Logf("Created DB replica connection to %s", "localhost:3307").Times(1)
cfg := Config{
Replicas: []ReplicaCredential{
{
Host: "localhost:3307",
User: "replica_user",
Password: "replica_pass",
},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
require.NotNil(t, replicas)
assert.Len(t, replicas, 1)
}
func TestConnectReplicas_MultipleReplicas(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(3)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: "replica1:3307", User: "user1", Password: "pass1"},
{Host: "replica2:3308", User: "user2", Password: "pass2"},
{Host: "replica3:3309", User: "user3", Password: "pass3"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
require.NotNil(t, replicas)
assert.Len(t, replicas, 3)
}
func TestConnectReplicas_InvalidHostFormat(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: "invalid-host-without-port", User: "user", Password: "pass"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, replicas)
assert.ErrorIs(t, err, errInvalidReplicaHostFormat)
}
func TestConnectReplicas_EmptyCredentials(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
tests := []struct {
name string
replica ReplicaCredential
}{
{
name: "empty host",
replica: ReplicaCredential{Host: "", User: "user", Password: "pass"},
},
{
name: "empty user",
replica: ReplicaCredential{Host: "localhost:3307", User: "", Password: "pass"},
},
{
name: "empty password",
replica: ReplicaCredential{Host: "localhost:3307", User: "user", Password: ""},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := Config{
Replicas: []ReplicaCredential{tt.replica},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, replicas)
assert.Contains(t, err.Error(), "empty credentials")
})
}
}
func TestConnectReplicas_InvalidPort(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: "localhost:abc", User: "user", Password: "pass"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, replicas)
assert.Contains(t, err.Error(), "invalid port")
}
func TestConnectReplicas_PasswordWithCommas(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(1)
cfg := Config{
Replicas: []ReplicaCredential{
{
Host: "localhost:3307",
User: "user",
Password: "pass,with,commas,and,special!@#$%",
},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
require.NotNil(t, replicas)
assert.Len(t, replicas, 1)
}
func TestConnectReplicas_PartialFailures(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Both replicas will succeed in creation (gofrSQL.NewSQL doesn't validate connectivity)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(2)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: "localhost:9999", User: "user", Password: "pass"},
{Host: "localhost:3307", User: "valid_user", Password: "pass"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
require.NotNil(t, replicas)
assert.Len(t, replicas, 2) // Both replicas created successfully
}
func TestConnectReplicas_AllReplicasFail(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Both replicas will succeed in creation
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(2)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: "localhost:9999", User: "user", Password: "pass"},
{Host: "localhost:9998", User: "user", Password: "pass"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err) // No error during creation
require.NotNil(t, replicas)
assert.Len(t, replicas, 2) // Both replicas created
}
func TestConnectReplicas_HostWithWhitespace(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{
"DB_NAME": "testdb",
})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(1)
cfg := Config{
Replicas: []ReplicaCredential{
{Host: " localhost : 3307 ", User: "user", Password: "pass"},
},
}
replicas, err := connectReplicas(&cfg, mockConfig, mockLogger, mockMetrics)
require.NoError(t, err)
require.NotNil(t, replicas)
assert.Len(t, replicas, 1)
}
func TestCreateReplicaConnection_MissingDBName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConfig := config.NewMockConfig(map[string]string{})
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
replica, err := createReplicaConnection(mockConfig, "localhost", "3307", "user", "pass", mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, replica)
assert.ErrorIs(t, err, errDBNameRequired)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/dbresolver
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/pubsub v1.50.1 // indirect
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
github.com/XSAM/otelsql v0.41.0 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/graphql-go/graphql v0.8.1 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/kafka-go v0.4.50 // indirect
github.com/vektah/gqlparser/v2 v2.5.31 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.270.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/dbresolver/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8=
github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d h1:abDbP7XBVgwda+h0J5Qra5p2OQpidU2FdkXvzCKL+H8=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d/go.mod h1:wKFzULXAPj3U2BDAPWXhSbQQNC6FU1+1/5iika6IY7g=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo=
github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k=
github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 h1:U++6AfUpXXSILim4iH6Jb2oeK/mp7J4lNzzyO8Cx4Zw=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0/go.mod h1:HVNUDNMGMeykut/2GZ++AZjglCqew/+Hf4lxRVqFFxQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0 h1:Z7ARHF7193vyVltPYcmuhSKPLf8dP5rtJZLtTQnbMH4=
go.opentelemetry.io/otel/exporters/zipkin v1.42.0/go.mod h1:DW09+gaEg5kydlb9g8kp4Nos3yqo9YSA1uHXkeJihXc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
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=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
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/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20190911185100-cd5d95a43a6e/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: pkg/gofr/datasource/dbresolver/logger.go
================================================
package dbresolver
import (
"fmt"
"regexp"
"strings"
)
var whitespaceRegex = regexp.MustCompile(`\s+`)
// Logger defines the logging interface for dbresolver.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
Warnf(pattern string, args ...any)
Info(args ...any)
Infof(format string, args ...any)
Error(args ...any)
Warn(args ...any)
}
// QueryLog contains information about a SQL query.
type QueryLog struct {
Type string `json:"type"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Target string `json:"target"`
IsRead bool `json:"is_read"`
}
// PrettyPrint formats the QueryLog for output.
func (ql *QueryLog) PrettyPrint(logger Logger) {
formattedLog := fmt.Sprintf("\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s\n",
clean(ql.Type), "DBRESOLVER", ql.Duration,
clean(fmt.Sprintf("%s %s", ql.Target, ql.Query)), clean(ql.Query))
logger.Debug(formattedLog)
}
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = whitespaceRegex.ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
return strings.TrimSpace(query)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/metrics.go
================================================
package dbresolver
import (
"context"
)
// Metrics interface for metrics operations.
type Metrics interface {
NewHistogram(name, description string, buckets ...float64)
NewGauge(name, description string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/mock_db.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: gofr.dev/pkg/gofr/container (interfaces: DB)
//
// Generated by this command:
//
// mockgen -package=dbresolver -destination=mock_db.go gofr.dev/pkg/gofr/container DB
//
// Package dbresolver is a generated GoMock package.
package dbresolver
import (
context "context"
sql "database/sql"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
datasource "gofr.dev/pkg/gofr/datasource"
sql0 "gofr.dev/pkg/gofr/datasource/sql"
)
// MockDBResolverProvider is a mock of DBResolverProvider interface.
type MockDBResolverProvider struct {
ctrl *gomock.Controller
recorder *MockDBResolverProviderMockRecorder
isgomock struct{}
}
// MockDBResolverProviderMockRecorder is the mock recorder for MockDBResolverProvider.
type MockDBResolverProviderMockRecorder struct {
mock *MockDBResolverProvider
}
// NewMockDBResolverProvider creates a new mock instance.
func NewMockDBResolverProvider(ctrl *gomock.Controller) *MockDBResolverProvider {
mock := &MockDBResolverProvider{ctrl: ctrl}
mock.recorder = &MockDBResolverProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDBResolverProvider) EXPECT() *MockDBResolverProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockDBResolverProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockDBResolverProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockDBResolverProvider)(nil).Connect))
}
// GetResolver mocks base method.
func (m *MockDBResolverProvider) GetResolver() container.DB {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetResolver")
ret0, _ := ret[0].(container.DB)
return ret0
}
// GetResolver indicates an expected call of GetResolver.
func (mr *MockDBResolverProviderMockRecorder) GetResolver() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetResolver", reflect.TypeOf((*MockDBResolverProvider)(nil).GetResolver))
}
// UseLogger mocks base method.
func (m *MockDBResolverProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockDBResolverProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockDBResolverProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockDBResolverProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockDBResolverProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockDBResolverProvider)(nil).UseMetrics), metrics)
}
// UseTracer mocks base method.
func (m *MockDBResolverProvider) UseTracer(tracer any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseTracer", tracer)
}
// UseTracer indicates an expected call of UseTracer.
func (mr *MockDBResolverProviderMockRecorder) UseTracer(tracer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseTracer", reflect.TypeOf((*MockDBResolverProvider)(nil).UseTracer), tracer)
}
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
isgomock struct{}
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// Begin mocks base method.
func (m *MockDB) Begin() (*sql0.Tx, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Begin")
ret0, _ := ret[0].(*sql0.Tx)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Begin indicates an expected call of Begin.
func (mr *MockDBMockRecorder) Begin() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockDB)(nil).Begin))
}
// Close mocks base method.
func (m *MockDB) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockDBMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockDB)(nil).Close))
}
// Dialect mocks base method.
func (m *MockDB) Dialect() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dialect")
ret0, _ := ret[0].(string)
return ret0
}
// Dialect indicates an expected call of Dialect.
func (mr *MockDBMockRecorder) Dialect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dialect", reflect.TypeOf((*MockDB)(nil).Dialect))
}
// Exec mocks base method.
func (m *MockDB) Exec(query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec.
func (mr *MockDBMockRecorder) Exec(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockDB)(nil).Exec), varargs...)
}
// ExecContext mocks base method.
func (m *MockDB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecContext", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecContext indicates an expected call of ExecContext.
func (mr *MockDBMockRecorder) ExecContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockDB)(nil).ExecContext), varargs...)
}
// HealthCheck mocks base method.
func (m *MockDB) HealthCheck() *datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck")
ret0, _ := ret[0].(*datasource.Health)
return ret0
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockDBMockRecorder) HealthCheck() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockDB)(nil).HealthCheck))
}
// Prepare mocks base method.
func (m *MockDB) Prepare(query string) (*sql.Stmt, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Prepare", query)
ret0, _ := ret[0].(*sql.Stmt)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Prepare indicates an expected call of Prepare.
func (mr *MockDBMockRecorder) Prepare(query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Prepare", reflect.TypeOf((*MockDB)(nil).Prepare), query)
}
// Query mocks base method.
func (m *MockDB) Query(query string, args ...any) (*sql.Rows, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(*sql.Rows)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockDBMockRecorder) Query(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDB)(nil).Query), varargs...)
}
// QueryContext mocks base method.
func (m *MockDB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryContext", varargs...)
ret0, _ := ret[0].(*sql.Rows)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryContext indicates an expected call of QueryContext.
func (mr *MockDBMockRecorder) QueryContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryContext", reflect.TypeOf((*MockDB)(nil).QueryContext), varargs...)
}
// QueryRow mocks base method.
func (m *MockDB) QueryRow(query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRow", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRow indicates an expected call of QueryRow.
func (mr *MockDBMockRecorder) QueryRow(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockDB)(nil).QueryRow), varargs...)
}
// QueryRowContext mocks base method.
func (m *MockDB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRowContext", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRowContext indicates an expected call of QueryRowContext.
func (mr *MockDBMockRecorder) QueryRowContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRowContext", reflect.TypeOf((*MockDB)(nil).QueryRowContext), varargs...)
}
// Select mocks base method.
func (m *MockDB) Select(ctx context.Context, data any, query string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{ctx, data, query}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Select", varargs...)
}
// Select indicates an expected call of Select.
func (mr *MockDBMockRecorder) Select(ctx, data, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, data, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockDB)(nil).Select), varargs...)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=dbresolver
//
// Package dbresolver is a generated GoMock package.
package dbresolver
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
func (m *MockLogger) Infof(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
func (mr *MockLoggerMockRecorder) Warn(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), args...)
}
func (m *MockLogger) Warn(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warn", varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// Warnf mocks base method.
func (m *MockLogger) Warnf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warnf", varargs...)
}
// Warnf indicates an expected call of Warnf.
func (mr *MockLoggerMockRecorder) Warnf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=dbresolver
//
// Package dbresolver is a generated GoMock package.
package dbresolver
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, description string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, description)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, description any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, description)
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, description string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, description}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, description any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, description}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/mock_strategy.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: strategy.go
//
// Generated by this command:
//
// mockgen -source=strategy.go -destination=mock_strategy.go -package=dbresolver
//
// Package dbresolver is a generated GoMock package.
package dbresolver
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockStrategy is a mock of Strategy interface.
type MockStrategy struct {
ctrl *gomock.Controller
recorder *MockStrategyMockRecorder
isgomock struct{}
}
// MockStrategyMockRecorder is the mock recorder for MockStrategy.
type MockStrategyMockRecorder struct {
mock *MockStrategy
}
// NewMockStrategy creates a new mock instance.
func NewMockStrategy(ctrl *gomock.Controller) *MockStrategy {
mock := &MockStrategy{ctrl: ctrl}
mock.recorder = &MockStrategyMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStrategy) EXPECT() *MockStrategyMockRecorder {
return m.recorder
}
// Next mocks base method.
func (m *MockStrategy) Next(count int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Next", count)
ret0, _ := ret[0].(int)
return ret0
}
// Next indicates an expected call of Next.
func (mr *MockStrategyMockRecorder) Next(count any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockStrategy)(nil).Next), count)
}
// Name mocks base method.
func (m *MockStrategy) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockStrategyMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockStrategy)(nil).Name))
}
================================================
FILE: pkg/gofr/datasource/dbresolver/options.go
================================================
package dbresolver
// Option is a function type for configuring the resolver.
type Option func(*Resolver)
// WithStrategy sets the strategy for the resolver.
func WithStrategy(strategy Strategy) Option {
return func(r *Resolver) {
r.strategy = strategy
}
}
// WithFallback sets whether to fallback to primary on replica failure.
func WithFallback(fallback bool) Option {
return func(r *Resolver) {
r.readFallback = fallback
}
}
func WithPrimaryRoutes(routes map[string]bool) Option {
return func(r *Resolver) {
r.primaryRoutes = routes
}
}
================================================
FILE: pkg/gofr/datasource/dbresolver/resolver.go
================================================
package dbresolver
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"sync"
"sync/atomic"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource"
gofrSQL "gofr.dev/pkg/gofr/datasource/sql"
)
// Constants for strategies and intervals.
const (
contextKeyHTTPMethod contextKey = "dbresolver.http_method"
// contextKeyRequestPath stores the HTTP request path in context.
// Used to determine if the route matches PrimaryRoutes configuration,
// forcing queries to use the primary database instead of replicas.
contextKeyRequestPath contextKey = "dbresolver.request_path"
defaultMaxFailures = 5
defaultTimeoutSec = 30
)
var errReplicaFailedNoFallback = errors.New("replica query failed and fallback disabled")
type contextKey string
// statistics holds atomic counters for various operations.
type statistics struct {
primaryReads atomic.Uint64
primaryWrites atomic.Uint64
replicaReads atomic.Uint64
primaryFallbacks atomic.Uint64
replicaFailures atomic.Uint64
totalQueries atomic.Uint64
}
// Replica wrapper with circuit breaker.
type replicaWrapper struct {
db container.DB
breaker *circuitBreaker
index int
}
// Resolver is the main struct that implements the container.DB interface.
type Resolver struct {
primary container.DB
replicas []*replicaWrapper
strategy Strategy
readFallback bool
logger Logger
metrics Metrics
tracer trace.Tracer
primaryRoutes map[string]bool
primaryPrefixes []string
stats *statistics
// Background task management.
stopChan chan struct{}
wg sync.WaitGroup
once sync.Once
}
// NewResolver creates a new Resolver instance with the provided primary and replicas.
func NewResolver(primary container.DB, replicas []container.DB, logger Logger, metrics Metrics, opts ...Option) container.DB {
// Wrap replicas with circuit breakers
replicaWrappers := make([]*replicaWrapper, len(replicas))
for i, replica := range replicas {
replicaWrappers[i] = &replicaWrapper{
db: replica,
breaker: newCircuitBreaker(defaultMaxFailures, time.Duration(defaultTimeoutSec)*time.Second),
index: i,
}
}
r := &Resolver{
primary: primary,
replicas: replicaWrappers,
readFallback: true, // Default to true
logger: logger,
metrics: metrics,
stats: &statistics{},
primaryRoutes: make(map[string]bool),
stopChan: make(chan struct{}),
}
// Default strategy
if len(replicas) > 0 {
r.strategy = NewRoundRobinStrategy()
}
// Apply options
for _, opt := range opts {
opt(r)
}
for route := range r.primaryRoutes {
if strings.HasSuffix(route, "*") {
r.primaryPrefixes = append(r.primaryPrefixes, strings.TrimSuffix(route, "*"))
}
}
// Initialize metrics and start background tasks.
r.initializeMetrics()
r.startBackgroundTasks()
if r.logger != nil {
r.logger.Logf("DB Resolver initialized with %d replicas using circuit breakers", len(replicas))
}
return r
}
// initializeMetrics sets up metrics following GoFr patterns.
func (r *Resolver) initializeMetrics() {
if r.metrics == nil {
return
}
// Histogram for query response times
buckets := []float64{0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0}
r.metrics.NewHistogram("dbresolver_query_duration", "Response time of DB resolver operations in microseconds", buckets...)
// Gauges for operation tracking
r.metrics.NewGauge("dbresolver_primary_reads", "Total reads routed to primary")
r.metrics.NewGauge("dbresolver_primary_writes", "Total writes routed to primary")
r.metrics.NewGauge("dbresolver_replica_reads", "Total reads routed to replicas")
r.metrics.NewGauge("dbresolver_fallbacks", "Total fallbacks to primary")
r.metrics.NewGauge("dbresolver_failures", "Total replica failures")
}
// startBackgroundTasks starts minimal background processing.
func (r *Resolver) startBackgroundTasks() {
r.wg.Add(1)
go r.backgroundProcessor()
}
// backgroundProcessor handles metrics collection with reduced frequency.
func (r *Resolver) backgroundProcessor() {
defer r.wg.Done()
ticker := time.NewTicker(time.Duration(defaultTimeoutSec) * time.Second)
defer ticker.Stop()
for {
select {
case <-r.stopChan:
return
case <-ticker.C:
r.updateMetrics()
}
}
}
// updateMetrics updates gauge metrics.
func (r *Resolver) updateMetrics() {
if r.metrics == nil {
return
}
r.metrics.SetGauge("dbresolver_primary_reads", float64(r.stats.primaryReads.Load()))
r.metrics.SetGauge("dbresolver_primary_writes", float64(r.stats.primaryWrites.Load()))
r.metrics.SetGauge("dbresolver_replica_reads", float64(r.stats.replicaReads.Load()))
r.metrics.SetGauge("dbresolver_fallbacks", float64(r.stats.primaryFallbacks.Load()))
r.metrics.SetGauge("dbresolver_failures", float64(r.stats.replicaFailures.Load()))
}
// shouldUseReplica determines routing based on HTTP method and path.
func (r *Resolver) shouldUseReplica(ctx context.Context) bool {
if len(r.replicas) == 0 {
return false
}
// Check if path requires primary.
if path, ok := ctx.Value(contextKeyRequestPath).(string); ok {
if r.isPrimaryRoute(path) {
return false
}
}
// Check HTTP method.
method, ok := ctx.Value(contextKeyHTTPMethod).(string)
if !ok {
return false // Default to primary for safety.
}
return method == "GET"
}
// isPrimaryRoute checks if path matches primary route patterns.
func (r *Resolver) isPrimaryRoute(path string) bool {
if r.primaryRoutes[path] {
return true
}
// Prefix match (precompiled patterns)
for _, prefix := range r.primaryPrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
// selectHealthyReplica chooses an available replica using circuit breaker.
func (r *Resolver) selectHealthyReplica() *replicaWrapper {
if len(r.replicas) == 0 {
return nil
}
// Build list of healthy replica indices
var healthyIndices []int
for i, wrapper := range r.replicas {
if wrapper.breaker.allowRequest() {
healthyIndices = append(healthyIndices, i)
}
}
if len(healthyIndices) == 0 {
if r.logger != nil {
r.logger.Warn("All replicas are unavailable (circuit breakers open), falling back to primary")
}
return nil
}
// Strategy picks index from healthy replicas
idx := r.strategy.Next(len(healthyIndices))
if idx < 0 || idx >= len(healthyIndices) {
return nil
}
originalIdx := healthyIndices[idx]
return r.replicas[originalIdx]
}
// addTrace adds tracing information to the context and returns a span.
func (r *Resolver) addTrace(ctx context.Context, method, query string) (context.Context, trace.Span) {
if r.tracer == nil {
return ctx, nil
}
tracedCtx, span := r.tracer.Start(ctx, fmt.Sprintf("dbresolver-%s", method))
if span != nil {
span.SetAttributes(
attribute.String("dbresolver.query", query),
attribute.String("dbresolver.method", method),
)
}
return tracedCtx, span
}
// recordStats records operation statistics and updates tracing spans.
func (r *Resolver) recordStats(start time.Time, method, target string, span trace.Span, isRead bool, replicaIndex *int) {
duration := time.Since(start).Microseconds()
// Update trace if available.
if span != nil {
defer span.End()
attrs := []attribute.KeyValue{
attribute.String("dbresolver.target", target),
attribute.Int64("dbresolver.duration", duration),
attribute.Bool("dbresolver.is_read", isRead),
}
// Add replica index if available
if replicaIndex != nil {
attrs = append(attrs, attribute.Int("dbresolver.replica_index", *replicaIndex))
}
span.SetAttributes(attrs...)
}
// Record metrics histogram only if metrics are enabled.
if r.metrics != nil {
r.metrics.RecordHistogram(context.Background(), "dbresolver_query_duration",
float64(duration), "method", method, "target", target)
}
}
// Query routes to replica for reads, primary for writes.
func (r *Resolver) Query(query string, args ...any) (*sql.Rows, error) {
return r.QueryContext(context.Background(), query, args...)
}
// QueryContext routes queries with optimized path.
func (r *Resolver) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
start := time.Now()
r.stats.totalQueries.Add(1)
tracedCtx, span := r.addTrace(ctx, "query", query)
useReplica := r.shouldUseReplica(ctx)
if useReplica && len(r.replicas) > 0 {
return r.executeReplicaQuery(tracedCtx, span, start, query, args...)
}
// Non-GET requests or no replicas - use primary.
r.stats.primaryWrites.Add(1)
rows, err := r.primary.QueryContext(tracedCtx, query, args...)
r.recordStats(start, "query", "primary", span, false, nil)
return rows, err
}
// executeReplicaQuery attempts to execute query on replica with fallback handling.
func (r *Resolver) executeReplicaQuery(ctx context.Context, span trace.Span, start time.Time,
query string, args ...any) (*sql.Rows, error) {
wrapper := r.selectHealthyReplica()
if wrapper == nil {
return r.fallbackToPrimary(ctx, span, start, query, "No healthy replica available, falling back to primary", args...)
}
rows, err := wrapper.db.QueryContext(ctx, query, args...)
if err == nil {
r.stats.replicaReads.Add(1)
wrapper.breaker.recordSuccess()
r.recordStats(start, "query", "replica", span, true, &wrapper.index)
return rows, nil
}
// Record failure.
wrapper.breaker.recordFailure()
r.stats.replicaFailures.Add(1)
if r.logger != nil {
r.logger.Errorf("Replica #%d failed, circuit breaker triggered: %v", wrapper.index+1, err)
}
return r.fallbackToPrimary(ctx, span, start, query, "Falling back to primary for read operation", args...)
}
// fallbackToPrimary handles primary fallback logic with custom warning message.
func (r *Resolver) fallbackToPrimary(ctx context.Context, span trace.Span, start time.Time,
query, warningMsg string, args ...any) (*sql.Rows, error) {
if !r.readFallback {
r.recordStats(start, "query", "replica-failed", span, true, nil)
return nil, errReplicaFailedNoFallback
}
r.stats.primaryFallbacks.Add(1)
r.stats.primaryReads.Add(1)
if r.logger != nil && warningMsg != "" {
r.logger.Warn(warningMsg)
}
rows, err := r.primary.QueryContext(ctx, query, args...)
r.recordStats(start, "query", "primary-fallback", span, true, nil)
return rows, err
}
// QueryRow routes to replica for reads, primary for writes.
func (r *Resolver) QueryRow(query string, args ...any) *sql.Row {
return r.QueryRowContext(context.Background(), query, args...)
}
// QueryRowContext routes queries with circuit breaker.
func (r *Resolver) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
start := time.Now()
r.stats.totalQueries.Add(1)
useReplica := r.shouldUseReplica(ctx)
tracedCtx, span := r.addTrace(ctx, "query-row", query)
if useReplica && len(r.replicas) > 0 {
wrapper := r.selectHealthyReplica()
if wrapper != nil {
r.stats.replicaReads.Add(1)
wrapper.breaker.recordSuccess()
r.recordStats(start, "query-row", "replica", span, true, &wrapper.index)
return wrapper.db.QueryRowContext(tracedCtx, query, args...)
}
r.stats.replicaFailures.Add(1)
}
r.stats.primaryWrites.Add(1)
r.recordStats(start, "query-row", "primary", span, false, nil)
return r.primary.QueryRowContext(tracedCtx, query, args...)
}
// Exec always routes to primary (write operation).
func (r *Resolver) Exec(query string, args ...any) (sql.Result, error) {
return r.ExecContext(context.Background(), query, args...)
}
// ExecContext always routes to primary (write operation).
func (r *Resolver) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
start := time.Now()
r.stats.primaryWrites.Add(1)
r.stats.totalQueries.Add(1)
tracedCtx, span := r.addTrace(ctx, "exec", query)
defer r.recordStats(start, "exec", "primary", span, false, nil)
return r.primary.ExecContext(tracedCtx, query, args...)
}
// Select routes to replica for reads, primary for writes.
func (r *Resolver) Select(ctx context.Context, data any, query string, args ...any) {
start := time.Now()
r.stats.totalQueries.Add(1)
tracedCtx, span := r.addTrace(ctx, "select", query)
useReplica := r.shouldUseReplica(ctx)
if useReplica && len(r.replicas) > 0 {
wrapper := r.selectHealthyReplica()
if wrapper != nil {
r.stats.replicaReads.Add(1)
wrapper.breaker.recordSuccess()
wrapper.db.Select(tracedCtx, data, query, args...)
r.recordStats(start, "select", "replica", span, true, &wrapper.index)
return
}
r.stats.replicaFailures.Add(1)
}
r.stats.primaryWrites.Add(1)
r.primary.Select(tracedCtx, data, query, args...)
r.recordStats(start, "select", "primary", span, false, nil)
}
// Prepare always routes to primary (consistency).
func (r *Resolver) Prepare(query string) (*sql.Stmt, error) {
r.stats.totalQueries.Add(1)
return r.primary.Prepare(query)
}
// Begin always routes to primary (transactions).
func (r *Resolver) Begin() (*gofrSQL.Tx, error) {
r.stats.totalQueries.Add(1)
return r.primary.Begin()
}
// Dialect returns the database dialect.
func (r *Resolver) Dialect() string {
return r.primary.Dialect()
}
// HealthCheck returns comprehensive health information.
func (r *Resolver) HealthCheck() *datasource.Health {
primaryHealth := r.primary.HealthCheck()
health := &datasource.Health{
Status: primaryHealth.Status,
Details: map[string]any{
"primary": primaryHealth,
"stats": map[string]any{
"primaryReads": r.stats.primaryReads.Load(),
"primaryWrites": r.stats.primaryWrites.Load(),
"replicaReads": r.stats.replicaReads.Load(),
"primaryFallbacks": r.stats.primaryFallbacks.Load(),
"replicaFailures": r.stats.replicaFailures.Load(),
"totalQueries": r.stats.totalQueries.Load(),
},
},
}
// Check replica health with circuit breaker status
replicaDetails := make([]any, len(r.replicas))
for i, wrapper := range r.replicas {
replicaHealth := wrapper.db.HealthCheck()
state := wrapper.breaker.state.Load()
var stateStr string
switch *state {
case circuitStateClosed:
stateStr = "CLOSED"
case circuitStateOpen:
stateStr = "OPEN"
case circuitStateHalfOpen:
stateStr = "HALF_OPEN"
}
replicaDetails[i] = map[string]any{
"index": i,
"health": replicaHealth,
"circuit_state": stateStr,
"failures": wrapper.breaker.failures.Load(),
}
}
health.Details["replicas"] = replicaDetails
return health
}
// Close cleans up resources properly.
func (r *Resolver) Close() error {
var err error
// Stop background tasks only once
r.once.Do(func() {
close(r.stopChan)
r.wg.Wait()
})
// Close primary
if closeErr := r.primary.Close(); closeErr != nil {
err = closeErr
}
// Close replicas
for _, wrapper := range r.replicas {
if closeErr := wrapper.db.Close(); closeErr != nil {
err = closeErr
}
}
return err
}
// WithHTTPMethod adds HTTP method to context for routing decisions.
func WithHTTPMethod(ctx context.Context, method string) context.Context {
return context.WithValue(ctx, contextKeyHTTPMethod, method)
}
// WithRequestPath adds request path to context.
func WithRequestPath(ctx context.Context, path string) context.Context {
return context.WithValue(ctx, contextKeyRequestPath, path)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/resolver_test.go
================================================
package dbresolver
import (
"database/sql"
"errors"
"fmt"
"reflect"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource"
gofrSQL "gofr.dev/pkg/gofr/datasource/sql"
)
var errTestReplicaFailed = errors.New("replica connection failed")
const (
healthStatusUP = "UP"
healthStatusDOWN = "DOWN"
)
// Mocks contains all the dependencies needed for testing.
type Mocks struct {
Ctrl *gomock.Controller
Primary *MockDB
Replicas []container.DB
MockReplicas []*MockDB
Strategy *MockStrategy
Logger MockLogger
Metrics Metrics
Resolver *Resolver
}
// setupMocks creates and returns common mocks used in tests.
func setupMocks(t *testing.T) *Mocks {
t.Helper()
// Create controller and mocks.
ctrl := gomock.NewController(t)
mockPrimary := NewMockDB(ctrl)
mockReplica1 := NewMockDB(ctrl)
mockReplica2 := NewMockDB(ctrl)
// Create both typed and interface versions of replicas.
typedMockReplicas := []*MockDB{mockReplica1, mockReplica2}
mockReplicas := make([]container.DB, len(typedMockReplicas))
for i, r := range typedMockReplicas {
mockReplicas[i] = r
}
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockStrategy := NewMockStrategy(ctrl)
mockMetrics.EXPECT().NewHistogram("dbresolver_query_duration",
"Response time of DB resolver operations in microseconds",
gomock.Any()).AnyTimes()
mockMetrics.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks := &Mocks{
Ctrl: ctrl,
Primary: mockPrimary,
Replicas: mockReplicas,
MockReplicas: typedMockReplicas,
Strategy: mockStrategy,
Logger: *mockLogger,
Metrics: mockMetrics,
}
replicaWrappers := make([]*replicaWrapper, len(mockReplicas))
for i, r := range mockReplicas {
replicaWrappers[i] = &replicaWrapper{
db: r,
breaker: newCircuitBreaker(5, 30*time.Second),
}
}
mocks.Resolver = &Resolver{
primary: mockPrimary,
replicas: replicaWrappers,
strategy: mockStrategy,
readFallback: true,
tracer: otel.GetTracerProvider().Tracer("gofr-dbresolver"),
logger: mockLogger,
metrics: mockMetrics,
stats: &statistics{},
stopChan: make(chan struct{}),
once: sync.Once{},
}
return mocks
}
// MockResult is a mock of sql.Result interface.
type MockResult struct {
ctrl *gomock.Controller
recorder *MockResultMockRecorder
}
// MockResultMockRecorder is the mock recorder for MockResult.
type MockResultMockRecorder struct {
mock *MockResult
}
// NewMockResult creates a new mock instance.
func NewMockResult(ctrl *gomock.Controller) *MockResult {
mock := &MockResult{ctrl: ctrl}
mock.recorder = &MockResultMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockResult) EXPECT() *MockResultMockRecorder {
return m.recorder
}
// LastInsertId mocks base method.
func (m *MockResult) LastInsertId() (int64, error) {
ret := m.ctrl.Call(m, "LastInsertId")
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// LastInsertId indicates an expected call of LastInsertId.
func (mr *MockResultMockRecorder) LastInsertId() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastInsertId", reflect.TypeOf((*MockResult)(nil).LastInsertId))
}
// RowsAffected mocks base method.
func (m *MockResult) RowsAffected() (int64, error) {
ret := m.ctrl.Call(m, "RowsAffected")
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// RowsAffected indicates an expected call of RowsAffected.
func (mr *MockResultMockRecorder) RowsAffected() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RowsAffected", reflect.TypeOf((*MockResult)(nil).RowsAffected))
}
func TestResolver_Query_ReadGoesToReplica(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRows := &sql.Rows{}
readQuery := "SELECT * FROM users"
ctx := WithHTTPMethod(t.Context(), "GET")
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().QueryContext(gomock.Any(), readQuery).Return(expectedRows, nil)
rows, err := mocks.Resolver.QueryContext(ctx, readQuery)
require.NoError(t, err)
assert.NoError(t, rows.Err())
assert.Equal(t, expectedRows, rows)
}
func TestResolver_Query_WriteGoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRows := &sql.Rows{}
writeQuery := "INSERT INTO users (name) VALUES (?)"
args := []any{"test_user"}
mocks.Primary.EXPECT().QueryContext(gomock.Any(), writeQuery, args[0]).Return(expectedRows, nil)
rows, err := mocks.Resolver.Query(writeQuery, args[0])
require.NoError(t, err)
assert.NoError(t, rows.Err())
assert.Equal(t, expectedRows, rows)
}
func TestResolver_QueryContext_ReadGoesToReplica(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRows := &sql.Rows{}
readQuery := "SELECT * FROM users WHERE id = ?"
args := []any{1}
ctx := WithHTTPMethod(t.Context(), "GET")
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().QueryContext(gomock.Any(), readQuery, args[0]).Return(expectedRows, nil)
rows, err := mocks.Resolver.QueryContext(ctx, readQuery, args[0])
require.NoError(t, err)
require.NoError(t, rows.Err())
assert.NotNil(t, rows)
assert.Equal(t, expectedRows, rows)
}
func TestResolver_QueryContext_WriteGoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRows := &sql.Rows{}
writeQuery := "INSERT INTO users (name) VALUES (?)"
args := []any{"test_user"}
mocks.Primary.EXPECT().QueryContext(gomock.Any(), writeQuery, args[0]).Return(expectedRows, nil)
rows, err := mocks.Resolver.QueryContext(t.Context(), writeQuery, args[0])
require.NoError(t, err)
assert.NoError(t, rows.Err())
assert.Equal(t, expectedRows, rows)
}
func TestResolver_QueryRow_ReadGoesToReplica(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRow := &sql.Row{}
readQuery := "SELECT * FROM users WHERE id = ?"
args := []any{1}
ctx := WithHTTPMethod(t.Context(), "GET")
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().QueryRowContext(gomock.Any(), readQuery, args[0]).Return(expectedRow)
row := mocks.Resolver.QueryRowContext(ctx, readQuery, args[0])
assert.NotNil(t, row)
assert.NoError(t, row.Err())
}
func TestResolver_QueryRow_WriteGoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRow := &sql.Row{}
writeQuery := "INSERT INTO users (name) VALUES (?) RETURNING id"
args := []any{"test_user"}
mocks.Primary.EXPECT().QueryRowContext(gomock.Any(), writeQuery, args[0]).Return(expectedRow)
row := mocks.Resolver.QueryRow(writeQuery, args[0])
assert.NotNil(t, row)
assert.NoError(t, row.Err())
}
func TestResolver_QueryRowContext_ReadGoesToReplica(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRow := &sql.Row{}
readQuery := "SELECT * FROM users WHERE id = ?"
args := []any{1}
ctx := WithHTTPMethod(t.Context(), "GET")
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().QueryRowContext(gomock.Any(), readQuery, args[0]).Return(expectedRow)
row := mocks.Resolver.QueryRowContext(ctx, readQuery, args[0])
assert.NotNil(t, row)
assert.NoError(t, row.Err())
}
func TestResolver_QueryRowContext_WriteGoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRow := &sql.Row{}
writeQuery := "INSERT INTO users (name) VALUES (?) RETURNING id"
args := []any{"test_user"}
mocks.Primary.EXPECT().QueryRowContext(gomock.Any(), writeQuery, args[0]).Return(expectedRow)
row := mocks.Resolver.QueryRowContext(t.Context(), writeQuery, args[0])
assert.NotNil(t, row)
assert.NoError(t, row.Err())
}
func TestResolver_ExecContext_GoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedResult := NewMockResult(mocks.Ctrl)
writeQuery := "UPDATE users SET name = ? WHERE id = ?"
args := []any{"new_name", 1}
mocks.Primary.EXPECT().ExecContext(gomock.Any(), writeQuery, args[0], args[1]).Return(expectedResult, nil)
result, err := mocks.Resolver.ExecContext(t.Context(), writeQuery, args[0], args[1])
require.NoError(t, err)
assert.Equal(t, expectedResult, result)
}
func TestResolver_Exec_GoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedResult := NewMockResult(mocks.Ctrl)
writeQuery := "UPDATE users SET name = ? WHERE id = ?"
args := []any{"new_name", 1}
mocks.Primary.EXPECT().ExecContext(gomock.Any(), writeQuery, args[0], args[1]).Return(expectedResult, nil)
result, err := mocks.Resolver.Exec(writeQuery, args[0], args[1])
require.NoError(t, err)
assert.Equal(t, expectedResult, result)
}
func TestResolver_Select_ReadGoesToReplica(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
data := &struct{ Name string }{}
readQuery := "SELECT name FROM users WHERE id = ?"
args := []any{1}
ctx := WithHTTPMethod(t.Context(), "GET")
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().Select(gomock.Any(), data, readQuery, args[0])
mocks.Resolver.Select(ctx, data, readQuery, args[0])
}
func TestResolver_Select_WriteGoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
data := &struct{ ID int64 }{}
writeQuery := "INSERT INTO users (name) VALUES (?) RETURNING id"
args := []any{"test_user"}
mocks.Primary.EXPECT().Select(gomock.Any(), data, writeQuery, args[0])
mocks.Resolver.Select(t.Context(), data, writeQuery, args[0])
}
func TestResolver_Prepare_GoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedStmt := &sql.Stmt{}
query := "SELECT * FROM users WHERE id = ?"
mocks.Primary.EXPECT().Prepare(query).Return(expectedStmt, nil)
stmt, err := mocks.Resolver.Prepare(query)
require.NoError(t, err)
assert.Equal(t, expectedStmt, stmt)
}
func TestResolver_Begin_GoesToPrimary(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedTx := &gofrSQL.Tx{}
mocks.Primary.EXPECT().Begin().Return(expectedTx, nil)
tx, err := mocks.Resolver.Begin()
require.NoError(t, err)
assert.Equal(t, expectedTx, tx)
}
func TestResolver_Dialect(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedDialect := "mysql"
mocks.Primary.EXPECT().Dialect().Return(expectedDialect)
dialect := mocks.Resolver.Dialect()
assert.Equal(t, expectedDialect, dialect)
}
func TestResolver_Close(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
// Test primary close
mocks.Primary.EXPECT().Close().Return(nil)
// Test replicas close
for _, replica := range mocks.MockReplicas {
replica.EXPECT().Close().Return(nil)
}
err := mocks.Resolver.Close()
assert.NoError(t, err)
}
func TestResolver_Close_WithError(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
// Primary returns error
mocks.Primary.EXPECT().Close().Return(errTestReplicaFailed)
// Replicas don't error
for _, replica := range mocks.MockReplicas {
replica.EXPECT().Close().Return(nil)
}
err := mocks.Resolver.Close()
assert.Equal(t, errTestReplicaFailed, err)
}
func TestResolver_QueryContext_WithFallback(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
expectedRows := &sql.Rows{}
readQuery := "SELECT * FROM users"
ctx := WithHTTPMethod(t.Context(), "GET")
// First replica attempt fails
mocks.Strategy.EXPECT().Next(2).Return(0)
mocks.MockReplicas[0].EXPECT().QueryContext(gomock.Any(), readQuery).Return(nil, errTestReplicaFailed)
mocks.Logger.EXPECT().Warn("Falling back to primary for read operation")
// Fallback to primary succeeds
mocks.Primary.EXPECT().QueryContext(gomock.Any(), readQuery).Return(expectedRows, nil)
rows, err := mocks.Resolver.QueryContext(ctx, readQuery)
require.NoError(t, rows.Err())
require.NoError(t, err)
assert.NotNil(t, rows)
}
func TestResolver_HealthCheck(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
primaryHealth := &datasource.Health{
Status: healthStatusUP,
Details: map[string]any{
"connections": 5,
},
}
replica1Health := &datasource.Health{
Status: healthStatusUP,
Details: map[string]any{
"connections": 3,
},
}
replica2Health := &datasource.Health{
Status: healthStatusDOWN,
Details: map[string]any{
"error": "connection refused",
},
}
mocks.Primary.EXPECT().HealthCheck().Return(primaryHealth)
mocks.MockReplicas[0].EXPECT().HealthCheck().Return(replica1Health)
mocks.MockReplicas[1].EXPECT().HealthCheck().Return(replica2Health)
health := mocks.Resolver.HealthCheck()
assert.Equal(t, healthStatusUP, health.Status)
assert.Equal(t, primaryHealth, health.Details["primary"])
replicas := health.Details["replicas"].([]any)
assert.Len(t, replicas, 2)
stats := health.Details["stats"].(map[string]any)
assert.Contains(t, stats, "primaryReads")
assert.Contains(t, stats, "replicaReads")
assert.Contains(t, stats, "totalQueries")
}
func TestResolver_AddTrace(t *testing.T) {
mocks := setupMocks(t)
defer mocks.Ctrl.Finish()
method := "query"
query := "SELECT * FROM users"
_, span := mocks.Resolver.addTrace(t.Context(), method, query)
assert.NotNil(t, span)
}
// setupNewResolverTest creates a test setup for NewResolver tests.
func setupNewResolverTest(t *testing.T, replicaCount int, expectLogger bool) (*gomock.Controller, *MockDB,
[]container.DB, *MockLogger, *MockMetrics) {
t.Helper()
ctrl := gomock.NewController(t)
mockPrimary := NewMockDB(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Create replicas
replicas := make([]container.DB, replicaCount)
for i := 0; i < replicaCount; i++ {
replicas[i] = NewMockDB(ctrl)
}
// Set up common expectations
mockMetrics.EXPECT().NewHistogram(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
mockMetrics.EXPECT().NewGauge(gomock.Any(), gomock.Any()).Times(5)
if expectLogger {
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(1)
}
return ctrl, mockPrimary, replicas, mockLogger, mockMetrics
}
func TestNewResolver_WithOptions(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
customStrategy := NewMockStrategy(ctrl)
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithStrategy(customStrategy),
WithFallback(false),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
assert.Equal(t, customStrategy, dbResolver.strategy)
assert.False(t, dbResolver.readFallback)
assert.Len(t, dbResolver.replicas, 1)
}
func TestNewResolver_WithPrimaryRoutes(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{
"/api/v1/admin": true,
"/api/v2/write*": true,
"/health": true,
}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
assert.Len(t, dbResolver.primaryRoutes, 3)
assert.Len(t, dbResolver.primaryPrefixes, 1)
assert.Contains(t, dbResolver.primaryPrefixes, "/api/v2/write")
}
func TestNewResolver_NoReplicas(t *testing.T) {
ctrl, mockPrimary, _, mockLogger, mockMetrics := setupNewResolverTest(t, 0, true)
defer ctrl.Finish()
resolver := NewResolver(mockPrimary, nil, mockLogger, mockMetrics)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
assert.Empty(t, dbResolver.replicas)
assert.Nil(t, dbResolver.strategy)
}
func TestNewResolver_NoLogger(t *testing.T) {
ctrl, mockPrimary, replicas, _, mockMetrics := setupNewResolverTest(t, 1, false)
defer ctrl.Finish()
resolver := NewResolver(mockPrimary, replicas, nil, mockMetrics)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
assert.Nil(t, dbResolver.logger)
}
func TestNewResolver_NoMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockPrimary := NewMockDB(ctrl)
mockReplica := NewMockDB(ctrl)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(1)
resolver := NewResolver(mockPrimary, []container.DB{mockReplica}, mockLogger, nil)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
assert.Nil(t, dbResolver.metrics)
}
func TestResolver_QueryContext_NoReplicasConfigured(t *testing.T) {
ctrl, mockPrimary, _, mockLogger, mockMetrics := setupNewResolverTest(t, 0, true)
defer ctrl.Finish()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dbresolver_query_duration", gomock.Any(), gomock.Any()).Times(1)
resolver := NewResolver(mockPrimary, nil, mockLogger, mockMetrics)
expectedRows := &sql.Rows{}
readQuery := "SELECT * FROM users"
ctx := WithHTTPMethod(t.Context(), "GET")
mockPrimary.EXPECT().QueryContext(gomock.Any(), readQuery).Return(expectedRows, nil)
rows, err := resolver.QueryContext(ctx, readQuery)
require.NoError(t, rows.Err())
require.NoError(t, err)
assert.Equal(t, expectedRows, rows)
}
func TestResolver_ShouldUseReplica_WithRequestPath(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{"/admin": true}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
ctx := WithHTTPMethod(t.Context(), "GET")
ctx = WithRequestPath(ctx, "/admin")
useReplica := dbResolver.shouldUseReplica(ctx)
assert.False(t, useReplica)
}
func TestResolver_ShouldUseReplica_WithPrefixMatch(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{"/api/admin/*": true}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
ctx := WithHTTPMethod(t.Context(), "GET")
ctx = WithRequestPath(ctx, "/api/admin/users")
useReplica := dbResolver.shouldUseReplica(ctx)
assert.False(t, useReplica)
}
func TestResolver_ShouldUseReplica_NoPathMatch(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{"/admin": true}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
ctx := WithHTTPMethod(t.Context(), "GET")
ctx = WithRequestPath(ctx, "/api/users")
useReplica := dbResolver.shouldUseReplica(ctx)
assert.True(t, useReplica)
}
func TestResolver_ShouldUseReplica_NoMethod(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
useReplica := dbResolver.shouldUseReplica(t.Context())
assert.False(t, useReplica)
}
func TestResolver_IsPrimaryRoute_ExactMatch(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{"/exact/path": true}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
isPrimary := dbResolver.isPrimaryRoute("/exact/path")
assert.True(t, isPrimary)
}
func TestResolver_IsPrimaryRoute_NoMatch(t *testing.T) {
ctrl, mockPrimary, replicas, mockLogger, mockMetrics := setupNewResolverTest(t, 1, true)
defer ctrl.Finish()
routes := map[string]bool{"/admin": true}
resolver := NewResolver(mockPrimary, replicas, mockLogger, mockMetrics,
WithPrimaryRoutes(routes),
)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
isPrimary := dbResolver.isPrimaryRoute("/api/users")
assert.False(t, isPrimary)
}
func TestResolver_UpdateMetrics_NoMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockPrimary := NewMockDB(ctrl)
mockReplica := NewMockDB(ctrl)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).Times(1)
resolver := NewResolver(mockPrimary, []container.DB{mockReplica}, mockLogger, nil)
dbResolver, ok := resolver.(*Resolver)
require.True(t, ok)
dbResolver.updateMetrics()
assert.Nil(t, dbResolver.metrics)
}
func TestQueryLog_PrettyPrint(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
tests := []struct {
name string
log *QueryLog
check func(string)
}{
{
name: "basic formatting",
log: &QueryLog{
Type: "SQL",
Query: "SELECT * FROM users WHERE id = 1",
Duration: 1500,
Target: "replica-1",
IsRead: true,
},
check: func(output string) {
assert.Contains(t, output, "DBRESOLVER")
assert.Contains(t, output, "1500")
assert.Contains(t, output, "replica-1")
},
},
{
name: "multiple whitespace cleanup",
log: &QueryLog{
Type: "SQL",
Query: "SELECT * FROM users WHERE id = 1",
Duration: 2000,
Target: "primary",
IsRead: false,
},
check: func(output string) {
assert.Contains(t, output, "SELECT * FROM users WHERE id = 1")
assert.NotContains(t, output, "SELECT *")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(*testing.T) {
var capturedLog string
mockLogger.EXPECT().Debug(gomock.Any()).Do(func(args ...any) {
capturedLog = fmt.Sprint(args...)
}).Times(1)
tt.log.PrettyPrint(mockLogger)
tt.check(capturedLog)
})
}
}
func TestClean(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{"multiple spaces", "SELECT * FROM users", "SELECT * FROM users"},
{"leading/trailing whitespace", " SELECT * FROM users ", "SELECT * FROM users"},
{"tabs", "SELECT\t*\tFROM\tusers", "SELECT * FROM users"},
{"newlines", "SELECT *\nFROM users\nWHERE id = 1", "SELECT * FROM users WHERE id = 1"},
{"mixed whitespace", " SELECT \t*\n FROM users\t\n", "SELECT * FROM users"},
{"empty string", "", ""},
{"only whitespace", " \t\n ", ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := clean(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
================================================
FILE: pkg/gofr/datasource/dbresolver/strategy.go
================================================
package dbresolver
import (
"math/rand"
"sync/atomic"
)
// StrategyType defines the load balancing strategy type.
type StrategyType string
const (
// StrategyRoundRobin distributes load sequentially across replicas.
StrategyRoundRobin StrategyType = "round-robin"
// StrategyRandom randomly selects a replica for each request.
StrategyRandom StrategyType = "random"
)
// Strategy interface defines replica selection logic.
type Strategy interface {
Name() string
Next(count int) int
}
// RoundRobinStrategy selects replicas in round-robin order.
type RoundRobinStrategy struct {
current atomic.Uint64
}
// NewRoundRobinStrategy creates a new round-robin strategy instance.
func NewRoundRobinStrategy() Strategy {
return &RoundRobinStrategy{}
}
// Next selects the next replica index in round-robin fashion.
func (s *RoundRobinStrategy) Next(count int) int {
if count <= 0 {
return -1
}
next := s.current.Add(1)
return int((next - 1) % uint64(count)) //nolint:gosec // count is validated to be positive
}
// Name returns the name of strategy.
func (*RoundRobinStrategy) Name() string {
return string(StrategyRoundRobin)
}
// RandomStrategy selects a replica randomly.
type RandomStrategy struct{}
// NewRandomStrategy creates a new random strategy instance.
func NewRandomStrategy() Strategy {
return &RandomStrategy{}
}
// Next selects a random replica index.
func (*RandomStrategy) Next(count int) int {
if count == 0 {
return -1
}
return rand.Intn(count) //nolint:gosec // acceptable randomness for load balance
}
// Name returns the name of the strategy.
func (*RandomStrategy) Name() string {
return string(StrategyRandom)
}
================================================
FILE: pkg/gofr/datasource/dbresolver/strategy_test.go
================================================
package dbresolver
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRoundRobinStrategy_Next(t *testing.T) {
strategy := NewRoundRobinStrategy()
// Test with 3 replicas
count := 3
// First call should return index 0
idx := strategy.Next(count)
assert.Equal(t, 0, idx)
// Second call should return index 1
idx = strategy.Next(count)
assert.Equal(t, 1, idx)
// Third call should return index 2
idx = strategy.Next(count)
assert.Equal(t, 2, idx)
// Fourth call should wrap around to index 0
idx = strategy.Next(count)
assert.Equal(t, 0, idx)
}
func TestRoundRobinStrategy_Next_SingleReplica(t *testing.T) {
strategy := NewRoundRobinStrategy()
// All calls should return index 0
for i := 0; i < 5; i++ {
idx := strategy.Next(1)
assert.Equal(t, 0, idx)
}
}
func TestRoundRobinStrategy_Next_NoReplicas(t *testing.T) {
strategy := NewRoundRobinStrategy()
idx := strategy.Next(0)
assert.Equal(t, -1, idx)
}
func TestRandomStrategy_Next(t *testing.T) {
strategy := NewRandomStrategy()
count := 3
// Verify that multiple calls return valid indices
seen := make(map[int]bool)
for i := 0; i < 100; i++ {
idx := strategy.Next(count)
assert.GreaterOrEqual(t, idx, 0)
assert.Less(t, idx, count)
seen[idx] = true
}
// With 100 iterations, we should see all 3 indices
assert.Len(t, seen, count)
}
func TestRandomStrategy_Next_SingleReplica(t *testing.T) {
strategy := NewRandomStrategy()
// All calls should return index 0
for i := 0; i < 5; i++ {
idx := strategy.Next(1)
assert.Equal(t, 0, idx)
}
}
func TestRandomStrategy_Next_NoReplicas(t *testing.T) {
strategy := NewRandomStrategy()
idx := strategy.Next(0)
assert.Equal(t, -1, idx)
}
func TestStrategy_Name(t *testing.T) {
roundRobin := NewRoundRobinStrategy()
assert.Equal(t, string(StrategyRoundRobin), roundRobin.Name())
random := NewRandomStrategy()
assert.Equal(t, string(StrategyRandom), random.Name())
}
================================================
FILE: pkg/gofr/datasource/dgraph/dgraph.go
================================================
package dgraph
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/dgraph-io/dgo/v210"
"github.com/dgraph-io/dgo/v210/protos/api"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Config holds the configuration for connecting to Dgraph.
type Config struct {
Host string
Port string
}
// Client represents the Dgraph client with logging and metrics.
type Client struct {
client DgraphClient
logger Logger
metrics Metrics
config Config
tracer trace.Tracer
}
type (
Mutation = api.Mutation
Operation = api.Operation
)
var (
errInvalidMutation = errors.New("invalid mutation type")
errInvalidOperation = errors.New("invalid operation type")
errHealthCheckFailed = errors.New("dgraph health check failed")
errEmptySchema = errors.New("schema cannot be empty")
errEmptyField = errors.New("field name cannot be empty")
)
// New creates a new Dgraph client with the given configuration.
func New(config Config) *Client {
return &Client{
config: config,
}
}
// Connect connects to the Dgraph database using the provided configuration.
func (d *Client) Connect() {
address := fmt.Sprintf("%s:%s", d.config.Host, d.config.Port)
d.logger.Debugf("connecting to Dgraph at %v", address)
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
d.logger.Errorf("error while connecting to Dgraph, err: %v", err)
return
}
var responseTimeBuckets = []float64{0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0}
// Register metrics
d.metrics.NewHistogram("dgraph_query_duration", "Response time of Dgraph queries in microseconds.", responseTimeBuckets...)
d.metrics.NewHistogram("dgraph_query_with_vars_duration", "Response time of Dgraph "+
"queries with variables in microseconds.", responseTimeBuckets...)
d.metrics.NewHistogram("dgraph_mutate_duration", "Response time of Dgraph mutations in microseconds.", responseTimeBuckets...)
d.metrics.NewHistogram("dgraph_alter_duration", "Response time of Dgraph alter operations in microseconds.", responseTimeBuckets...)
dg := dgo.NewDgraphClient(api.NewDgraphClient(conn))
d.client = NewDgraphClient(dg)
// Check connection by performing a basic health check
if _, err := d.HealthCheck(context.Background()); err != nil {
d.logger.Errorf("error while connecting to Dgraph: %v", err)
return
}
d.logger.Logf("connected to Dgraph server at %v:%v", d.config.Host, d.config.Port)
}
// UseLogger sets the logger for the Dgraph client which asserts the Logger interface.
func (d *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
d.logger = l
}
}
// UseMetrics sets the metrics for the Dgraph client which asserts the Metrics interface.
func (d *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
d.metrics = m
}
}
// UseTracer sets the tracer for DGraph client.
func (d *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
d.tracer = tracer
}
}
// ApplySchema applies a validated Dgraph schema.
// The provided schema must be non-empty; otherwise, an error is returned.
func (d *Client) ApplySchema(ctx context.Context, schema string) error {
if schema == "" {
return errEmptySchema
}
op := &api.Operation{Schema: schema}
return d.Alter(ctx, op)
}
// AddOrUpdateField creates or updates a field (predicate) definition in the Dgraph schema.
// It adds a new field if it does not exist, or updates the field's type and/or directives if it does.
func (d *Client) AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error {
if strings.TrimSpace(fieldName) == "" {
return errEmptyField
}
// Construct the field definition in the format: ": ."
newDef := fmt.Sprintf("%s: %s %s.", fieldName, fieldType, directives)
return d.ApplySchema(ctx, newDef)
}
// DropField removes a specific field (predicate) and its associated data from the Dgraph schema.
// It does so by creating an operation that instructs Dgraph to drop the given attribute.
// An error is returned if the operation fails.
func (d *Client) DropField(ctx context.Context, fieldName string) error {
op := &api.Operation{DropAttr: fieldName}
return d.Alter(ctx, op)
}
// Query executes a read-only query in the Dgraph database and returns the result.
func (d *Client) Query(ctx context.Context, query string) (any, error) {
start := time.Now()
tracedCtx, span := d.addTrace(ctx, "query")
// Execute query
resp, err := d.client.NewTxn().Query(tracedCtx, query)
duration := time.Since(start).Microseconds()
// Create and log the query details
ql := &QueryLog{
Type: "query",
URL: query,
Duration: duration,
}
if err != nil {
d.logger.Error("dgraph query failed: ", err)
ql.PrettyPrint(d.logger)
return nil, err
}
d.sendOperationStats(tracedCtx, start, query, "query", span, ql, "dgraph_query_duration")
return resp, nil
}
// QueryWithVars executes a read-only query with variables in the Dgraph database.
// QueryWithVars executes a read-only query with variables in the Dgraph database.
func (d *Client) QueryWithVars(ctx context.Context, query string, vars map[string]string) (any, error) {
start := time.Now()
tracedCtx, span := d.addTrace(ctx, "query-with-vars")
// Execute the query with variables
resp, err := d.client.NewTxn().QueryWithVars(tracedCtx, query, vars)
duration := time.Since(start).Microseconds()
// Create and log the query details
ql := &QueryLog{
Type: "queryWithVars",
URL: fmt.Sprintf("Query: %s, Vars: %v", query, vars),
Duration: duration,
}
if span != nil {
span.SetAttributes(attribute.String("dgraph.query.vars", fmt.Sprintf("%v", vars)))
}
if err != nil {
d.logger.Error("dgraph queryWithVars failed: ", err)
ql.PrettyPrint(d.logger)
return nil, err
}
d.sendOperationStats(tracedCtx, start, query, "query-with-vars", span, ql, "dgraph_query_with_vars_duration")
return resp, nil
}
// Mutate executes a write operation (mutation) in the Dgraph database and returns the result.
func (d *Client) Mutate(ctx context.Context, mu any) (any, error) {
start := time.Now()
tracedCtx, span := d.addTrace(ctx, "mutate")
// Cast to proper mutation type
mutation, ok := mu.(*api.Mutation)
if !ok {
return nil, errInvalidMutation
}
// Execute mutation
resp, err := d.client.NewTxn().Mutate(tracedCtx, mutation)
duration := time.Since(start).Microseconds()
// Create and log the mutation details
ql := &QueryLog{
Type: "mutation",
URL: mutationToString(mutation),
Duration: duration,
}
if err != nil {
d.logger.Error("dgraph mutation failed: ", err)
ql.PrettyPrint(d.logger)
return nil, err
}
d.sendOperationStats(tracedCtx, start, mutationToString(mutation), "mutate", span, ql, "dgraph_mutate_duration")
return resp, nil
}
// Alter applies schema or other changes to the Dgraph database.
func (d *Client) Alter(ctx context.Context, op any) error {
start := time.Now()
tracedCtx, span := d.addTrace(ctx, "alter")
// Cast to proper operation type
operation, ok := op.(*api.Operation)
if !ok {
d.logger.Error("invalid operation type provided to alter")
return errInvalidOperation
}
// Apply the schema changes
err := d.client.Alter(tracedCtx, operation)
duration := time.Since(start).Microseconds()
// Create and log the operation details
ql := &QueryLog{
Type: "Alter",
URL: operation.String(),
Duration: duration,
}
if span != nil {
span.SetAttributes(attribute.String("dgraph.alter.operation", operation.String()))
}
if err != nil {
d.logger.Error("dgraph alter failed: ", err)
ql.PrettyPrint(d.logger)
return err
}
d.sendOperationStats(tracedCtx, start, operation.String(), "alter", span, ql, "dgraph_alter_duration")
return nil
}
// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database.
func (d *Client) NewTxn() any {
return d.client.NewTxn()
}
// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database.
func (d *Client) NewReadOnlyTxn() any {
return d.client.NewReadOnlyTxn()
}
// HealthCheck performs a basic health check by pinging the Dgraph server.
func (d *Client) HealthCheck(ctx context.Context) (any, error) {
healthResponse, err := d.client.NewTxn().Query(ctx, `{
health(func: has(dgraph.type)) {
status
}
}`)
if err != nil || len(healthResponse.Json) == 0 {
d.logger.Error("dgraph health check failed: ", err)
return "DOWN", errHealthCheckFailed
}
return "UP", nil
}
func (d *Client) addTrace(ctx context.Context, method string) (context.Context, trace.Span) {
if d.tracer == nil {
return ctx, nil
}
tracedCtx, span := d.tracer.Start(ctx, fmt.Sprintf("dgraph-%v", method))
return tracedCtx, span
}
func (d *Client) sendOperationStats(ctx context.Context, start time.Time, query, method string,
span trace.Span, queryLog *QueryLog, metricName string) {
duration := time.Since(start).Microseconds()
if span != nil {
defer span.End()
span.SetAttributes(attribute.String(fmt.Sprintf("dgraph.%v.query", method), query))
span.SetAttributes(attribute.Int64(fmt.Sprintf("dgraph.%v.duration", method), duration))
}
queryLog.PrettyPrint(d.logger)
d.metrics.RecordHistogram(ctx, metricName, float64(queryLog.Duration))
}
func mutationToString(mutation *api.Mutation) string {
var compacted bytes.Buffer
if err := json.Compact(&compacted, mutation.SetJson); err != nil {
return ""
}
return compacted.String()
}
================================================
FILE: pkg/gofr/datasource/dgraph/dgraph_test.go
================================================
package dgraph
import (
"context"
"errors"
"testing"
"github.com/dgraph-io/dgo/v210/protos/api"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
var (
errQueryFailed = errors.New("query failed")
errAlterFailed = errors.New("alter failed")
errMutationFailed = errors.New("mutation failed")
)
func setupDB(t *testing.T) (*Client, *MockDgraphClient, *MockLogger, *MockMetrics) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
config := Config{Host: "localhost", Port: "9080"}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-dgraph"))
mockDgraphClient := NewMockDgraphClient(ctrl)
client.client = mockDgraphClient
return client, mockDgraphClient, mockLogger, mockMetrics
}
func TestClient_Connect_Failure(t *testing.T) {
client, _, mockLogger, mockMetrics := setupDB(t)
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any())
mockLogger.EXPECT().Error(gomock.Any(), gomock.Any())
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any())
// Mock Metric behavior
mockMetrics.EXPECT().NewHistogram(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Perform the connect operation
client.Connect()
require.True(t, mockLogger.ctrl.Satisfied())
require.True(t, mockMetrics.ctrl.Satisfied())
}
func Test_Query_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Debugf("dgraph query succeeded in %dµs", gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_query_duration", gomock.Any())
resp, err := client.Query(context.Background(), "my query")
require.NoError(t, err, "Test_Query_Success Failed!")
require.NotNil(t, resp, "Test_Query_Success Failed!")
require.Equal(t, &api.Response{Json: []byte(`{"result": "success"}`)}, resp, "Test_Query_Success Failed!")
}
func Test_Query_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
client.tracer = otel.GetTracerProvider().Tracer("gofr-dgraph")
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
mockTxn.EXPECT().Query(gomock.Any(), "my query").Return(nil, errQueryFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Error("dgraph query failed: ", errQueryFailed)
resp, err := client.Query(context.Background(), "my query")
require.EqualError(t, err, errQueryFailed.Error(), "Test_Query_Error Failed!")
require.Nil(t, resp, "Test_Query_Error Failed!")
}
func Test_QueryWithVars_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
query := "my query with vars"
vars := map[string]string{"$var": "value"}
mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(&api.Response{Json: []byte(`{"result": "success"}`)}, nil)
mockLogger.EXPECT().Debugf("dgraph queryWithVars succeeded in %dµs", gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_query_with_vars_duration", gomock.Any())
// Call the QueryWithVars method
resp, err := client.QueryWithVars(context.Background(), query, vars)
require.NoError(t, err, "Test_QueryWithVars_Success Failed!")
require.NotNil(t, resp, "Test_QueryWithVars_Success Failed!")
require.Equal(t, &api.Response{Json: []byte(`{"result": "success"}`)}, resp, "Test_QueryWithVars_Success Failed!")
}
func Test_QueryWithVars_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
query := "my query with vars"
vars := map[string]string{"$var": "value"}
mockTxn.EXPECT().QueryWithVars(gomock.Any(), query, vars).Return(nil, errQueryFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Error("dgraph queryWithVars failed: ", errQueryFailed)
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
// Call the QueryWithVars method
resp, err := client.QueryWithVars(context.Background(), query, vars)
require.ErrorIs(t, err, errQueryFailed, "Test_QueryWithVars_Error Failed!")
require.Nil(t, resp, "Test_QueryWithVars_Error Failed!")
}
func Test_Mutate_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
mutation := &api.Mutation{CommitNow: true}
mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(&api.Response{Json: []byte(`{"result": "mutation success"}`)}, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Debugf("dgraph mutation succeeded in %dµs", gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_mutate_duration", gomock.Any())
// Call the Mutate method
resp, err := client.Mutate(context.Background(), mutation)
require.NoError(t, err, "Test_Mutate_Success Failed!")
require.NotNil(t, resp, "Test_Mutate_Success Failed!")
require.Equal(t, &api.Response{Json: []byte(`{"result": "mutation success"}`)}, resp, "Test_Mutate_Success Failed!")
}
func Test_Mutate_InvalidMutation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
config := Config{Host: "localhost", Port: "9080"}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
mockDgraphClient := NewMockDgraphClient(ctrl)
client.client = mockDgraphClient
// Call the Mutate method with an invalid type
resp, err := client.Mutate(context.Background(), "invalid mutation")
require.EqualError(t, err, errInvalidMutation.Error(), "Test_Mutate_InvalidMutation Failed!")
require.Nil(t, resp, "Test_Mutate_InvalidMutation Failed!")
}
func Test_Mutate_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
mutation := &api.Mutation{CommitNow: true}
mockTxn.EXPECT().Mutate(gomock.Any(), mutation).Return(nil, errMutationFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Error("dgraph mutation failed: ", errMutationFailed)
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
// Call the Mutate method
resp, err := client.Mutate(context.Background(), mutation)
require.EqualError(t, err, "mutation failed", "Test_Mutate_Error Failed!")
require.Nil(t, resp, "Test_Mutate_Error Failed!")
}
func Test_Alter_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
op := &api.Operation{}
mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any())
err := client.Alter(context.Background(), op)
require.NoError(t, err, "Test_Alter_Success Failed!")
}
func Test_Alter_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
op := &api.Operation{}
mockDgraphClient.EXPECT().Alter(gomock.Any(), op).Return(errAlterFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Error("dgraph alter failed: ", errAlterFailed)
err := client.Alter(context.Background(), op)
require.ErrorIs(t, err, errAlterFailed, "Test_Alter_Error Failed!")
}
func Test_Alter_InvalidOperation(t *testing.T) {
client, _, mockLogger, _ := setupDB(t)
op := "invalid operation"
mockLogger.EXPECT().Error("invalid operation type provided to alter")
err := client.Alter(context.Background(), op)
require.EqualError(t, err, errInvalidOperation.Error(), "Test_Alter_InvalidOperation Failed!")
}
func Test_NewTxn(t *testing.T) {
client, mockDgraphClient, _, _ := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
txn := client.NewTxn()
require.NotNil(t, txn, "Test_NewTxn Failed!")
}
func Test_NewReadOnlyTxn(t *testing.T) {
client, mockDgraphClient, _, _ := setupDB(t)
mockReadOnlyTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewReadOnlyTxn().Return(mockReadOnlyTxn)
txn := client.NewReadOnlyTxn()
require.NotNil(t, txn, "Test_NewReadOnlyTxn Failed!")
}
func Test_HealthCheck_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
mockTxn := NewMockTxn(mockDgraphClient.ctrl)
mockDgraphClient.EXPECT().NewTxn().Return(mockTxn)
mockLogger.EXPECT().Error("dgraph health check failed: ", errQueryFailed)
mockQueryResponse := &api.Response{}
mockTxn.EXPECT().Query(gomock.Any(), gomock.Any()).Return(mockQueryResponse, errQueryFailed)
_, err := client.HealthCheck(context.Background())
require.EqualError(t, err, errHealthCheckFailed.Error(), "Test_HealthCheck_Error Failed!")
}
func Test_ApplySchema_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
schema := "name: string @index(exact) ."
expectedOp := &api.Operation{Schema: schema}
mockDgraphClient.EXPECT().Alter(gomock.Any(), expectedOp).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any())
err := client.ApplySchema(context.Background(), schema)
require.NoError(t, err, "Test_ApplySchema_Success Failed!")
}
func Test_ApplySchema_EmptySchema(t *testing.T) {
config := Config{Host: "localhost", Port: "9080"}
client := New(config)
err := client.ApplySchema(context.Background(), "")
require.Equal(t, err, errEmptySchema, "Test_ApplySchema_EmptySchema Failed!")
}
func Test_AddOrUpdateField_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
fieldName := "email"
fieldType := "string"
directives := "@index(hash)"
expectedSchema := "email: string @index(hash)."
expectedOp := &api.Operation{Schema: expectedSchema}
mockDgraphClient.EXPECT().Alter(gomock.Any(), expectedOp).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any())
err := client.AddOrUpdateField(context.Background(), fieldName, fieldType, directives)
require.NoError(t, err, "Test_AddOrUpdateField_Success Failed!")
}
func Test_AddOrUpdateField_EmptyFieldName(t *testing.T) {
config := Config{Host: "localhost", Port: "9080"}
client := New(config)
err := client.AddOrUpdateField(context.Background(), "", "string", "@index(hash)")
require.Equal(t, err, errEmptyField, "Test_AddOrUpdateField_EmptyFieldName Failed!")
}
func Test_DropField_Success(t *testing.T) {
client, mockDgraphClient, mockLogger, mockMetrics := setupDB(t)
fieldName := "email"
expectedOp := &api.Operation{DropAttr: fieldName}
mockDgraphClient.EXPECT().Alter(gomock.Any(), expectedOp).Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Debugf("dgraph alter succeeded in %dµs", gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "dgraph_alter_duration", gomock.Any())
err := client.DropField(context.Background(), fieldName)
require.NoError(t, err, "Test_DropField_Success Failed!")
}
func Test_DropField_Error(t *testing.T) {
client, mockDgraphClient, mockLogger, _ := setupDB(t)
fieldName := "email"
expectedOp := &api.Operation{DropAttr: fieldName}
mockDgraphClient.EXPECT().Alter(gomock.Any(), expectedOp).Return(errAlterFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockLogger.EXPECT().Log(gomock.Any()).Times(1)
mockLogger.EXPECT().Error("dgraph alter failed: ", errAlterFailed)
err := client.DropField(context.Background(), fieldName)
require.ErrorIs(t, err, errAlterFailed, "Test_DropField_Error Failed!")
}
================================================
FILE: pkg/gofr/datasource/dgraph/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/dgraph
go 1.25.0
require (
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d
github.com/prometheus/client_golang v1.23.2
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
google.golang.org/grpc v1.79.3
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/dgraph/go.sum
================================================
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d h1:abDbP7XBVgwda+h0J5Qra5p2OQpidU2FdkXvzCKL+H8=
github.com/dgraph-io/dgo/v210 v210.0.0-20230328113526-b66f8ae53a2d/go.mod h1:wKFzULXAPj3U2BDAPWXhSbQQNC6FU1+1/5iika6IY7g=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
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=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/dgraph/interfaces.go
================================================
package dgraph
import (
"context"
"github.com/dgraph-io/dgo/v210"
"github.com/dgraph-io/dgo/v210/protos/api"
)
// Txn is an interface for Dgraph transactions.
type Txn interface {
// BestEffort sets the transaction to best-effort mode.
BestEffort() Txn
// Query executes a query against the transaction.
Query(ctx context.Context, q string) (*api.Response, error)
// QueryRDF executes an RDF query against the transaction.
QueryRDF(ctx context.Context, q string) (*api.Response, error)
// QueryWithVars executes a query with variables against the transaction.
QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error)
// QueryRDFWithVars executes an RDF query with variables against the transaction.
QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error)
// Mutate applies a mutation to the transaction.
Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error)
// Do performs a raw request against the transaction.
Do(ctx context.Context, req *api.Request) (*api.Response, error)
// Commit commits the transaction.
Commit(ctx context.Context) error
// Discard discards the transaction.
Discard(ctx context.Context) error
}
// DgraphClient is an interface that defines the methods for interacting with Dgraph.
//
//nolint:revive // dgraph.DgraphClient is repetitive. A better name could have been chosen, but it's too late as it's already exported.
type DgraphClient interface {
// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database.
NewTxn() Txn
// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database.
NewReadOnlyTxn() Txn
// Alter applies schema or other changes to the Dgraph database.
Alter(ctx context.Context, op *api.Operation) error
// Login logs in to the Dgraph database.
Login(ctx context.Context, userid string, password string) error
// LoginIntoNamespace logs in to the Dgraph database with a specific namespace.
LoginIntoNamespace(ctx context.Context, userid string, password string, namespace uint64) error
// GetJwt returns the JWT token for the Dgraph client.
GetJwt() api.Jwt
// Relogin relogs in to the Dgraph database.
Relogin(ctx context.Context) error
}
// dgraphClientImpl is a struct that implements the DgraphClient interface.
type dgraphClientImpl struct {
client *dgo.Dgraph
}
// NewDgraphClient returns a new Dgraph client.
func NewDgraphClient(client *dgo.Dgraph) DgraphClient {
return &dgraphClientImpl{client: client}
}
// NewTxn creates a new transaction (read-write) for interacting with the Dgraph database.
func (d *dgraphClientImpl) NewTxn() Txn {
return &txnImpl{d.client.NewTxn()}
}
// NewReadOnlyTxn creates a new read-only transaction for querying the Dgraph database.
func (d *dgraphClientImpl) NewReadOnlyTxn() Txn {
return &txnImpl{d.client.NewReadOnlyTxn()}
}
// Alter applies schema or other changes to the Dgraph database.
func (d *dgraphClientImpl) Alter(ctx context.Context, op *api.Operation) error {
return d.client.Alter(ctx, op)
}
// Login logs in to the Dgraph database.
func (d *dgraphClientImpl) Login(ctx context.Context, userid, password string) error {
return d.client.Login(ctx, userid, password)
}
// LoginIntoNamespace logs in to the Dgraph database with a specific namespace.
func (d *dgraphClientImpl) LoginIntoNamespace(ctx context.Context, userid, password string, namespace uint64) error {
return d.client.LoginIntoNamespace(ctx, userid, password, namespace)
}
// GetJwt returns the JWT token for the Dgraph client.
func (d *dgraphClientImpl) GetJwt() api.Jwt {
return d.client.GetJwt()
}
// Relogin relogs in to the Dgraph database.
func (d *dgraphClientImpl) Relogin(ctx context.Context) error {
return d.client.Relogin(ctx)
}
// txnImpl is the struct that implements the Txn interface by wrapping *dgo.Txn.
type txnImpl struct {
txn *dgo.Txn
}
// BestEffort sets the transaction to best-effort mode.
func (t *txnImpl) BestEffort() Txn {
t.txn.BestEffort()
return t
}
// Query executes a query against the transaction.
func (t *txnImpl) Query(ctx context.Context, q string) (*api.Response, error) {
return t.txn.Query(ctx, q)
}
// QueryRDF executes an RDF query against the transaction.
func (t *txnImpl) QueryRDF(ctx context.Context, q string) (*api.Response, error) {
return t.txn.QueryRDF(ctx, q)
}
// QueryWithVars executes a query with variables against the transaction.
func (t *txnImpl) QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) {
return t.txn.QueryWithVars(ctx, q, vars)
}
// QueryRDFWithVars executes an RDF query with variables against the transaction.
func (t *txnImpl) QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) {
return t.txn.QueryRDFWithVars(ctx, q, vars)
}
// Mutate applies a mutation to the transaction.
func (t *txnImpl) Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) {
return t.txn.Mutate(ctx, mu)
}
// Do performs a raw request against the transaction.
func (t *txnImpl) Do(ctx context.Context, req *api.Request) (*api.Response, error) {
return t.txn.Do(ctx, req)
}
// Commit commits the transaction.
func (t *txnImpl) Commit(ctx context.Context) error {
return t.txn.Commit(ctx)
}
// Discard discards the transaction.
func (t *txnImpl) Discard(ctx context.Context) error {
return t.txn.Discard(ctx)
}
================================================
FILE: pkg/gofr/datasource/dgraph/logger.go
================================================
package dgraph
import (
"fmt"
"regexp"
"strings"
)
var whitespaceRegex = regexp.MustCompile(`\s+`)
// Logger interface with required methods.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
// QueryLog represents the structure for query logging.
type QueryLog struct {
Type string `json:"type"`
URL string `json:"url"`
Duration int64 `json:"duration"` // Duration in microseconds
}
// PrettyPrint logs the QueryLog in a structured format to the given writer.
func (ql *QueryLog) PrettyPrint(logger Logger) {
// Format the log string
formattedLog := fmt.Sprintf(
"\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s",
clean(ql.Type), "DGRAPH", ql.Duration, clean(ql.URL),
)
// Log the formatted string using the logger
logger.Debug(formattedLog)
}
// clean replaces multiple consecutive whitespace characters with a single space and trims leading/trailing whitespace.
func clean(query string) string {
query = whitespaceRegex.ReplaceAllString(query, " ")
return strings.TrimSpace(query)
}
================================================
FILE: pkg/gofr/datasource/dgraph/logger_test.go
================================================
package dgraph
import (
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := QueryLog{
Type: "GET",
Duration: 12345,
}
logger := NewMockLogger(gomock.NewController(t))
logger.EXPECT().Debug(gomock.Any())
queryLog.PrettyPrint(logger)
require.True(t, logger.ctrl.Satisfied(), "Test_PrettyPrint Failed!")
}
================================================
FILE: pkg/gofr/datasource/dgraph/metrics.go
================================================
package dgraph
import (
"context"
"github.com/prometheus/client_golang/prometheus"
)
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
type PrometheusMetrics struct {
histograms map[string]*prometheus.HistogramVec
}
// NewHistogram creates a new histogram metric with the given name, description, and optional bucket sizes.
func (p *PrometheusMetrics) NewHistogram(name, desc string, buckets ...float64) {
histogram := prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: name,
Help: desc,
Buckets: buckets,
},
[]string{}, // labels can be added here if needed
)
p.histograms[name] = histogram
prometheus.MustRegister(histogram)
}
// RecordHistogram records a value to the specified histogram metric with optional labels.
func (p *PrometheusMetrics) RecordHistogram(_ context.Context, name string, value float64, labels ...string) {
histogram, exists := p.histograms[name]
if !exists {
// Handle error: histogram not found
return
}
histogram.WithLabelValues(labels...).Observe(value)
}
================================================
FILE: pkg/gofr/datasource/dgraph/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces.go
//
// Generated by this command:
//
// mockgen -source=interfaces.go -destination=mock_interfaces.go -package=dgraph
//
// Package dgraph is a generated GoMock package.
package dgraph
import (
context "context"
reflect "reflect"
api "github.com/dgraph-io/dgo/v210/protos/api"
gomock "go.uber.org/mock/gomock"
)
// MockTxn is a mock of Txn interface.
type MockTxn struct {
ctrl *gomock.Controller
recorder *MockTxnMockRecorder
}
// MockTxnMockRecorder is the mock recorder for MockTxn.
type MockTxnMockRecorder struct {
mock *MockTxn
}
// NewMockTxn creates a new mock instance.
func NewMockTxn(ctrl *gomock.Controller) *MockTxn {
mock := &MockTxn{ctrl: ctrl}
mock.recorder = &MockTxnMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTxn) EXPECT() *MockTxnMockRecorder {
return m.recorder
}
// BestEffort mocks base method.
func (m *MockTxn) BestEffort() Txn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BestEffort")
ret0, _ := ret[0].(Txn)
return ret0
}
// BestEffort indicates an expected call of BestEffort.
func (mr *MockTxnMockRecorder) BestEffort() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BestEffort", reflect.TypeOf((*MockTxn)(nil).BestEffort))
}
// Commit mocks base method.
func (m *MockTxn) Commit(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Commit", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Commit indicates an expected call of Commit.
func (mr *MockTxnMockRecorder) Commit(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTxn)(nil).Commit), ctx)
}
// Discard mocks base method.
func (m *MockTxn) Discard(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Discard", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Discard indicates an expected call of Discard.
func (mr *MockTxnMockRecorder) Discard(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Discard", reflect.TypeOf((*MockTxn)(nil).Discard), ctx)
}
// Do mocks base method.
func (m *MockTxn) Do(ctx context.Context, req *api.Request) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", ctx, req)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do.
func (mr *MockTxnMockRecorder) Do(ctx, req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockTxn)(nil).Do), ctx, req)
}
// Mutate mocks base method.
func (m *MockTxn) Mutate(ctx context.Context, mu *api.Mutation) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mutate", ctx, mu)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Mutate indicates an expected call of Mutate.
func (mr *MockTxnMockRecorder) Mutate(ctx, mu any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mutate", reflect.TypeOf((*MockTxn)(nil).Mutate), ctx, mu)
}
// Query mocks base method.
func (m *MockTxn) Query(ctx context.Context, q string) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, q)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockTxnMockRecorder) Query(ctx, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockTxn)(nil).Query), ctx, q)
}
// QueryRDF mocks base method.
func (m *MockTxn) QueryRDF(ctx context.Context, q string) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryRDF", ctx, q)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryRDF indicates an expected call of QueryRDF.
func (mr *MockTxnMockRecorder) QueryRDF(ctx, q any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRDF", reflect.TypeOf((*MockTxn)(nil).QueryRDF), ctx, q)
}
// QueryRDFWithVars mocks base method.
func (m *MockTxn) QueryRDFWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryRDFWithVars", ctx, q, vars)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryRDFWithVars indicates an expected call of QueryRDFWithVars.
func (mr *MockTxnMockRecorder) QueryRDFWithVars(ctx, q, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRDFWithVars", reflect.TypeOf((*MockTxn)(nil).QueryRDFWithVars), ctx, q, vars)
}
// QueryWithVars mocks base method.
func (m *MockTxn) QueryWithVars(ctx context.Context, q string, vars map[string]string) (*api.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryWithVars", ctx, q, vars)
ret0, _ := ret[0].(*api.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryWithVars indicates an expected call of QueryWithVars.
func (mr *MockTxnMockRecorder) QueryWithVars(ctx, q, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithVars", reflect.TypeOf((*MockTxn)(nil).QueryWithVars), ctx, q, vars)
}
// MockDgraphClient is a mock of DgraphClient interface.
type MockDgraphClient struct {
ctrl *gomock.Controller
recorder *MockDgraphClientMockRecorder
}
// MockDgraphClientMockRecorder is the mock recorder for MockDgraphClient.
type MockDgraphClientMockRecorder struct {
mock *MockDgraphClient
}
// NewMockDgraphClient creates a new mock instance.
func NewMockDgraphClient(ctrl *gomock.Controller) *MockDgraphClient {
mock := &MockDgraphClient{ctrl: ctrl}
mock.recorder = &MockDgraphClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDgraphClient) EXPECT() *MockDgraphClientMockRecorder {
return m.recorder
}
// Alter mocks base method.
func (m *MockDgraphClient) Alter(ctx context.Context, op *api.Operation) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Alter", ctx, op)
ret0, _ := ret[0].(error)
return ret0
}
// Alter indicates an expected call of Alter.
func (mr *MockDgraphClientMockRecorder) Alter(ctx, op any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Alter", reflect.TypeOf((*MockDgraphClient)(nil).Alter), ctx, op)
}
// GetJwt mocks base method.
func (m *MockDgraphClient) GetJwt() api.Jwt {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetJwt")
ret0, _ := ret[0].(api.Jwt)
return ret0
}
// GetJwt indicates an expected call of GetJwt.
func (mr *MockDgraphClientMockRecorder) GetJwt() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetJwt", reflect.TypeOf((*MockDgraphClient)(nil).GetJwt))
}
// Login mocks base method.
func (m *MockDgraphClient) Login(ctx context.Context, userid, password string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Login", ctx, userid, password)
ret0, _ := ret[0].(error)
return ret0
}
// Login indicates an expected call of Login.
func (mr *MockDgraphClientMockRecorder) Login(ctx, userid, password any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockDgraphClient)(nil).Login), ctx, userid, password)
}
// LoginIntoNamespace mocks base method.
func (m *MockDgraphClient) LoginIntoNamespace(ctx context.Context, userid, password string, namespace uint64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LoginIntoNamespace", ctx, userid, password, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// LoginIntoNamespace indicates an expected call of LoginIntoNamespace.
func (mr *MockDgraphClientMockRecorder) LoginIntoNamespace(ctx, userid, password, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginIntoNamespace", reflect.TypeOf((*MockDgraphClient)(nil).LoginIntoNamespace), ctx, userid, password, namespace)
}
// NewReadOnlyTxn mocks base method.
func (m *MockDgraphClient) NewReadOnlyTxn() Txn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewReadOnlyTxn")
ret0, _ := ret[0].(Txn)
return ret0
}
// NewReadOnlyTxn indicates an expected call of NewReadOnlyTxn.
func (mr *MockDgraphClientMockRecorder) NewReadOnlyTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReadOnlyTxn", reflect.TypeOf((*MockDgraphClient)(nil).NewReadOnlyTxn))
}
// NewTxn mocks base method.
func (m *MockDgraphClient) NewTxn() Txn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewTxn")
ret0, _ := ret[0].(Txn)
return ret0
}
// NewTxn indicates an expected call of NewTxn.
func (mr *MockDgraphClientMockRecorder) NewTxn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTxn", reflect.TypeOf((*MockDgraphClient)(nil).NewTxn))
}
// Relogin mocks base method.
func (m *MockDgraphClient) Relogin(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Relogin", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Relogin indicates an expected call of Relogin.
func (mr *MockDgraphClientMockRecorder) Relogin(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Relogin", reflect.TypeOf((*MockDgraphClient)(nil).Relogin), ctx)
}
================================================
FILE: pkg/gofr/datasource/dgraph/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=dgraph
//
// Package dgraph is a generated GoMock package.
package dgraph
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/dgraph/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=dgraph
//
// Package dgraph is a generated GoMock package.
package dgraph
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/documents.go
================================================
package elasticsearch
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/elastic/go-elasticsearch/v8/esapi"
)
// IndexDocument indexes (creates or replaces) a single document.
func (c *Client) IndexDocument(ctx context.Context, index, id string, document any) error {
if strings.TrimSpace(index) == "" {
return errEmptyIndex
}
if strings.TrimSpace(id) == "" {
return errEmptyDocumentID
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "index-document", []string{index}, id)
body, err := json.Marshal(document)
if err != nil {
return fmt.Errorf("%w: document: %w", errMarshaling, err)
}
req := esapi.IndexRequest{
Index: index,
DocumentID: id,
Body: bytes.NewReader(body),
Refresh: "true",
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return fmt.Errorf("%w: indexing document: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("%w: %s", errResponse, res.String())
}
c.sendOperationStats(start, fmt.Sprintf("INDEX DOCUMENT %s/%s", index, id), []string{id},
"", document, span)
return nil
}
// GetDocument retrieves a document by its ID.
func (c *Client) GetDocument(ctx context.Context, index, id string) (map[string]any, error) {
if strings.TrimSpace(index) == "" {
return nil, errEmptyIndex
}
if strings.TrimSpace(id) == "" {
return nil, errEmptyDocumentID
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "get-document", []string{index}, id)
req := esapi.GetRequest{
Index: index,
DocumentID: id,
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return nil, fmt.Errorf("%w: getting document: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("%w: %s", errResponse, res.String())
}
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("%w: %w", errParsingResponse, err)
}
c.sendOperationStats(start, fmt.Sprintf("GET DOCUMENT %s/%s", index, id),
[]string{index}, id, nil, span)
return result, nil
}
// UpdateDocument applies a partial update to an existing document.
func (c *Client) UpdateDocument(ctx context.Context, index, id string, update map[string]any) error {
if strings.TrimSpace(index) == "" {
return errEmptyIndex
}
if strings.TrimSpace(id) == "" {
return errEmptyDocumentID
}
if len(update) == 0 {
return errEmptyQuery
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "update-document", []string{index}, id)
body, err := json.Marshal(map[string]any{"doc": update})
if err != nil {
return fmt.Errorf("%w: update: %w", errMarshaling, err)
}
req := esapi.UpdateRequest{
Index: index,
DocumentID: id,
Body: bytes.NewReader(body),
Refresh: "true",
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return fmt.Errorf("%w: updating document: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("%w: %s", errResponse, res.String())
}
c.sendOperationStats(start, fmt.Sprintf("UPDATE DOCUMENT %s/%s", index, id),
[]string{index}, id, map[string]any{"doc": update}, span)
return nil
}
// DeleteDocument removes a document by ID.
func (c *Client) DeleteDocument(ctx context.Context, index, id string) error {
if strings.TrimSpace(index) == "" {
return errEmptyIndex
}
if strings.TrimSpace(id) == "" {
return errEmptyDocumentID
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "delete-document", []string{index}, id)
req := esapi.DeleteRequest{
Index: index,
DocumentID: id,
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return fmt.Errorf("%w: deleting document: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("%w: %s", errResponse, res.String())
}
c.sendOperationStats(start, fmt.Sprintf("DELETE DOCUMENT %s/%s", index, id),
[]string{index}, id, nil, span)
return nil
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/elasticsearch.go
================================================
package elasticsearch
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
es "github.com/elastic/go-elasticsearch/v8"
"github.com/elastic/go-elasticsearch/v8/esapi"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const (
statusDown = "DOWN"
statusUp = "UP"
defaultTimeout = 5 * time.Second
)
var (
errEmptyIndex = errors.New("index name cannot be empty")
errEmptyDocumentID = errors.New("document ID cannot be empty")
errEmptyQuery = errors.New("query cannot be empty")
errEmptyOperations = errors.New("operations cannot be empty")
errHealthCheckFailed = errors.New("elasticsearch health check failed")
errOperation = errors.New("elasticsearch operation error")
errMarshaling = errors.New("error marshaling data")
errParsingResponse = errors.New("error parsing response")
errResponse = errors.New("invalid elasticsearch response")
errEncodingOperation = errors.New("error encoding operation")
)
// Config holds the configuration for connecting to Elasticsearch.
type Config struct {
Addresses []string
Username string
Password string
}
// Client represents the Elasticsearch client.
type Client struct {
config Config
client *es.Client
logger Logger
metrics Metrics
tracer trace.Tracer
}
// UseLogger sets the logger for the Elasticsearch client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the Elasticsearch client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for Elasticsearch client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// New creates a new Elasticsearch client with the provided configuration.
func New(config Config) *Client {
return &Client{
config: config,
}
}
func (c *Client) Connect() {
cfg := es.Config{
Addresses: c.config.Addresses,
Username: c.config.Username,
Password: c.config.Password,
}
c.logger.Debugf("connecting to Elasticsearch at %v", c.config.Addresses)
client, err := es.NewClient(cfg)
if err != nil {
c.logger.Errorf("error creating Elasticsearch client: %v", err)
return
}
c.client = client
var responseTimeBuckets = []float64{0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0}
c.metrics.NewHistogram("es_request_duration_ms", "Duration of Elasticsearch requests in ms", responseTimeBuckets...)
if _, err := c.HealthCheck(context.Background()); err != nil {
c.logger.Errorf("Elasticsearch health check failed: %v", err)
return
}
c.logger.Logf("connected to Elasticsearch successfully at : %v", c.config.Addresses)
}
// CreateIndex creates an index in Elasticsearch with the specified settings.
func (c *Client) CreateIndex(ctx context.Context, index string, settings map[string]any) error {
if strings.TrimSpace(index) == "" {
return errEmptyIndex
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "create-index", []string{index}, "")
body, err := json.Marshal(settings)
if err != nil {
return fmt.Errorf("%w: settings: %w", errMarshaling, err)
}
req := esapi.IndicesCreateRequest{
Index: index,
Body: bytes.NewReader(body),
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return fmt.Errorf("%w: creating index: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("%w: %s", errResponse, res.String())
}
c.sendOperationStats(start, fmt.Sprintf("CREATE INDEX %s", index),
[]string{index}, "", settings, span)
return nil
}
func (c *Client) DeleteIndex(ctx context.Context, index string) error {
if strings.TrimSpace(index) == "" {
return errEmptyIndex
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "delete-index", []string{index}, "")
req := esapi.IndicesDeleteRequest{
Index: []string{index},
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return fmt.Errorf("%w: deleting index: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return fmt.Errorf("%w: %s", errResponse, res.String())
}
c.sendOperationStats(start, fmt.Sprintf("DELETE INDEX %s", index),
[]string{index}, "", nil, span)
return nil
}
// Search executes a query against one or more indices.
// Returns the entire response JSON as a map.
func (c *Client) Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error) {
if len(indices) == 0 {
return nil, errEmptyIndex
}
if len(query) == 0 {
return nil, errEmptyQuery
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "search", indices, "")
body, err := json.Marshal(query)
if err != nil {
return nil, fmt.Errorf("%w: query: %w", errMarshaling, err)
}
req := esapi.SearchRequest{
Index: indices,
Body: bytes.NewReader(body),
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return nil, fmt.Errorf("%w: executing search: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("%w: %s", errResponse, res.String())
}
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("%w: %w", errParsingResponse, err)
}
c.sendOperationStats(start, "SEARCH", indices, "", query, span)
return result, nil
}
// Bulk executes multiple indexing/updating/deleting operations in one request.
// Each entry in `operations` should be a JSON‑serializable object
// following the Elasticsearch bulk API format.
func (c *Client) Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error) {
if len(operations) == 0 {
return nil, errEmptyOperations
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "bulk", nil, "")
var buf bytes.Buffer
for _, op := range operations {
if err := json.NewEncoder(&buf).Encode(op); err != nil {
return nil, fmt.Errorf("%w: %w", errEncodingOperation, err)
}
}
req := esapi.BulkRequest{
Body: &buf,
Refresh: "true",
}
res, err := req.Do(tracedCtx, c.client)
if err != nil {
return nil, fmt.Errorf("%w: executing bulk: %w", errOperation, err)
}
defer res.Body.Close()
if res.IsError() {
return nil, fmt.Errorf("%w: %s", errResponse, res.String())
}
var result map[string]any
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("%w: %w", errParsingResponse, err)
}
c.sendOperationStats(start, "BULK", nil, "", operations, span)
return result, nil
}
// Health represents the health status of Elasticsearch connection.
type Health struct {
Status string `json:"status"` // "UP" or "DOWN"
Details map[string]any `json:"details,omitempty"` // extra metadata
}
// HealthCheck verifies connectivity via Ping, then enriches via Info().
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
h := Health{Details: make(map[string]any)}
h.Details["addresses"] = c.config.Addresses
h.Details["username"] = c.config.Username
// 1) Ping with a 2s timeout
pingCtx, cancel := context.WithTimeout(ctx, defaultTimeout)
defer cancel()
pingRes, err := c.client.Ping(
c.client.Ping.WithContext(pingCtx),
)
if err != nil {
h.Status = statusDown
h.Details["error"] = err.Error()
return &h, errHealthCheckFailed
}
defer pingRes.Body.Close()
if pingRes.IsError() {
h.Status = statusDown
h.Details["error"] = pingRes.String()
return &h, errHealthCheckFailed
}
// 2) Fetch cluster info for more details
infoRes, err := c.client.Info()
if err == nil {
defer infoRes.Body.Close()
var clusterInfo struct {
ClusterName string `json:"cluster_name"`
Version struct {
Number string `json:"number"`
} `json:"version"`
}
if err := json.NewDecoder(infoRes.Body).Decode(&clusterInfo); err == nil {
h.Details["cluster_name"] = clusterInfo.ClusterName
h.Details["version"] = clusterInfo.Version.Number
}
}
h.Status = statusUp
return &h, nil
}
func (c *Client) addTrace(ctx context.Context, method string, indices []string,
documentID string) (context.Context, trace.Span) {
if c.tracer == nil {
return ctx, nil
}
spanName := fmt.Sprintf("elasticsearch-%s", strings.ToLower(method))
tracedCtx, span := c.tracer.Start(ctx, spanName)
span.SetAttributes(attribute.String("db.operation", method))
if len(indices) > 0 {
span.SetAttributes(attribute.StringSlice("elasticsearch.indices", indices))
}
if documentID != "" {
span.SetAttributes(attribute.String("elasticsearch.document_id", documentID))
}
return tracedCtx, span
}
func (c *Client) sendOperationStats(start time.Time,
operation string, indices []string, documentID string, request any, span trace.Span) {
duration := time.Since(start).Microseconds()
if span != nil {
span.SetAttributes(
attribute.Int64("elasticsearch.duration", duration),
)
if request != nil {
if b, err := json.Marshal(request); err == nil {
span.SetAttributes(attribute.String("elasticsearch.query", clean(string(b))))
}
}
span.End()
}
c.metrics.RecordHistogram(context.Background(), "es_request_duration_ms", float64(duration))
// Structured log
ql := QueryLog{
Operation: operation,
Indices: indices,
DocumentID: documentID,
Request: request,
Duration: duration,
}
ql.PrettyPrint(c.logger)
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/elasticsearch_test.go
================================================
package elasticsearch
import (
"bytes"
"errors"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/elastic/go-elasticsearch/v8"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
var (
errTestFailed = errors.New("test operation failed")
)
// mockTransport implements the elasticsearch.Transport interface for testing.
type mockTransport struct {
response *http.Response
err error
}
func (t *mockTransport) RoundTrip(*http.Request) (*http.Response, error) {
return t.response, t.err
}
func (t *mockTransport) Perform(*http.Request) (*http.Response, error) {
return t.response, t.err
}
func createMockResponse(statusCode int, body string) *http.Response {
return &http.Response{
StatusCode: statusCode,
Body: io.NopCloser(bytes.NewBufferString(body)),
Header: http.Header{"Content-Type": []string{"application/json"},
"X-Elastic-Product": []string{"Elasticsearch"}},
}
}
// setupTest creates a client with mocked components for testing.
func setupTest(t *testing.T) (*Client, *mockTransport) {
t.Helper()
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockTransport := &mockTransport{}
// Create a client with a custom transport
es, err := elasticsearch.NewClient(elasticsearch.Config{
Transport: mockTransport,
DiscoverNodesOnStart: false,
EnableMetrics: false,
EnableCompatibilityMode: true,
})
require.NoError(t, err)
config := Config{
Addresses: []string{"http://localhost:9200"},
Username: "elastic",
Password: "changeme",
}
client := New(config)
client.client = es // Replace the client with our mocked version
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-elasticsearch"))
// Setup common expectations for metrics and logging
mockMetrics.EXPECT().NewHistogram("es_request_duration_ms", gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "es_request_duration_ms", gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
return client, mockTransport
}
func TestClient_CreateIndex_Success(t *testing.T) {
client, transport := setupTest(t)
transport.response = createMockResponse(200, `{"acknowledged": true}`)
defer transport.response.Body.Close()
transport.err = nil
settings := map[string]any{
"settings": map[string]any{
"number_of_shards": 1,
},
}
err := client.CreateIndex(t.Context(), "test-index", settings)
require.NoError(t, err)
}
func TestClient_CreateIndex_Errors(t *testing.T) {
resp := createMockResponse(400, `{"error": "index already exists"}`)
defer resp.Body.Close()
tests := []struct {
name string
index string
settings map[string]any
resp *http.Response
httpErr error
errMessage string
}{
{
name: "empty index name",
index: "",
settings: map[string]any{},
errMessage: "index name cannot be empty",
},
{
name: "elasticsearch operation error",
index: "test-index",
settings: map[string]any{},
httpErr: errTestFailed,
errMessage: "elasticsearch operation error",
},
{
name: "elasticsearch response error",
index: "test-index",
settings: map[string]any{},
resp: resp,
errMessage: "invalid elasticsearch response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.resp
transport.err = tt.httpErr
err := client.CreateIndex(t.Context(), tt.index, tt.settings)
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMessage)
})
}
}
func TestClient_DeleteIndex_Success(t *testing.T) {
client, transport := setupTest(t)
transport.response = createMockResponse(200, `{"acknowledged": true}`)
transport.err = nil
defer transport.response.Body.Close()
err := client.DeleteIndex(t.Context(), "test-index")
require.NoError(t, err)
}
func TestClient_DeleteIndex_Errors(t *testing.T) {
resp := createMockResponse(404, `{"error": "index not found"}`)
defer resp.Body.Close()
tests := []struct {
name string
index string
resp *http.Response
httpErr error
errMessage string
}{
{
name: "empty index name",
index: "",
errMessage: "index name cannot be empty",
},
{
name: "elasticsearch operation error",
index: "test-index",
httpErr: errTestFailed,
errMessage: "elasticsearch operation error",
},
{
name: "elasticsearch response error",
index: "test-index",
resp: resp,
errMessage: "invalid elasticsearch response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.resp
transport.err = tt.httpErr
err := client.DeleteIndex(t.Context(), tt.index)
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMessage)
})
}
}
func TestClient_IndexDocument_Success(t *testing.T) {
client, transport := setupTest(t)
transport.response = createMockResponse(201, `{"result":"created"}`)
transport.err = nil
defer transport.response.Body.Close()
document := map[string]any{
"title": "Test Document",
}
err := client.IndexDocument(t.Context(), "test-index", "123", document)
require.NoError(t, err)
}
func TestClient_IndexDocument_Errors(t *testing.T) {
resp := createMockResponse(400, `{"error":"bad request"}`)
defer resp.Body.Close()
tests := []struct {
name string
index string
id string
document any
resp *http.Response
httpErr error
errMessage string
}{
{
name: "empty index",
index: "",
id: "123",
document: map[string]any{},
errMessage: "index name cannot be empty",
},
{
name: "empty document ID",
index: "test-index",
id: "",
document: map[string]any{},
errMessage: "document ID cannot be empty",
},
{
name: "json `marshaling` error",
index: "test-index",
id: "123",
document: make(chan int),
errMessage: "error marshaling data",
},
{
name: "elasticsearch operation error",
index: "test-index",
id: "123",
document: map[string]any{},
httpErr: errTestFailed,
errMessage: "elasticsearch operation error",
},
{
name: "elasticsearch response error",
index: "test-index",
id: "123",
document: map[string]any{},
resp: resp,
errMessage: "invalid elasticsearch response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.resp
transport.err = tt.httpErr
err := client.IndexDocument(t.Context(), tt.index, tt.id, tt.document)
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMessage)
})
}
}
func TestClient_GetDocument_Success(t *testing.T) {
client, transport := setupTest(t)
responseBody := `{"_id":"123", "_source": {"title": "Test Document"}}`
transport.response = createMockResponse(200, responseBody)
transport.err = nil
defer transport.response.Body.Close()
result, err := client.GetDocument(t.Context(), "test-index", "123")
require.NoError(t, err)
require.Equal(t, "123", result["_id"])
require.Equal(t, map[string]any{"title": "Test Document"}, result["_source"])
}
func TestClient_GetDocument_Errors(t *testing.T) {
invalidJSONResp := createMockResponse(200, `{"_source":`)
defer invalidJSONResp.Body.Close()
notFoundResp := createMockResponse(404, `{"error":"not found"}`)
defer notFoundResp.Body.Close()
tests := []struct {
name string
index string
id string
resp *http.Response
httpErr error
errMessage string
}{
{
name: "empty index",
index: "",
id: "123",
errMessage: "index name cannot be empty",
},
{
name: "empty document ID",
index: "test-index",
id: "",
errMessage: "document ID cannot be empty",
},
{
name: "elasticsearch operation error",
index: "test-index",
id: "123",
httpErr: errTestFailed,
errMessage: "elasticsearch operation error",
},
{
name: "elasticsearch response error",
index: "test-index",
id: "123",
resp: createMockResponse(404, `{"error":"not found"}`),
errMessage: "invalid elasticsearch response",
},
{
name: "json decoding error",
index: "test-index",
id: "123",
resp: invalidJSONResp,
errMessage: "error parsing response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.resp
transport.err = tt.httpErr
_, err := client.GetDocument(t.Context(), tt.index, tt.id)
require.Error(t, err)
require.Contains(t, err.Error(), tt.errMessage)
})
}
}
func TestClient_UpdateDocument_Success(t *testing.T) {
client, transport := setupTest(t)
transport.response = createMockResponse(200, `{"result":"updated"}`)
transport.err = nil
defer transport.response.Body.Close()
err := client.UpdateDocument(t.Context(), "test-index", "123", map[string]any{
"name": "updated name",
})
require.NoError(t, err)
}
func TestClient_UpdateDocument_Errors(t *testing.T) {
errResp := createMockResponse(500, `{"error":"internal error"}`)
defer errResp.Body.Close()
tests := []struct {
name string
index string
id string
update map[string]any
response *http.Response
err error
expectedMsg string
}{
{
name: "empty index",
index: "",
id: "123",
update: map[string]any{"field": "value"},
expectedMsg: "index name cannot be empty",
},
{
name: "empty document ID",
index: "test-index",
id: "",
update: map[string]any{"field": "value"},
expectedMsg: "document ID cannot be empty",
},
{
name: "empty update map",
index: "test-index",
id: "123",
update: map[string]any{},
expectedMsg: "query cannot be empty",
},
{
name: "json marshal error",
index: "test-index",
id: "123",
update: map[string]any{"field": make(chan int)},
expectedMsg: "error marshaling data: update: json: unsupported type: chan int",
},
{
name: "elasticsearch transport error",
index: "test-index",
id: "123",
update: map[string]any{"field": "value"},
err: errOperation,
expectedMsg: "elasticsearch operation error: updating document",
},
{
name: "elasticsearch error response",
index: "test-index",
id: "123",
update: map[string]any{"field": "value"},
response: errResp,
expectedMsg: "invalid elasticsearch response: [500 Internal Server Error]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.response
transport.err = tt.err
err := client.UpdateDocument(t.Context(), tt.index, tt.id, tt.update)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
})
}
}
func TestClient_DeleteDocument_Success(t *testing.T) {
client, transport := setupTest(t)
transport.response = createMockResponse(200, `{"result":"deleted"}`)
transport.err = nil
defer transport.response.Body.Close()
err := client.DeleteDocument(t.Context(), "test-index", "123")
require.NoError(t, err)
}
func TestClient_DeleteDocument_Errors(t *testing.T) {
notFoundResp := createMockResponse(500, `{"error":"not found"}`)
defer notFoundResp.Body.Close()
tests := []struct {
name string
index string
id string
response *http.Response
err error
expectedMsg string
}{
{
name: "empty index",
index: "",
id: "123",
expectedMsg: "index name cannot be empty",
},
{
name: "empty document ID",
index: "test-index",
id: "",
expectedMsg: "document ID cannot be empty",
},
{
name: "elasticsearch transport error",
index: "test-index",
id: "123",
err: errOperation,
expectedMsg: "elasticsearch operation error: deleting document",
},
{
name: "elasticsearch error response",
index: "test-index",
id: "123",
response: createMockResponse(500, `{"error":"not found"}`),
expectedMsg: "invalid elasticsearch response: [500 Internal Server Error]",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.response
transport.err = tt.err
err := client.DeleteDocument(t.Context(), tt.index, tt.id)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
})
}
}
func TestClient_Search_Success(t *testing.T) {
client, transport := setupTest(t)
responseBody := `{
"took": 15,
"hits": {
"total": {"value": 1},
"hits": [{"_id": "1", "_source": {"title": "Test"}}]
}
}`
transport.response = createMockResponse(200, responseBody)
transport.err = nil
defer transport.response.Body.Close()
query := map[string]any{
"query": map[string]any{
"match_all": map[string]any{},
},
}
result, err := client.Search(t.Context(), []string{"test-index"}, query)
require.NoError(t, err)
require.InDelta(t, 1.0, result["hits"].(map[string]any)["total"].(map[string]any)["value"], 0.0001)
}
func TestClient_Search_Errors(t *testing.T) {
internalServerErrorResp := createMockResponse(500, `{"error": "internal server error"}`)
defer internalServerErrorResp.Body.Close()
invalidJSONResp := createMockResponse(200, `{"invalid": json`)
defer invalidJSONResp.Body.Close()
tests := []struct {
name string
indices []string
query map[string]any
response *http.Response
err error
expectedMsg string
}{
{
name: "empty indices",
indices: []string{},
query: map[string]any{"query": map[string]any{}},
expectedMsg: "index name cannot be empty",
},
{
name: "nil query",
indices: []string{"test-index"},
query: nil,
expectedMsg: "query cannot be empty",
},
{
name: "elasticsearch transport error",
indices: []string{"test-index"},
query: map[string]any{"query": map[string]any{}},
err: errOperation,
expectedMsg: "elasticsearch operation error: executing search",
},
{
name: "elasticsearch error response",
indices: []string{"test-index"},
query: map[string]any{"query": map[string]any{}},
response: internalServerErrorResp,
expectedMsg: "invalid elasticsearch response",
},
{
name: "invalid json response",
indices: []string{"test-index"},
query: map[string]any{"query": map[string]any{}},
response: invalidJSONResp,
expectedMsg: "error parsing response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.response
transport.err = tt.err
_, err := client.Search(t.Context(), tt.indices, tt.query)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
})
}
}
func TestClient_Bulk_Success(t *testing.T) {
client, transport := setupTest(t)
responseBody := `{
"took": 30,
"errors": false,
"items": [
{"index": {"status": 201}},
{"index": {"status": 201}}
]
}`
transport.response = createMockResponse(200, responseBody)
transport.err = nil
defer transport.response.Body.Close()
operations := []map[string]any{
{"index": map[string]any{"_index": "test-index", "_id": "1"}},
{"title": "Document 1"},
{"index": map[string]any{"_index": "test-index", "_id": "2"}},
{"title": "Document 2"},
}
result, err := client.Bulk(t.Context(), operations)
require.NoError(t, err)
require.False(t, result["errors"].(bool))
require.Len(t, result["items"].([]any), 2)
}
func TestClient_Bulk_Errors(t *testing.T) {
invalidJSONResp := createMockResponse(200, `{"invalid": json`)
defer invalidJSONResp.Body.Close()
badRequestResp := createMockResponse(400, `{"error": "bad request"}`)
defer badRequestResp.Body.Close()
tests := []struct {
name string
operations []map[string]any
response *http.Response
err error
expectedMsg string
}{
{
name: "empty operations",
operations: []map[string]any{},
expectedMsg: "operations cannot be empty",
},
{
name: "json marshal error",
operations: []map[string]any{{"invalid": make(chan int)}},
expectedMsg: "error encoding operation",
},
{
name: "elasticsearch transport error",
operations: []map[string]any{{"index": map[string]any{"_index": "test-index"}}},
err: errOperation,
expectedMsg: "elasticsearch operation error: executing bulk",
},
{
name: "elasticsearch error response",
operations: []map[string]any{{"index": map[string]any{"_index": "test-index"}}},
response: badRequestResp,
expectedMsg: "invalid elasticsearch response",
},
{
name: "invalid json response",
operations: []map[string]any{{"index": map[string]any{"_index": "test-index"}}},
response: invalidJSONResp,
expectedMsg: "error parsing response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, transport := setupTest(t)
transport.response = tt.response
transport.err = tt.err
_, err := client.Bulk(t.Context(), tt.operations)
require.Error(t, err)
require.Contains(t, err.Error(), tt.expectedMsg)
})
}
}
func TestClient_Connect_Success(t *testing.T) {
ctrl := gomock.NewController(t)
client, _ := setupTest(t)
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
mux := http.NewServeMux()
// Handle ping endpoint
mux.HandleFunc("/_ping", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Elastic-Product", "Elasticsearch")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{}`))
if err != nil {
t.Error("failed to write response: ", err)
}
})
// Handle info endpoint
mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("X-Elastic-Product", "Elasticsearch")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{
"name": "test-node",
"cluster_name": "test-cluster",
"version": {
"number": "8.0.0"
}
}`))
if err != nil {
t.Error("failed to write response: ", err)
}
})
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf("Elasticsearch health check failed: %v", gomock.Any())
server := httptest.NewServer(mux)
defer server.Close()
client.Connect()
require.NotNil(t, client.client, "Elasticsearch client should be initialized")
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/elasticsearch
go 1.25.0
require (
github.com/elastic/go-elasticsearch/v8 v8.19.3
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/elastic/elastic-transport-go/v8 v8.8.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/elasticsearch/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/elastic/elastic-transport-go/v8 v8.8.0 h1:7k1Ua+qluFr6p1jfJjGDl97ssJS/P7cHNInzfxgBQAo=
github.com/elastic/elastic-transport-go/v8 v8.8.0/go.mod h1:YLHer5cj0csTzNFXoNQ8qhtGY1GTvSqPnKWKaqQE3Hk=
github.com/elastic/go-elasticsearch/v8 v8.19.3 h1:5LDg0hfGJXBa9Y+2QlUgRTsNJ/7rm7oNidydtFAq0LI=
github.com/elastic/go-elasticsearch/v8 v8.19.3/go.mod h1:tHJQdInFa6abmDbDCEH2LJja07l/SIpaGpJcm13nt7s=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/elasticsearch/logger.go
================================================
package elasticsearch
import (
"encoding/json"
"fmt"
"regexp"
"strings"
)
var whitespaceRegex = regexp.MustCompile(`\s+`)
// Logger interface with required methods for Elasticsearch logging.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
// QueryLog holds information about an Elasticsearch operation for structured logging.
type QueryLog struct {
Operation string `json:"operation"` // e.g., "search", "index-document"
Indices []string `json:"indices,omitempty"` // target indices
DocumentID string `json:"document_id,omitempty"` // document ID for doc operations
Target string `json:"target,omitempty"` // custom context (e.g., index/alias)
Request any `json:"request,omitempty"` // raw query or body payload
Duration int64 `json:"duration"` // duration in microseconds
}
// PrettyPrint formats the QueryLog and emits a colored, structured log line.
func (ql *QueryLog) PrettyPrint(logger Logger) {
var payload string
if ql.Request != nil {
if data, err := json.Marshal(ql.Request); err == nil {
payload = string(data)
}
}
op := clean(ql.Operation)
pl := clean(payload)
var ctxParts []string
if len(ql.Indices) > 0 {
ctxParts = append(ctxParts, strings.Join(ql.Indices, ","))
}
if ql.DocumentID != "" {
ctxParts = append(ctxParts, ql.DocumentID)
}
if ql.Target != "" {
ctxParts = append(ctxParts, ql.Target)
}
contextStr := clean(strings.Join(ctxParts, " "))
formatted := fmt.Sprintf(
"\u001B[38;5;8m%-32s \u001B[38;5;208m%-7s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-20s %s",
op, "ELASTIC", ql.Duration, contextStr, pl,
)
logger.Debug(formatted)
}
// clean replaces consecutive whitespace with a single space and trims.
func clean(s string) string {
s = whitespaceRegex.ReplaceAllString(s, " ")
return strings.TrimSpace(s)
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/metrics.go
================================================
package elasticsearch
import "context"
// Metrics defines the interface for capturing metrics.
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=elasticsearch
//
// Package elasticsearch is a generated GoMock package.
package elasticsearch
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/elasticsearch/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=elasticsearch
//
// Package elasticsearch is a generated GoMock package.
package elasticsearch
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/errors.go
================================================
package datasource
import (
"net/http"
"github.com/pkg/errors"
)
// ErrorDB represents an error specific to database operations.
type ErrorDB struct {
Err error
Message string
}
func (e ErrorDB) Error() string {
switch {
case e.Message == "":
return e.Err.Error()
case e.Err == nil:
return e.Message
default:
return errors.Wrap(e.Err, e.Message).Error()
}
}
// WithStack adds a stack trace to the Error.
func (e ErrorDB) WithStack() ErrorDB {
e.Err = errors.WithStack(e.Err)
return e
}
func (ErrorDB) StatusCode() int {
return http.StatusInternalServerError
}
// ErrorRecordNotFound represents the scenario where no records are found in the DB for the given ID.
type ErrorRecordNotFound ErrorDB
// StatusCode implementation on the ErrorRecordNotFound is an aberration
// since the errors in datasource package should not have anything to do with HTTP status codes.
func (ErrorRecordNotFound) StatusCode() int {
return http.StatusNotFound
}
================================================
FILE: pkg/gofr/datasource/errors_test.go
================================================
package datasource
import (
"net/http"
"os"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_ErrorDB(t *testing.T) {
wrappedErr := errors.New("underlying error")
tests := []struct {
desc string
err ErrorDB
expectedMsg string
}{
{"wrapped error", ErrorDB{Err: wrappedErr, Message: "custom message"}.WithStack(), "custom message: underlying error"},
{"without wrapped error", ErrorDB{Message: "custom message"}, "custom message"},
{"no custom error message", ErrorDB{Err: wrappedErr}, "underlying error"},
}
for i, tc := range tests {
require.ErrorContains(t, tc.err, tc.expectedMsg, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestErrorDB_StatusCode(t *testing.T) {
dbErr := ErrorDB{Message: "custom message"}
expectedCode := http.StatusInternalServerError
assert.Equal(t, expectedCode, dbErr.StatusCode(), "TEST Failed.\n")
}
func TestErrorRecordNotFound_StatusCode(t *testing.T) {
dbErr := ErrorRecordNotFound{Message: "custom message"}
assert.Equal(t, http.StatusNotFound, dbErr.StatusCode(), "TEST Failed.\n")
}
================================================
FILE: pkg/gofr/datasource/file/azure/fs.go
================================================
package azure
import (
"context"
"errors"
"time"
"gofr.dev/pkg/gofr/datasource/file"
)
var (
errInvalidConfig = errors.New("invalid Azure configuration: share name is required")
errAccountNameRequired = errors.New("invalid Azure configuration: account name is required")
errAccountKeyRequired = errors.New("invalid Azure configuration: account key is required")
)
const defaultTimeout = 10 * time.Second
type azureFileSystem struct {
*file.CommonFileSystem
}
// Config represents the Azure File Storage configuration.
type Config struct {
AccountName string // Azure Storage Account name
AccountKey string // Azure Storage Account key
ShareName string // Azure File Share name
Endpoint string // Azure Storage endpoint (optional, defaults to core.windows.net)
}
// New creates and validates a new Azure File Storage file system.
// Returns error if configuration is invalid.
// Connection will be established when Connect() is called.
func New(config *Config) (file.FileSystemProvider, error) {
if config == nil {
return nil, errInvalidConfig
}
if config.ShareName == "" {
return nil, errInvalidConfig
}
if config.AccountName == "" {
return nil, errAccountNameRequired
}
if config.AccountKey == "" {
return nil, errAccountKeyRequired
}
adapter := &storageAdapter{cfg: config}
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: config.ShareName,
ProviderName: "Azure", // Set provider name for observability
},
}
return fs, nil
}
// Connect tries a single immediate connect via provider; on failure it starts a background retry.
func (f *azureFileSystem) Connect() {
if f.CommonFileSystem.IsConnected() {
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf("Attempting to connect to Azure File Share %s (timeout: %v)",
f.CommonFileSystem.Location, defaultTimeout)
}
// Use CommonFileSystem.Connect for bookkeeping
if err := f.CommonFileSystem.Connect(ctx); err != nil {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Warnf("Azure File Share %s not available, starting background retry: %v", f.CommonFileSystem.Location, err)
}
go f.startRetryConnect()
return
}
// Connected successfully
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf("Successfully connected to Azure File Share %s", f.CommonFileSystem.Location)
}
}
// startRetryConnect repeatedly calls provider.Connect until success.
func (f *azureFileSystem) startRetryConnect() {
f.logRetryStart()
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
retryCount := 0
for range ticker.C {
if f.shouldExitRetry() {
f.logRetryExit()
return
}
retryCount++
f.logRetryAttempt(retryCount)
if f.attemptConnection(retryCount) {
return
}
}
}
// logRetryStart logs the start of background retry.
func (f *azureFileSystem) logRetryStart() {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
f.CommonFileSystem.Location,
)
}
}
// shouldExitRetry checks if retry loop should exit.
func (f *azureFileSystem) shouldExitRetry() bool {
return f.CommonFileSystem.IsConnected() || f.CommonFileSystem.IsRetryDisabled()
}
// logRetryExit logs the exit reason from retry loop.
func (f *azureFileSystem) logRetryExit() {
if f.CommonFileSystem.Logger == nil {
return
}
if f.CommonFileSystem.IsConnected() {
f.CommonFileSystem.Logger.Debugf(
"Retry loop exiting: Azure File Share %s is now connected",
f.CommonFileSystem.Location,
)
} else {
f.CommonFileSystem.Logger.Debugf(
"Retry loop exiting: retry disabled for Azure File Share %s",
f.CommonFileSystem.Location,
)
}
}
// logRetryAttempt logs a retry attempt.
func (f *azureFileSystem) logRetryAttempt(retryCount int) {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf(
"Retry attempt #%d: attempting to connect to Azure File Share %s (timeout: %v)",
retryCount,
f.CommonFileSystem.Location,
defaultTimeout,
)
}
}
// attemptConnection attempts to connect and returns true if successful.
func (f *azureFileSystem) attemptConnection(retryCount int) bool {
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
err := f.CommonFileSystem.Connect(ctx)
if err == nil {
f.logRetrySuccess(retryCount)
return true
}
f.logRetryFailure(retryCount, err)
return false
}
// logRetrySuccess logs successful retry.
func (f *azureFileSystem) logRetrySuccess(_ int) {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Infof("Azure connection restored to share %s", f.CommonFileSystem.Location)
}
}
// logRetryFailure logs failed retry attempt.
func (f *azureFileSystem) logRetryFailure(retryCount int, err error) {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf(
"Retry attempt #%d failed for Azure File Share %s: %v (will retry in 1 minute)",
retryCount,
f.CommonFileSystem.Location,
err,
)
}
}
================================================
FILE: pkg/gofr/datasource/file/azure/fs_test.go
================================================
package azure
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/file"
)
var errConnectionFailed = errors.New("connection failed")
func TestNew_NilConfig(t *testing.T) {
fs, err := New(nil)
require.Error(t, err)
assert.Nil(t, fs)
assert.ErrorIs(t, err, errInvalidConfig)
}
func TestNew_EmptyShareName(t *testing.T) {
config := &Config{ShareName: ""}
fs, err := New(config)
require.Error(t, err)
assert.Nil(t, fs)
assert.ErrorIs(t, err, errInvalidConfig)
}
func TestNew_EmptyAccountName(t *testing.T) {
config := &Config{
ShareName: "testshare",
AccountName: "",
AccountKey: "testkey",
}
fs, err := New(config)
require.Error(t, err)
assert.Nil(t, fs)
assert.ErrorIs(t, err, errAccountNameRequired)
}
func TestNew_EmptyAccountKey(t *testing.T) {
config := &Config{
ShareName: "testshare",
AccountName: "testaccount",
AccountKey: "",
}
fs, err := New(config)
require.Error(t, err)
assert.Nil(t, fs)
assert.ErrorIs(t, err, errAccountKeyRequired)
}
func TestNew_ConnectionFailure_StartsRetry(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
AccountName: "non-existent-account",
AccountKey: "invalid-key",
ShareName: "non-existent-share",
}
fs, err := New(config)
require.NoError(t, err)
require.NotNil(t, fs)
// Set logger and metrics via UseLogger/UseMetrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
// Expect debug log for connection attempt
mockLogger.EXPECT().Debugf(
gomock.Any(), // Format string
gomock.Any(), // Share name
gomock.Any(), // Timeout duration
)
// Expect warning about background retry
mockLogger.EXPECT().Warnf(
"Azure File Share %s not available, starting background retry: %v",
"non-existent-share",
gomock.Any(), // Error message varies
)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// Expect debug log for starting background retry (from startRetryConnect goroutine)
// This may be called asynchronously, so use MaxTimes to allow flexibility
mockLogger.EXPECT().Debugf(
gomock.Any(), // Format string
gomock.Any(), // Share name
).MaxTimes(1)
// Now call Connect which will attempt connection and start retry
fs.Connect()
time.Sleep(100 * time.Millisecond)
fs.(*azureFileSystem).CommonFileSystem.SetDisableRetry(true)
}
func TestAzureFileSystem_Connect_AlreadyConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
fs, err := New(config)
require.NoError(t, err)
// Set logger and metrics via UseLogger/UseMetrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
// Replace provider with mock for successful connection
fs.(*azureFileSystem).CommonFileSystem.Provider = mockProvider
mockLogger.EXPECT().Debugf("Attempting to connect to Azure File Share %s (timeout: %v)", "testshare", gomock.Any())
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil)
mockLogger.EXPECT().Infof("connected to %s", "testshare")
mockLogger.EXPECT().Debugf("Successfully connected to Azure File Share %s", "testshare")
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// First Connect() call - will attempt connection and succeed
fs.(*azureFileSystem).Connect()
// If already connected, Connect() should return immediately (no more calls expected)
fs.(*azureFileSystem).Connect()
fs.(*azureFileSystem).CommonFileSystem.SetDisableRetry(true)
}
func TestAzureFileSystem_Connect_NotConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
fs, err := New(config)
require.NoError(t, err)
// Set logger and metrics via UseLogger/UseMetrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockLogger.EXPECT().Debugf("Attempting to connect to Azure File Share %s (timeout: %v)", "testshare", gomock.Any())
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Warnf("Azure File Share %s not available, starting background retry: %v", "testshare", gomock.Any())
// Expect logRetryStart from the goroutine (may or may not be called depending on timing)
mockLogger.EXPECT().Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
"testshare",
).MaxTimes(1)
// Call Connect - should attempt to connect and start retry
fs.(*azureFileSystem).Connect()
// Give goroutine time to start
time.Sleep(50 * time.Millisecond)
// Mark as not connected and disable retry to stop the goroutine
fs.(*azureFileSystem).CommonFileSystem.SetDisableRetry(true)
// Give goroutine time to check the flag and exit
time.Sleep(50 * time.Millisecond)
}
func TestAzureFileSystem_startRetryConnect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
fs, err := New(config)
require.NoError(t, err)
// Set logger and metrics via UseLogger/UseMetrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Warnf(gomock.Any(), gomock.Any(), gomock.Any()).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// Call Connect which will start retry on failure
fs.Connect()
// Disable retry to stop the goroutine quickly
fs.(*azureFileSystem).CommonFileSystem.SetDisableRetry(true)
// Give it a moment to start
time.Sleep(50 * time.Millisecond)
}
func TestAzureFileSystem_startRetryConnect_RetryDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
fs, err := New(config)
require.NoError(t, err)
// Set logger and metrics via UseLogger/UseMetrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Warnf(gomock.Any(), gomock.Any(), gomock.Any()).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// Disable retry immediately - retry loop should exit
fs.(*azureFileSystem).CommonFileSystem.SetDisableRetry(true)
// Call Connect which would start retry, but retry is disabled
fs.Connect()
// Give it a moment to check the retry disabled flag
time.Sleep(50 * time.Millisecond)
}
// TestAzureFileSystem_Observe_ProviderName tests that Observe uses "Azure" as provider name.
func TestAzureFileSystem_Observe_ProviderName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
// Create azureFileSystem directly without calling New() to avoid initialization logs
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: nil, // Not needed for this test
Location: "testshare",
Logger: mockLogger,
Metrics: mockMetrics,
ProviderName: "Azure", // Set provider name for observability
},
}
// Expect RecordHistogram to be called with "Azure" as provider label
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(), // context
file.AppFileStats,
gomock.Any(), // duration
"type", gomock.Any(), // operation type
"status", gomock.Any(), // status
"provider", "Azure", // provider name should be "Azure"
)
// Expect Debug to be called with OperationLog containing "Azure" as provider
mockLogger.EXPECT().Debug(gomock.Any()).Do(func(log any) {
opLog, ok := log.(*file.OperationLog)
require.True(t, ok, "log should be OperationLog")
assert.Equal(t, "Azure", opLog.Provider, "provider should be Azure")
})
// Call Observe directly to test provider name
operation := file.OpConnect
startTime := time.Now()
status := "SUCCESS"
message := "test message"
fs.Observe(operation, startTime, &status, &message)
}
func TestNew_SuccessfulConnection(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
// Create fs manually with mock provider to test successful connection path
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Location: config.ShareName,
Logger: mockLogger,
Metrics: mockMetrics,
ProviderName: "Azure",
},
}
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil)
mockLogger.EXPECT().Infof("connected to %s", "testshare")
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
err := fs.CommonFileSystem.Connect(ctx)
require.NoError(t, err)
assert.Equal(t, "Azure", fs.CommonFileSystem.ProviderName)
assert.True(t, fs.CommonFileSystem.IsConnected())
}
func TestNew_NilLogger(t *testing.T) {
config := &Config{
AccountName: "testaccount",
AccountKey: "testkey",
ShareName: "testshare",
}
fs, err := New(config)
require.NoError(t, err)
require.NotNil(t, fs)
}
func TestAzureFileSystem_Connect_NotConnected_WithLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: nil,
Location: "testshare",
Logger: mockLogger,
Metrics: mockMetrics,
ProviderName: "Azure",
},
}
mockLogger.EXPECT().Debugf("Attempting to connect to Azure File Share %s (timeout: %v)", "testshare", gomock.Any())
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Warnf("Azure File Share %s not available, starting background retry: %v", "testshare", gomock.Any())
// Expect logRetryStart from the goroutine
mockLogger.EXPECT().Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
"testshare",
)
fs.Connect()
// Give goroutine time to start
time.Sleep(100 * time.Millisecond)
// Disable retry to stop the goroutine
fs.CommonFileSystem.SetDisableRetry(true)
// Give goroutine time to check the flag and exit
time.Sleep(100 * time.Millisecond)
}
func TestAzureFileSystem_logRetryStart_WithLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Logger: mockLogger,
Location: "testshare",
ProviderName: "Azure",
},
}
mockLogger.EXPECT().Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
"testshare",
)
fs.logRetryStart()
}
func TestAzureFileSystem_shouldExitRetry_Connected(t *testing.T) {
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
ProviderName: "Azure",
},
}
fs.CommonFileSystem.SetDisableRetry(false)
// Simulate connected state by setting it directly
// Note: This is testing the logic, actual connection state is managed internally
result := fs.shouldExitRetry()
assert.False(t, result)
}
func TestAzureFileSystem_shouldExitRetry_Disabled(t *testing.T) {
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
ProviderName: "Azure",
},
}
fs.CommonFileSystem.SetDisableRetry(true)
result := fs.shouldExitRetry()
assert.True(t, result)
}
func TestAzureFileSystem_logRetryExit_Connected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
// Connect to set the connected state
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil)
mockLogger.EXPECT().Infof("connected to %s", "testshare")
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
err := fs.CommonFileSystem.Connect(ctx)
require.NoError(t, err)
assert.True(t, fs.CommonFileSystem.IsConnected())
mockLogger.EXPECT().Debugf(
"Retry loop exiting: Azure File Share %s is now connected",
"testshare",
)
fs.logRetryExit()
}
func TestAzureFileSystem_logRetryExit_Disabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Logger: mockLogger,
Location: "testshare",
ProviderName: "Azure",
},
}
fs.CommonFileSystem.SetDisableRetry(true)
mockLogger.EXPECT().Debugf(
"Retry loop exiting: retry disabled for Azure File Share %s",
"testshare",
)
fs.logRetryExit()
}
func TestAzureFileSystem_logRetryAttempt_WithLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Logger: mockLogger,
Location: "testshare",
ProviderName: "Azure",
},
}
mockLogger.EXPECT().Debugf(
"Retry attempt #%d: attempting to connect to Azure File Share %s (timeout: %v)",
1,
"testshare",
defaultTimeout,
)
fs.logRetryAttempt(1)
}
func TestAzureFileSystem_logRetrySuccess_WithLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Logger: mockLogger,
Location: "testshare",
ProviderName: "Azure",
},
}
mockLogger.EXPECT().Infof("Azure connection restored to share %s", "testshare")
fs.logRetrySuccess(1)
}
func TestAzureFileSystem_logRetryFailure_WithLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Logger: mockLogger,
Location: "testshare",
ProviderName: "Azure",
},
}
mockLogger.EXPECT().Debugf(
"Retry attempt #%d failed for Azure File Share %s: %v (will retry in 1 minute)",
2,
"testshare",
errConnectionFailed,
)
fs.logRetryFailure(2, errConnectionFailed)
}
func TestAzureFileSystem_attemptConnection_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil)
mockLogger.EXPECT().Infof("connected to %s", "testshare")
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Infof("Azure connection restored to share %s", "testshare")
result := fs.attemptConnection(1)
assert.True(t, result)
}
func TestAzureFileSystem_attemptConnection_Failure(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(errConnectionFailed)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debugf(
"Retry attempt #%d failed for Azure File Share %s: %v (will retry in 1 minute)",
1,
"testshare",
errConnectionFailed,
)
result := fs.attemptConnection(1)
assert.False(t, result)
}
func TestAzureFileSystem_Connect_WhenNotConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
// Ensure not connected
assert.False(t, fs.CommonFileSystem.IsConnected())
mockLogger.EXPECT().Debugf("Attempting to connect to Azure File Share %s (timeout: %v)", "testshare", gomock.Any())
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil)
mockLogger.EXPECT().Infof("connected to %s", "testshare")
mockLogger.EXPECT().Debugf("Successfully connected to Azure File Share %s", "testshare")
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
fs.Connect()
assert.True(t, fs.CommonFileSystem.IsConnected())
}
func TestAzureFileSystem_startRetryConnect_ExitsOnConnection(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
// Setup expectations for retry loop
mockLogger.EXPECT().Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
"testshare",
)
mockLogger.EXPECT().Debugf(
"Retry attempt #%d: attempting to connect to Azure File Share %s (timeout: %v)",
1,
"testshare",
defaultTimeout,
).AnyTimes()
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
mockProvider.EXPECT().Connect(gomock.Any()).Return(nil).AnyTimes()
mockLogger.EXPECT().Infof("connected to %s", "testshare").AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof("Azure connection restored to share %s", "testshare").AnyTimes()
mockLogger.EXPECT().Debugf(
"Retry loop exiting: Azure File Share %s is now connected",
"testshare",
).AnyTimes()
// Start retry in background
go fs.startRetryConnect()
// Wait a bit for retry to start
time.Sleep(100 * time.Millisecond)
// Disable retry to stop the goroutine
fs.CommonFileSystem.SetDisableRetry(true)
// Give it time to exit
time.Sleep(50 * time.Millisecond)
}
func TestAzureFileSystem_startRetryConnect_ExitsOnDisable(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
mockProvider := file.NewMockStorageProvider(ctrl)
fs := &azureFileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: mockProvider,
Logger: mockLogger,
Metrics: mockMetrics,
Location: "testshare",
ProviderName: "Azure",
},
}
// Setup expectations
mockLogger.EXPECT().Debugf(
"Starting background retry for Azure File Share %s (retry interval: 1 minute)",
"testshare",
)
mockLogger.EXPECT().Debugf(
"Retry loop exiting: retry disabled for Azure File Share %s",
"testshare",
).MaxTimes(1)
// Start retry in background
go fs.startRetryConnect()
// Wait a bit for retry to start
time.Sleep(50 * time.Millisecond)
// Disable retry - should cause exit
fs.CommonFileSystem.SetDisableRetry(true)
// Give it time to check and exit
time.Sleep(100 * time.Millisecond)
}
================================================
FILE: pkg/gofr/datasource/file/azure/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/file/azure
go 1.25.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.4
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/text v0.34.0 // indirect
google.golang.org/api v0.270.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/file/azure/go.sum
================================================
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8=
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.4 h1:tZh20RjgfMxKBxJiIS75iTVAKIUxrST5X2dVHMTptL4=
github.com/Azure/azure-sdk-for-go/sdk/storage/azfile v1.5.4/go.mod h1:vGYAk36rhMVCfTP7v+RVruCR0zmPe6S+36KRpDCLySw=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/file/azure/storage_adapter.go
================================================
package azure
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"mime"
"path/filepath"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/directory"
azfile "github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/file"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/share"
"gofr.dev/pkg/gofr/datasource/file"
)
var (
// Storage adapter errors.
errAzureConfigNil = errors.New("azure config is nil")
errAzureClientNotInitialized = errors.New("azure client or share is not initialized")
errEmptyObjectName = errors.New("object name is empty")
errInvalidOffset = errors.New("invalid offset: must be >= 0")
errEmptySourceOrDest = errors.New("source and destination names cannot be empty")
errSameSourceOrDest = errors.New("source and destination are the same")
errFailedToCreateReader = errors.New("failed to create reader")
errFailedToCreateRangeReader = errors.New("failed to create range reader")
errObjectNotFound = errors.New("object not found")
errFailedToGetProperties = errors.New("failed to get properties")
errFailedToDeleteObject = errors.New("failed to delete object")
errFailedToCopyObject = errors.New("failed to copy object")
errFailedToListObjects = errors.New("failed to list objects")
errFailedToListDirectory = errors.New("failed to list directory")
errWriterAlreadyClosed = errors.New("writer already closed")
errInvalidWhence = errors.New("invalid whence")
errNegativeOffset = errors.New("negative offset")
errShareNameEmpty = errors.New("share name cannot be empty")
)
const (
contentTypeDirectory = "application/x-directory"
contentTypeOctetStream = "application/octet-stream"
)
// storageAdapter adapts Azure File Storage client to implement file.StorageProvider.
type storageAdapter struct {
cfg *Config
shareClient *share.Client
}
// Connect initializes the Azure File Storage client and validates share access.
func (s *storageAdapter) Connect(ctx context.Context) error {
// fast-path
if s.shareClient != nil {
return nil
}
if s.cfg == nil {
return errAzureConfigNil
}
// Create Azure credentials
cred, err := share.NewSharedKeyCredential(s.cfg.AccountName, s.cfg.AccountKey)
if err != nil {
return fmt.Errorf("failed to create shared key credential: %w", err)
}
// Build the share URL
endpoint := s.cfg.Endpoint
if endpoint == "" {
endpoint = "https://" + s.cfg.AccountName + ".file.core.windows.net"
}
// Validate share name format (Azure share names must be lowercase, 3-63 chars, alphanumeric and hyphens)
shareName := strings.TrimSpace(s.cfg.ShareName)
if shareName == "" {
return errShareNameEmpty
}
// URL encode the share name to handle any special characters
shareURL := endpoint + "/" + shareName
// Create share client
shareClient, err := share.NewClientWithSharedKeyCredential(shareURL, cred, nil)
if err != nil {
return fmt.Errorf("failed to create share client: %w", err)
}
// Validate share access by getting properties
_, err = shareClient.GetProperties(ctx, nil)
if err != nil {
// Check if error is due to context deadline exceeded (timeout)
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return fmt.Errorf("share validation failed: connection timeout (10s): %w", err)
}
return fmt.Errorf("share validation failed: %w", err)
}
s.shareClient = shareClient
return nil
}
// Health checks if the Azure connection is healthy by verifying share access.
func (s *storageAdapter) Health(ctx context.Context) error {
if s.shareClient == nil {
return errAzureClientNotInitialized
}
_, err := s.shareClient.GetProperties(ctx, nil)
if err != nil {
return fmt.Errorf("azure health check failed: %w", err)
}
return nil
}
// Close closes the Azure client connection.
func (*storageAdapter) Close() error {
// Azure SDK clients don't have explicit Close methods
// They are stateless and can be reused
return nil
}
// NewReader creates a reader for the given file.
func (s *storageAdapter) NewReader(ctx context.Context, name string) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
fileClient, err := s.getFileClient(name)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateReader, name, err)
}
// Download file
downloadResp, err := fileClient.DownloadStream(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateReader, name, err)
}
return downloadResp.Body, nil
}
// NewRangeReader creates a range reader for the given file.
func (s *storageAdapter) NewRangeReader(ctx context.Context, name string, offset, length int64) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
if offset < 0 {
return nil, fmt.Errorf("%w (got: %d)", errInvalidOffset, offset)
}
fileClient, err := s.getFileClient(name)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateRangeReader, name, err)
}
// Download file with range
opts := &azfile.DownloadStreamOptions{
Range: azfile.HTTPRange{
Offset: offset,
Count: length,
},
}
downloadResp, err := fileClient.DownloadStream(ctx, opts)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateRangeReader, name, err)
}
return downloadResp.Body, nil
}
// NewWriter creates a writer for the given file.
// Automatically creates parent directories to ensure they appear in listings
// (Azure File Storage requires explicit directory creation for directories to be visible).
func (s *storageAdapter) NewWriter(ctx context.Context, name string) io.WriteCloser {
if name == "" {
return &failWriter{err: errEmptyObjectName}
}
// Ensure parent directories exist (Azure requires explicit directory creation for listings)
// This matches local filesystem behavior where parent directories are auto-created
// Log error but don't fail - file creation might still work
// Azure may create directories implicitly, but they won't appear in listings
// This is a best-effort attempt to ensure directories are visible
_ = s.ensureParentDirectories(ctx, name)
fileClient, err := s.getFileClient(name)
if err != nil {
return &failWriter{err: err}
}
// Detect content type based on file extension (matches S3 behavior)
contentType := mime.TypeByExtension(filepath.Ext(name))
if contentType == "" {
contentType = contentTypeOctetStream // Default for unknown extensions
}
// Create file if it doesn't exist (size 0 initially, will be resized on close)
// File might already exist, that's okay for writing
// Set content type based on file extension (e.g., .json -> application/json, .txt -> text/plain)
_, _ = fileClient.Create(ctx, 0, &azfile.CreateOptions{
HTTPHeaders: &azfile.HTTPHeaders{
ContentType: &contentType,
},
})
return &azureWriter{
ctx: ctx,
fileClient: fileClient,
name: name,
buffer: make([]byte, 0),
}
}
// failWriter is a helper for NewWriter validation errors.
type failWriter struct {
err error
}
func (fw *failWriter) Write([]byte) (int, error) {
return 0, fw.err
}
func (fw *failWriter) Close() error {
return fw.err
}
// azureWriter implements io.WriteCloser for Azure File Storage.
type azureWriter struct {
ctx context.Context
fileClient *azfile.Client
name string
buffer []byte
offset int64
closed bool
}
func (w *azureWriter) Write(p []byte) (int, error) {
if w.closed {
return 0, errWriterAlreadyClosed
}
w.buffer = append(w.buffer, p...)
return len(p), nil
}
func (w *azureWriter) Close() error {
if w.closed {
return nil
}
w.closed = true
if len(w.buffer) == 0 {
return nil
}
// Get current file size to determine if we need to resize
props, err := w.fileClient.GetProperties(w.ctx, nil)
if err != nil {
return w.createNewFile()
}
return w.resizeAndUpload(&props)
}
// createNewFile creates a new file with the buffer size.
func (w *azureWriter) createNewFile() error {
// Detect content type based on file extension (matches S3 behavior)
contentType := mime.TypeByExtension(filepath.Ext(w.name))
if contentType == "" {
contentType = contentTypeOctetStream // Default for unknown extensions
}
_, createErr := w.fileClient.Create(w.ctx, int64(len(w.buffer)), &azfile.CreateOptions{
HTTPHeaders: &azfile.HTTPHeaders{
ContentType: &contentType,
},
})
if createErr != nil {
return fmt.Errorf("failed to create file: %w", createErr)
}
reader := &bytesReadSeekCloser{data: w.buffer}
_, err := w.fileClient.UploadRange(w.ctx, w.offset, reader, &azfile.UploadRangeOptions{})
return err
}
// resizeAndUpload resizes the file if needed and uploads the buffer.
func (w *azureWriter) resizeAndUpload(props *azfile.GetPropertiesResponse) error {
if props == nil || props.ContentLength == nil {
// If we can't get current size, just upload
reader := &bytesReadSeekCloser{data: w.buffer}
_, err := w.fileClient.UploadRange(w.ctx, w.offset, reader, &azfile.UploadRangeOptions{})
return err
}
currentSize := *props.ContentLength
if int64(len(w.buffer)) > currentSize {
_, resizeErr := w.fileClient.Resize(w.ctx, int64(len(w.buffer)), nil)
if resizeErr != nil {
return fmt.Errorf("failed to resize file: %w", resizeErr)
}
}
reader := &bytesReadSeekCloser{data: w.buffer}
_, err := w.fileClient.UploadRange(w.ctx, w.offset, reader, &azfile.UploadRangeOptions{})
return err
}
// statDirectory returns metadata for a directory.
func (s *storageAdapter) statDirectory(ctx context.Context, name string) (*file.ObjectInfo, error) {
dirPath := strings.TrimSuffix(name, "/")
dirPath = strings.TrimPrefix(dirPath, "/")
var dirClient *directory.Client
if dirPath == "" {
dirClient = s.shareClient.NewRootDirectoryClient()
} else {
dirClient = s.shareClient.NewDirectoryClient(dirPath)
}
props, err := dirClient.GetProperties(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
var lastModified time.Time
if props.LastModified != nil {
lastModified = *props.LastModified
}
return &file.ObjectInfo{
Name: name,
Size: 0,
ContentType: contentTypeDirectory,
LastModified: lastModified,
IsDir: true,
}, nil
}
// statFile returns metadata for a file.
func (s *storageAdapter) statFile(ctx context.Context, name string) (*file.ObjectInfo, error) {
fileClient, err := s.getFileClient(name)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToGetProperties, name, err)
}
props, err := fileClient.GetProperties(ctx, nil)
if err != nil {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
contentType := ""
if props.ContentType != nil {
contentType = *props.ContentType
}
var size int64
if props.ContentLength != nil {
size = *props.ContentLength
}
var lastModified time.Time
if props.LastModified != nil {
lastModified = *props.LastModified
}
return &file.ObjectInfo{
Name: name,
Size: size,
ContentType: contentType,
LastModified: lastModified,
IsDir: false,
}, nil
}
// StatObject returns metadata for the given file or directory.
func (s *storageAdapter) StatObject(ctx context.Context, name string) (*file.ObjectInfo, error) {
if name == "" {
return nil, errEmptyObjectName
}
// Check if it's a directory (ends with /)
if strings.HasSuffix(name, "/") {
return s.statDirectory(ctx, name)
}
return s.statFile(ctx, name)
}
// DeleteObject deletes the file or directory with the given name.
func (s *storageAdapter) DeleteObject(ctx context.Context, name string) error {
if name == "" {
return errEmptyObjectName
}
// Check if it's a directory
if strings.HasSuffix(name, "/") {
dirPath := strings.TrimSuffix(name, "/")
dirPath = strings.TrimPrefix(dirPath, "/")
var dirClient *directory.Client
if dirPath == "" {
dirClient = s.shareClient.NewRootDirectoryClient()
} else {
dirClient = s.shareClient.NewDirectoryClient(dirPath)
}
_, err := dirClient.Delete(ctx, nil)
if err != nil {
return fmt.Errorf("%w %q: %w", errFailedToDeleteObject, name, err)
}
return nil
}
// It's a file
fileClient, err := s.getFileClient(name)
if err != nil {
return fmt.Errorf("%w for %q: %w", errFailedToDeleteObject, name, err)
}
_, err = fileClient.Delete(ctx, nil)
if err != nil {
return fmt.Errorf("%w %q: %w", errFailedToDeleteObject, name, err)
}
return nil
}
// CopyObject copies a file from src to dst.
func (s *storageAdapter) CopyObject(ctx context.Context, src, dst string) error {
if src == "" || dst == "" {
return errEmptySourceOrDest
}
if src == dst {
return errSameSourceOrDest
}
// Get source file client and download
downloadResp, srcProps, err := s.getSourceFileData(ctx, src)
if err != nil {
return err
}
defer downloadResp.Body.Close()
// Create destination file
dstClient, err := s.createDestinationFile(ctx, dst, srcProps)
if err != nil {
return err
}
// Copy content type if available
if err := s.copyContentType(ctx, dstClient, srcProps); err != nil {
return err
}
// Upload content to destination
return s.uploadToDestination(ctx, dstClient, downloadResp.Body, src, dst)
}
// getSourceFileData downloads the source file and gets its properties.
func (s *storageAdapter) getSourceFileData(ctx context.Context, src string) (
azfile.DownloadStreamResponse, *azfile.GetPropertiesResponse, error) {
srcClient, err := s.getFileClient(src)
if err != nil {
return azfile.DownloadStreamResponse{}, nil,
fmt.Errorf("%w: failed to get source client: %w", errFailedToCopyObject, err)
}
downloadResp, err := srcClient.DownloadStream(ctx, nil)
if err != nil {
return azfile.DownloadStreamResponse{}, nil,
fmt.Errorf("%w from %q: %w", errFailedToCopyObject, src, err)
}
srcProps, err := srcClient.GetProperties(ctx, nil)
if err != nil {
downloadResp.Body.Close()
return azfile.DownloadStreamResponse{}, nil,
fmt.Errorf("%w: failed to get source properties: %w", errFailedToCopyObject, err)
}
return downloadResp, &srcProps, nil
}
// createDestinationFile creates the destination file with the same size as source.
// Automatically creates parent directories to ensure they appear in listings
// (matches local filesystem behavior where parent directories are auto-created).
func (s *storageAdapter) createDestinationFile(
ctx context.Context, dst string, srcProps *azfile.GetPropertiesResponse) (*azfile.Client, error) {
// Ensure parent directories exist (Azure requires explicit directory creation for listings)
// This matches local filesystem behavior where parent directories are auto-created in CopyObject
// Log error but don't fail - directory creation might still work
// Azure may create directories implicitly, but they won't appear in listings
// This is a best-effort attempt to ensure directories are visible
_ = s.ensureParentDirectories(ctx, dst)
dstClient, err := s.getFileClient(dst)
if err != nil {
return nil, fmt.Errorf("%w: failed to get destination client: %w", errFailedToCopyObject, err)
}
var contentLength int64
if srcProps != nil && srcProps.ContentLength != nil {
contentLength = *srcProps.ContentLength
}
// Detect content type based on destination file extension (matches S3 behavior)
// If source has content type, prefer it; otherwise detect from extension
var contentType string
if srcProps != nil && srcProps.ContentType != nil && *srcProps.ContentType != "" {
contentType = *srcProps.ContentType
} else {
contentType = mime.TypeByExtension(filepath.Ext(dst))
if contentType == "" {
contentType = contentTypeOctetStream // Default for unknown extensions
}
}
_, err = dstClient.Create(ctx, contentLength, &azfile.CreateOptions{
HTTPHeaders: &azfile.HTTPHeaders{
ContentType: &contentType,
},
})
if err != nil {
return nil, fmt.Errorf("%w: failed to create destination file: %w", errFailedToCopyObject, err)
}
// Note: copyContentType is still called after this to ensure content type is set correctly
// even if Create() didn't set it properly. This provides redundancy and ensures correctness.
return dstClient, nil
}
// copyContentType copies the content type from source to destination if available.
// This is called after createDestinationFile to ensure content type is correctly set.
// If content type was already set during Create(), this will set it again to the same value (safe).
func (*storageAdapter) copyContentType(ctx context.Context, dstClient *azfile.Client, srcProps *azfile.GetPropertiesResponse) error {
if srcProps == nil || srcProps.ContentType == nil || *srcProps.ContentType == "" {
return nil
}
_, err := dstClient.SetHTTPHeaders(ctx, &azfile.SetHTTPHeadersOptions{
HTTPHeaders: &azfile.HTTPHeaders{
ContentType: srcProps.ContentType,
},
})
if err != nil {
return fmt.Errorf("%w: failed to set content type: %w", errFailedToCopyObject, err)
}
return nil
}
// uploadToDestination uploads the content to the destination file.
func (*storageAdapter) uploadToDestination(ctx context.Context, dstClient *azfile.Client, body io.ReadCloser, src, dst string) error {
readSeekCloser := &readSeekCloserWrapper{reader: body}
_, err := dstClient.UploadRange(ctx, 0, readSeekCloser, &azfile.UploadRangeOptions{})
if err != nil {
return fmt.Errorf("%w from %q to %q: %w", errFailedToCopyObject, src, dst, err)
}
return nil
}
// normalizePrefix normalizes the prefix for listing operations.
func normalizePrefix(prefix string) string {
normalizedPrefix := strings.TrimPrefix(prefix, "/")
if normalizedPrefix != "" && !strings.HasSuffix(normalizedPrefix, "/") {
normalizedPrefix += "/"
}
return normalizedPrefix
}
// getDirectoryClient returns the appropriate directory client for the given prefix.
func (s *storageAdapter) getDirectoryClient(normalizedPrefix string) *directory.Client {
if normalizedPrefix == "" {
return s.shareClient.NewRootDirectoryClient()
}
dirPath := strings.TrimSuffix(normalizedPrefix, "/")
return s.shareClient.NewDirectoryClient(dirPath)
}
// processListObjectsPage processes a single page of list results and adds files to objects.
func processListObjectsPage(page *directory.ListFilesAndDirectoriesResponse, normalizedPrefix string, objects []string) []string {
for _, fileItem := range page.Segment.Files {
if fileItem.Name == nil {
continue
}
fileName := *fileItem.Name
if normalizedPrefix == "" || strings.HasPrefix(fileName, normalizedPrefix) {
objects = append(objects, fileName)
}
}
return objects
}
// ListObjects lists all files with the given prefix.
func (s *storageAdapter) ListObjects(ctx context.Context, prefix string) ([]string, error) {
if s.shareClient == nil {
return nil, errAzureClientNotInitialized
}
var objects []string
normalizedPrefix := normalizePrefix(prefix)
dirClient := s.getDirectoryClient(normalizedPrefix)
pager := dirClient.NewListFilesAndDirectoriesPager(&directory.ListFilesAndDirectoriesOptions{})
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("%w with prefix %q: %w", errFailedToListObjects, prefix, err)
}
objects = processListObjectsPage(&page, normalizedPrefix, objects)
}
return objects, nil
}
// processListDirDirectories processes directory items from a list page.
func processListDirDirectories(directories []*directory.Directory, prefixes []string) []string {
for _, dirItem := range directories {
if dirItem.Name == nil {
continue
}
dirName := *dirItem.Name
if !strings.HasSuffix(dirName, "/") {
dirName += "/"
}
prefixes = append(prefixes, dirName)
}
return prefixes
}
// processListDirFiles processes file items from a list page.
func processListDirFiles(files []*directory.File, objects []file.ObjectInfo) []file.ObjectInfo {
for _, fileItem := range files {
if fileItem.Name == nil || fileItem.Properties == nil {
continue
}
var size int64
if fileItem.Properties.ContentLength != nil {
size = *fileItem.Properties.ContentLength
}
var lastModified time.Time
if fileItem.Properties.LastModified != nil {
lastModified = *fileItem.Properties.LastModified
}
// FileProperty doesn't have ContentType field in directory package
// We'll use empty string for ContentType as it's not available in directory listing
objects = append(objects, file.ObjectInfo{
Name: *fileItem.Name,
Size: size,
ContentType: "", // ContentType not available in directory listing
LastModified: lastModified,
IsDir: false,
})
}
return objects
}
// ListDir lists files and directories (prefixes) under the given prefix.
func (s *storageAdapter) ListDir(ctx context.Context, prefix string) ([]file.ObjectInfo, []string, error) {
if s.shareClient == nil {
return nil, nil, errAzureClientNotInitialized
}
var (
objects []file.ObjectInfo
prefixes []string
)
normalizedPrefix := normalizePrefix(prefix)
dirClient := s.getDirectoryClient(normalizedPrefix)
pager := dirClient.NewListFilesAndDirectoriesPager(&directory.ListFilesAndDirectoriesOptions{})
for pager.More() {
page, err := pager.NextPage(ctx)
if err != nil {
return nil, nil, fmt.Errorf("%w %q: %w", errFailedToListDirectory, prefix, err)
}
prefixes = processListDirDirectories(page.Segment.Directories, prefixes)
objects = processListDirFiles(page.Segment.Files, objects)
}
return objects, prefixes, nil
}
// bytesReadSeekCloser wraps a byte slice to implement io.ReadSeekCloser.
type bytesReadSeekCloser struct {
data []byte
offset int64
}
func (b *bytesReadSeekCloser) Read(p []byte) (int, error) {
if b.offset >= int64(len(b.data)) {
return 0, io.EOF
}
n := copy(p, b.data[b.offset:])
b.offset += int64(n)
return n, nil
}
func (b *bytesReadSeekCloser) Seek(offset int64, whence int) (int64, error) {
var newOffset int64
switch whence {
case io.SeekStart:
newOffset = offset
case io.SeekCurrent:
newOffset = b.offset + offset
case io.SeekEnd:
newOffset = int64(len(b.data)) + offset
default:
return 0, fmt.Errorf("%w: %d", errInvalidWhence, whence)
}
if newOffset < 0 {
return 0, fmt.Errorf("%w: %d", errNegativeOffset, newOffset)
}
if newOffset > int64(len(b.data)) {
newOffset = int64(len(b.data))
}
b.offset = newOffset
return newOffset, nil
}
func (*bytesReadSeekCloser) Close() error {
return nil
}
// readSeekCloserWrapper wraps io.ReadCloser to implement io.ReadSeekCloser.
// Note: Seek operations will read and discard data, which is inefficient but necessary for compatibility.
type readSeekCloserWrapper struct {
reader io.ReadCloser
offset int64
buffer []byte
}
func (r *readSeekCloserWrapper) Read(p []byte) (int, error) {
return r.reader.Read(p)
}
func (r *readSeekCloserWrapper) Seek(offset int64, whence int) (int64, error) {
// For simplicity, we'll read all data into buffer on first seek
// This is not efficient but works for copy operations
if r.buffer == nil {
data, err := io.ReadAll(r.reader)
if err != nil {
return 0, err
}
r.buffer = data
r.reader = io.NopCloser(bytes.NewReader(r.buffer))
}
var newOffset int64
switch whence {
case io.SeekStart:
newOffset = offset
case io.SeekCurrent:
newOffset = r.offset + offset
case io.SeekEnd:
newOffset = int64(len(r.buffer)) + offset
default:
return 0, fmt.Errorf("%w: %d", errInvalidWhence, whence)
}
if newOffset < 0 {
return 0, fmt.Errorf("%w: %d", errNegativeOffset, newOffset)
}
if newOffset > int64(len(r.buffer)) {
newOffset = int64(len(r.buffer))
}
r.offset = newOffset
r.reader = io.NopCloser(bytes.NewReader(r.buffer[newOffset:]))
return newOffset, nil
}
func (r *readSeekCloserWrapper) Close() error {
return r.reader.Close()
}
// isDirectoryExistsError checks if the error indicates the directory already exists.
func isDirectoryExistsError(err error) bool {
errStr := err.Error()
return strings.Contains(errStr, "already exists") ||
strings.Contains(errStr, "ShareAlreadyExists") ||
strings.Contains(errStr, "ResourceAlreadyExists")
}
// createDirectoryLevel creates a single directory level and handles errors.
func (s *storageAdapter) createDirectoryLevel(ctx context.Context, dirPath string) error {
dirClient := s.shareClient.NewDirectoryClient(dirPath)
_, err := dirClient.Create(ctx, nil)
if err != nil && !isDirectoryExistsError(err) {
return fmt.Errorf("failed to create directory %q: %w", dirPath, err)
}
return nil
}
// ensureParentDirectories creates all parent directories for the given file path.
// Azure File Storage requires explicit directory creation for directories to appear in listings.
// This function ensures parent directories are created before file creation, matching
// local filesystem behavior where os.MkdirAll is called automatically.
func (s *storageAdapter) ensureParentDirectories(ctx context.Context, filePath string) error {
if s.shareClient == nil {
return errAzureClientNotInitialized
}
// Extract parent directory path
parentDir := getParentDir(filePath)
if parentDir == "" {
return nil // File is in root, no parent directories needed
}
// Normalize path (remove leading/trailing slashes)
parentDir = strings.Trim(parentDir, "/")
if parentDir == "" {
return nil
}
// Split into components and create each level
components := strings.Split(parentDir, "/")
currentPath := ""
for _, component := range components {
if component == "" || component == "." || component == ".." {
continue
}
if currentPath == "" {
currentPath = component
} else {
currentPath = currentPath + "/" + component
}
if err := s.createDirectoryLevel(ctx, currentPath); err != nil {
return err
}
}
return nil
}
// getParentDir extracts the parent directory path from a file path.
// Example: "dir1/subdir/file.txt" -> "dir1/subdir".
// Example: "file.txt" -> "" (root directory).
func getParentDir(filePath string) string {
// Remove leading slash if present
filePath = strings.TrimPrefix(filePath, "/")
// Find last slash
lastSlash := strings.LastIndex(filePath, "/")
if lastSlash == -1 {
return "" // File is in root
}
// Return everything before the last slash
return filePath[:lastSlash]
}
// getFileClient returns a file client for the given path.
func (s *storageAdapter) getFileClient(name string) (*azfile.Client, error) {
if s.shareClient == nil {
return nil, errAzureClientNotInitialized
}
// Normalize the path
name = strings.TrimPrefix(name, "/")
dirPath := filepath.Dir(name)
fileName := filepath.Base(name)
// Handle root directory case
if dirPath == "." || dirPath == "" || dirPath == "/" {
// File is in root directory
return s.shareClient.NewRootDirectoryClient().NewFileClient(fileName), nil
}
// File is in a subdirectory
// Normalize directory path (remove leading/trailing slashes)
dirPath = strings.TrimPrefix(dirPath, "/")
dirPath = strings.TrimSuffix(dirPath, "/")
return s.shareClient.NewDirectoryClient(dirPath).NewFileClient(fileName), nil
}
================================================
FILE: pkg/gofr/datasource/file/azure/storage_adapter_test.go
================================================
package azure
import (
"context"
"crypto/tls"
"errors"
"io"
"mime"
"net/http"
"net/http/httptest"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azfile/share"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var errTest = errors.New("test error")
// TestStorageAdapter_Connect tests the Connect method with table-driven tests.
func TestStorageAdapter_Connect(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
expectedError error
expectedMsg string
description string
}{
{
name: "nil_config",
adapter: &storageAdapter{},
expectedError: errAzureConfigNil,
expectedMsg: "azure config is nil",
description: "Should return error when config is nil",
},
{
name: "empty_share_name",
adapter: &storageAdapter{
cfg: &Config{
AccountName: "testaccount",
AccountKey: "dGVzdGtleQ==", // base64 encoded "testkey"
ShareName: "",
},
},
expectedError: errShareNameEmpty,
expectedMsg: "share name cannot be empty",
description: "Should return error when share name is empty",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.Connect(context.Background())
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
if tt.expectedMsg != "" {
assert.Contains(t, err.Error(), tt.expectedMsg)
}
})
}
}
// TestStorageAdapter_Connect_AlreadyConnected tests the Connect method when already connected.
func TestStorageAdapter_Connect_AlreadyConnected(t *testing.T) {
adapter := &storageAdapter{
cfg: &Config{ShareName: "test"},
shareClient: &share.Client{},
}
err := adapter.Connect(context.Background())
require.NoError(t, err)
}
// TestStorageAdapter_Health tests the Health method with table-driven tests.
func TestStorageAdapter_Health(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
expectedError error
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{},
expectedError: errAzureClientNotInitialized,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.Health(context.Background())
require.Error(t, err)
assert.ErrorIs(t, err, tt.expectedError)
})
}
}
// TestStorageAdapter_Close tests the Close method with table-driven tests.
func TestStorageAdapter_Close(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{},
description: "Should return nil when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.Close()
require.NoError(t, err)
})
}
}
// TestStorageAdapter_NewReader tests the NewReader method with table-driven tests.
func TestStorageAdapter_NewReader(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
objectName string
expectedError error
expectedNil bool
description string
}{
{
name: "empty_name",
adapter: &storageAdapter{},
objectName: "",
expectedError: errEmptyObjectName,
expectedNil: true,
description: "Should return error when object name is empty",
},
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
objectName: "file.txt",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader, err := tt.adapter.NewReader(context.Background(), tt.objectName)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
assert.Nil(t, reader)
})
}
}
// TestStorageAdapter_NewRangeReader tests the NewRangeReader method with table-driven tests.
func TestStorageAdapter_NewRangeReader(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
objectName string
offset int64
length int64
expectedError error
expectedNil bool
description string
}{
{
name: "empty_name",
adapter: &storageAdapter{},
objectName: "",
offset: 0,
length: 100,
expectedError: errEmptyObjectName,
expectedNil: true,
description: "Should return error when object name is empty",
},
{
name: "invalid_offset",
adapter: &storageAdapter{},
objectName: "file.txt",
offset: -1,
length: 100,
expectedError: errInvalidOffset,
expectedNil: true,
description: "Should return error when offset is negative",
},
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
objectName: "file.txt",
offset: 0,
length: 100,
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
reader, err := tt.adapter.NewRangeReader(context.Background(), tt.objectName, tt.offset, tt.length)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
assert.Nil(t, reader)
})
}
}
// TestStorageAdapter_NewWriter_EmptyName tests NewWriter with empty name.
func TestStorageAdapter_NewWriter_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
writer := adapter.NewWriter(context.Background(), "")
require.NotNil(t, writer)
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
require.Error(t, err)
require.ErrorIs(t, err, errEmptyObjectName)
}
// TestStorageAdapter_NewWriter_ValidName tests NewWriter with valid name.
func TestStorageAdapter_NewWriter_ValidName(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{ShareName: "test"}}
writer := adapter.NewWriter(context.Background(), "file.txt")
require.NotNil(t, writer)
}
// TestAzureWriter_Write_Success tests successful write operation.
func TestAzureWriter_Write_Success(t *testing.T) {
writer := &azureWriter{
buffer: make([]byte, 0),
closed: false,
}
n, err := writer.Write([]byte("test data"))
assert.Equal(t, 9, n)
require.NoError(t, err)
}
// TestAzureWriter_Write_WhenClosed tests write operation when writer is closed.
func TestAzureWriter_Write_WhenClosed(t *testing.T) {
writer := &azureWriter{
buffer: make([]byte, 0),
closed: true,
}
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
require.Error(t, err)
require.ErrorIs(t, err, errWriterAlreadyClosed)
}
// TestAzureWriter_Close_AlreadyClosed tests Close when writer is already closed.
func TestAzureWriter_Close_AlreadyClosed(t *testing.T) {
writer := &azureWriter{
closed: true,
}
err := writer.Close()
require.NoError(t, err)
}
// TestAzureWriter_Close_EmptyBuffer tests Close when buffer is empty.
func TestAzureWriter_Close_EmptyBuffer(t *testing.T) {
writer := &azureWriter{
closed: false,
buffer: []byte{},
}
err := writer.Close()
require.NoError(t, err)
}
// TestStorageAdapter_StatObject tests the StatObject method with table-driven tests.
func TestStorageAdapter_StatObject(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
objectName string
expectedError error
expectedNil bool
description string
}{
{
name: "empty_name",
adapter: &storageAdapter{},
objectName: "",
expectedError: errEmptyObjectName,
expectedNil: true,
description: "Should return error when object name is empty",
},
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
objectName: "file.txt",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info, err := tt.adapter.StatObject(context.Background(), tt.objectName)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
assert.Nil(t, info)
})
}
}
// TestStorageAdapter_DeleteObject tests the DeleteObject method with table-driven tests.
func TestStorageAdapter_DeleteObject(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
objectName string
expectedError error
description string
}{
{
name: "empty_name",
adapter: &storageAdapter{},
objectName: "",
expectedError: errEmptyObjectName,
description: "Should return error when object name is empty",
},
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
objectName: "file.txt",
expectedError: errAzureClientNotInitialized,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.DeleteObject(context.Background(), tt.objectName)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
})
}
}
// TestStorageAdapter_CopyObject tests the CopyObject method with table-driven tests.
func TestStorageAdapter_CopyObject(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
source string
destination string
expectedError error
description string
}{
{
name: "empty_source",
adapter: &storageAdapter{},
source: "",
destination: "dest.txt",
expectedError: errEmptySourceOrDest,
description: "Should return error when source is empty",
},
{
name: "empty_destination",
adapter: &storageAdapter{},
source: "source.txt",
destination: "",
expectedError: errEmptySourceOrDest,
description: "Should return error when destination is empty",
},
{
name: "both_empty",
adapter: &storageAdapter{},
source: "",
destination: "",
expectedError: errEmptySourceOrDest,
description: "Should return error when both source and destination are empty",
},
{
name: "same_source_and_dest",
adapter: &storageAdapter{},
source: "file.txt",
destination: "file.txt",
expectedError: errSameSourceOrDest,
description: "Should return error when source and destination are the same",
},
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
source: "source.txt",
destination: "dest.txt",
expectedError: errAzureClientNotInitialized,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.CopyObject(context.Background(), tt.source, tt.destination)
require.Error(t, err)
assert.ErrorIs(t, err, tt.expectedError)
})
}
}
// TestStorageAdapter_ListObjects tests the ListObjects method with table-driven tests.
func TestStorageAdapter_ListObjects(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
prefix string
expectedError error
expectedNil bool
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
prefix: "prefix/",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil",
},
{
name: "empty_prefix",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
prefix: "",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil (empty prefix)",
},
{
name: "root_prefix",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
prefix: "/",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil (root prefix)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objects, err := tt.adapter.ListObjects(context.Background(), tt.prefix)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
assert.Nil(t, objects)
})
}
}
// TestStorageAdapter_ListDir tests the ListDir method with table-driven tests.
func TestStorageAdapter_ListDir(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
prefix string
expectedError error
expectedNil bool
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
prefix: "prefix/",
expectedError: errAzureClientNotInitialized,
expectedNil: true,
description: "Should return error when client is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
objects, prefixes, err := tt.adapter.ListDir(context.Background(), tt.prefix)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
assert.Nil(t, objects)
assert.Nil(t, prefixes)
})
}
}
// TestFailWriter tests the failWriter implementation with table-driven tests.
func TestFailWriter(t *testing.T) {
tests := []struct {
name string
writer *failWriter
data []byte
writeN int
writeErr error
closeErr error
description string
}{
{
name: "write_error",
writer: &failWriter{err: errTest},
data: []byte("test"),
writeN: 0,
writeErr: errTest,
closeErr: errTest,
description: "Should return error on write and close",
},
{
name: "empty_object_name_error",
writer: &failWriter{err: errEmptyObjectName},
data: []byte("data"),
writeN: 0,
writeErr: errEmptyObjectName,
closeErr: errEmptyObjectName,
description: "Should return empty object name error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
n, err := tt.writer.Write(tt.data)
assert.Equal(t, tt.writeN, n)
require.ErrorIs(t, err, tt.writeErr)
err = tt.writer.Close()
assert.ErrorIs(t, err, tt.closeErr)
})
}
}
// TestBytesReadSeekCloser_Read_Success tests successful read operation.
func TestBytesReadSeekCloser_Read_Success(t *testing.T) {
brsc := &bytesReadSeekCloser{data: []byte("hello world")}
readBuf := make([]byte, 5)
n, err := brsc.Read(readBuf)
assert.Equal(t, 5, n)
require.NoError(t, err)
}
// TestBytesReadSeekCloser_Read_EOF tests read operation when at end of data.
func TestBytesReadSeekCloser_Read_EOF(t *testing.T) {
brsc := &bytesReadSeekCloser{data: []byte("hi"), offset: 2}
readBuf := make([]byte, 10)
n, err := brsc.Read(readBuf)
assert.Equal(t, 0, n)
require.Error(t, err)
require.ErrorIs(t, err, io.EOF)
}
// TestBytesReadSeekCloser_Seek tests the Seek method of bytesReadSeekCloser.
func TestBytesReadSeekCloser_Seek(t *testing.T) {
tests := []struct {
name string
setup func() *bytesReadSeekCloser
seekOffset int64
seekWhence int
seekPos int64
seekErr error
description string
}{
{
name: "seek_start",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello world"), offset: 5}
},
seekOffset: 2,
seekWhence: io.SeekStart,
seekPos: 2,
seekErr: nil,
description: "Should seek to position from start",
},
{
name: "seek_current",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello world"), offset: 3}
},
seekOffset: 2,
seekWhence: io.SeekCurrent,
seekPos: 5,
seekErr: nil,
description: "Should seek relative to current position",
},
{
name: "seek_end",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello world")}
},
seekOffset: -3,
seekWhence: io.SeekEnd,
seekPos: 8,
seekErr: nil,
description: "Should seek relative to end",
},
{
name: "seek_invalid_whence",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello world")}
},
seekOffset: 0,
seekWhence: 99,
seekPos: 0,
seekErr: errInvalidWhence,
description: "Should return error for invalid whence",
},
{
name: "seek_negative_offset",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello world")}
},
seekOffset: -1,
seekWhence: io.SeekStart,
seekPos: 0,
seekErr: errNegativeOffset,
description: "Should return error for negative offset",
},
{
name: "seek_beyond_end",
setup: func() *bytesReadSeekCloser {
return &bytesReadSeekCloser{data: []byte("hello")}
},
seekOffset: 100,
seekWhence: io.SeekStart,
seekPos: 5,
seekErr: nil,
description: "Should clamp to data length when seeking beyond end",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
brsc := tt.setup()
pos, err := brsc.Seek(tt.seekOffset, tt.seekWhence)
assert.Equal(t, tt.seekPos, pos)
if tt.seekErr != nil {
require.Error(t, err)
require.ErrorIs(t, err, tt.seekErr)
} else {
require.NoError(t, err)
}
})
}
}
// TestBytesReadSeekCloser_Close tests the Close method of bytesReadSeekCloser.
func TestBytesReadSeekCloser_Close(t *testing.T) {
brsc := &bytesReadSeekCloser{data: []byte("hello world")}
err := brsc.Close()
assert.NoError(t, err)
}
// TestStorageAdapter_getFileClient tests the getFileClient method with table-driven tests.
func TestStorageAdapter_getFileClient(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
filePath string
expectedError error
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
filePath: "file.txt",
expectedError: errAzureClientNotInitialized,
description: "Should return error when shareClient is nil",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.adapter.getFileClient(tt.filePath)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
})
}
}
// TestGetParentDir tests the getParentDir helper function with table-driven tests.
func TestGetParentDir(t *testing.T) {
tests := []struct {
name string
filePath string
expected string
description string
}{
{
name: "root_file",
filePath: "file.txt",
expected: "",
description: "Should return empty string for root file",
},
{
name: "single_level",
filePath: "dir/file.txt",
expected: "dir",
description: "Should return parent directory for single level",
},
{
name: "nested_path",
filePath: "dir1/subdir/file.txt",
expected: "dir1/subdir",
description: "Should return parent directory for nested path",
},
{
name: "leading_slash",
filePath: "/dir/file.txt",
expected: "dir",
description: "Should handle leading slash",
},
{
name: "deeply_nested",
filePath: "level1/level2/level3/file.txt",
expected: "level1/level2/level3",
description: "Should return parent for deeply nested path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getParentDir(tt.filePath)
assert.Equal(t, tt.expected, result, tt.description)
})
}
}
// TestNewWriter_SetsContentType tests that NewWriter sets content type based on file extension.
func TestNewWriter_SetsContentType(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{ShareName: "test"}}
testCases := []struct {
name string
expectedCT string
description string
}{
{"file.json", "application/json", "JSON files should have application/json content type"},
{"file.txt", "text/plain", "Text files should have text/plain content type"},
{"file.csv", "text/csv", "CSV files should have text/csv content type"},
{"file.xml", "text/xml", "XML files should have text/xml content type"},
{"file.html", "text/html", "HTML files should have text/html content type"},
{"file.pdf", "application/pdf", "PDF files should have application/pdf content type"},
{"file.js", "application/javascript", "JavaScript files should have application/javascript content type"},
{"file.css", "text/css", "CSS files should have text/css content type"},
{"file.yaml", "application/x-yaml", "YAML files should have application/x-yaml content type"},
{"file.yml", "application/x-yaml", "YAML files (.yml) should have application/x-yaml content type"},
{"file.unknown", "application/octet-stream", "Unknown extensions should default to application/octet-stream"},
{"noextension", "application/octet-stream", "Files without extensions should default to application/octet-stream"},
{"dir/subdir/file.json", "application/json", "Nested paths should detect content type from filename"},
{"dir/subdir/file.txt", "text/plain", "Nested paths should detect content type from filename"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
writer := adapter.NewWriter(context.Background(), tc.name)
require.NotNil(t, writer)
})
}
}
// TestContentTypeDetection_Logic tests the content type detection logic directly.
func TestContentTypeDetection_Logic(t *testing.T) {
testCases := []struct {
name string
expectedBaseCT string
description string
}{
{"file.json", "application/json", "JSON files"},
{"file.txt", "text/plain", "Text files"},
{"file.csv", "text/csv", "CSV files"},
{"file.xml", "application/xml", "XML files (mime returns application/xml)"},
{"file.html", "text/html", "HTML files"},
{"file.pdf", "application/pdf", "PDF files"},
{"file.js", "text/javascript", "JavaScript files (mime returns text/javascript)"},
{"file.css", "text/css", "CSS files"},
{"file.yaml", "application/octet-stream", "YAML files (not in standard mime types)"},
{"file.yml", "application/octet-stream", "YAML files (.yml, not in standard mime types)"},
{"file.unknown", "application/octet-stream", "Unknown extensions"},
{"noextension", "application/octet-stream", "Files without extensions"},
{"dir/subdir/file.json", "application/json", "Nested paths"},
{"dir/subdir/file.txt", "text/plain", "Nested paths"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
contentType := mime.TypeByExtension(filepath.Ext(tc.name))
if contentType == "" {
contentType = "application/octet-stream"
}
baseContentType := contentType
if idx := strings.Index(contentType, ";"); idx != -1 {
baseContentType = contentType[:idx]
}
assert.Equal(t, tc.expectedBaseCT, baseContentType, tc.description)
assert.NotEmpty(t, contentType, "Content type should not be empty")
})
}
}
// TestCreateNewFile_SetsContentType tests that createNewFile sets content type based on file extension.
func TestCreateNewFile_SetsContentType(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{ShareName: "test"}}
testCases := []struct {
filename string
expectedCT string
}{
{"test.json", "application/json"},
{"test.txt", "text/plain"},
{"test.csv", "text/csv"},
{"test.unknown", "application/octet-stream"},
}
for _, tc := range testCases {
t.Run(tc.filename, func(t *testing.T) {
writer := adapter.NewWriter(context.Background(), tc.filename)
require.NotNil(t, writer)
})
}
}
// TestCopyObject_ContentTypeHandling tests that CopyObject handles content types correctly.
func TestCopyObject_ContentTypeHandling(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{ShareName: "test"}}
testCases := []struct {
name string
source string
destination string
description string
}{
{"copy json to json", "source.json", "dest.json", "JSON to JSON copy should preserve content type"},
{"copy txt to txt", "source.txt", "dest.txt", "Text to text copy should preserve content type"},
{"copy json to txt", "source.json", "dest.txt", "JSON to text copy should detect from destination"},
{"copy txt to json", "source.txt", "dest.json", "Text to JSON copy should detect from destination"},
{"copy with nested paths", "dir1/file.json", "dir2/file.json", "Nested paths should handle content types"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := adapter.CopyObject(context.Background(), tc.source, tc.destination)
require.Error(t, err)
assert.ErrorIs(t, err, errAzureClientNotInitialized)
})
}
}
// TestEnsureParentDirectories tests the ensureParentDirectories helper function.
func TestEnsureParentDirectories(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
filePath string
expectedError error
description string
}{
{
name: "nil_client",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
filePath: "dir/file.txt",
expectedError: errAzureClientNotInitialized,
description: "Should return error when shareClient is nil",
},
{
name: "root_file",
adapter: &storageAdapter{cfg: &Config{ShareName: "test"}},
filePath: "file.txt",
expectedError: errAzureClientNotInitialized,
description: "Should return error for root file (nil client, but getParentDir returns empty)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.adapter.ensureParentDirectories(context.Background(), tt.filePath)
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedError)
})
}
}
// TestCopyContentType tests the copyContentType helper function.
// Note: We can't easily create azfile.GetPropertiesResponse in tests, so we test with nil.
func TestCopyContentType(t *testing.T) {
adapter := &storageAdapter{}
// Test with nil srcProps - should return nil without error
err := adapter.copyContentType(context.Background(), nil, nil)
assert.NoError(t, err)
}
// setupAzureTestServer creates an HTTPS httptest server that mocks Azure File Storage REST API.
// Azure SDK requires HTTPS for authenticated requests.
func setupAzureTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
t.Helper()
srv := httptest.NewTLSServer(handler)
return srv
}
// createTestShareClient creates a share client pointing to the test server.
// Note: Azure SDK requires HTTPS, so we use httptest.NewTLSServer.
// We configure the client to skip TLS verification for testing.
func createTestShareClient(t *testing.T, serverURL string) (*share.Client, error) {
t.Helper()
cred, err := share.NewSharedKeyCredential("testaccount", "dGVzdGtleQ==")
require.NoError(t, err)
// Create HTTP client that skips TLS verification for test server
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, //nolint:gosec // Only for testing
},
}
httpClient := &http.Client{
Transport: transport,
}
// Create client options with custom HTTP client
clientOptions := &share.ClientOptions{
ClientOptions: policy.ClientOptions{
Transport: httpClient,
},
}
shareURL := serverURL + "/testshare"
shareClient, err := share.NewClientWithSharedKeyCredential(shareURL, cred, clientOptions)
return shareClient, err
}
// TestStorageAdapter_Connect_Success tests successful connection using httptest server.
func TestStorageAdapter_Connect_Success(t *testing.T) {
tests := []struct {
name string
setupClient bool
description string
}{
{
name: "already_connected",
setupClient: true,
description: "Should return nil when already connected (fast-path)",
},
{
name: "new_connection",
setupClient: false,
description: "Should connect successfully to test server",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mock GetProperties for share validation
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=share") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(` `))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
adapter := &storageAdapter{
cfg: &Config{
AccountName: "testaccount",
AccountKey: "dGVzdGtleQ==", // base64 encoded "testkey"
ShareName: "testshare",
Endpoint: srv.URL,
},
}
// Create shareClient manually for testing
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter.shareClient = shareClient
err = adapter.Connect(context.Background())
require.NoError(t, err)
assert.NotNil(t, adapter.shareClient)
})
}
}
// TestStorageAdapter_Health_Success tests successful health check using httptest server.
func TestStorageAdapter_Health_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=share") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(` `))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.Health(context.Background())
require.NoError(t, err)
}
// TestStorageAdapter_NewReader_Success tests successful file reading using httptest server.
func TestStorageAdapter_NewReader_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// File download request
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/file.txt") &&
!strings.Contains(r.URL.RawQuery, "restype") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello world"))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
reader, err := adapter.NewReader(context.Background(), "file.txt")
require.NoError(t, err)
require.NotNil(t, reader)
defer reader.Close()
data, err := io.ReadAll(reader)
require.NoError(t, err)
assert.Equal(t, "hello world", string(data))
}
// TestStorageAdapter_NewRangeReader_Success tests successful range reading using httptest server.
func TestStorageAdapter_NewRangeReader_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Range request - Azure File Storage uses Range header
// Azure SDK may send Range header or use query params
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/file.txt") {
rangeHeader := r.Header.Get("Range")
// Check for range in query params as well (Azure SDK might use this)
hasRange := rangeHeader != "" || strings.Contains(r.URL.RawQuery, "range")
if hasRange && (strings.Contains(rangeHeader, "bytes=5-9") || strings.Contains(rangeHeader, "bytes=5-")) {
// Handle range request
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Range", "bytes 5-9/11")
w.Header().Set("Content-Length", "5")
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write([]byte(" world"))
return
}
// Regular download request (fallback)
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello world"))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
reader, err := adapter.NewRangeReader(context.Background(), "file.txt", 5, 5)
// May fail if handler doesn't match Azure SDK's exact request format
// But we test the code path
if err == nil {
defer reader.Close()
data, readErr := io.ReadAll(reader)
if readErr == nil {
// If we got data, verify it (might be full file if range not handled)
assert.NotEmpty(t, data)
}
}
}
// TestStorageAdapter_StatObject_Success tests successful file stat using httptest server.
func TestStorageAdapter_StatObject_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// File properties request
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/file.txt") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "123")
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
info, err := adapter.StatObject(context.Background(), "file.txt")
require.NoError(t, err)
require.NotNil(t, info)
assert.Equal(t, "file.txt", info.Name)
assert.Equal(t, int64(123), info.Size)
assert.Equal(t, "text/plain", info.ContentType)
assert.False(t, info.IsDir)
}
// TestStorageAdapter_StatObject_Directory tests successful directory stat using httptest server.
func TestStorageAdapter_StatObject_Directory(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Directory properties request - Azure uses GET with restype=directory query param
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/mydir") &&
strings.Contains(r.URL.RawQuery, "restype=directory") {
w.Header().Set("Content-Type", "application/x-directory")
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
w.WriteHeader(http.StatusOK)
return
}
// Also handle HEAD requests
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/mydir") {
w.Header().Set("Content-Type", "application/x-directory")
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
info, err := adapter.StatObject(context.Background(), "mydir/")
require.NoError(t, err)
require.NotNil(t, info)
assert.Equal(t, "mydir/", info.Name)
assert.True(t, info.IsDir)
}
// TestStorageAdapter_DeleteObject_Success tests successful file deletion using httptest server.
func TestStorageAdapter_DeleteObject_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// File delete request
if r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "testshare/file.txt") {
w.WriteHeader(http.StatusAccepted)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.DeleteObject(context.Background(), "file.txt")
require.NoError(t, err)
}
// TestStorageAdapter_DeleteObject_Directory tests successful directory deletion using httptest server.
func TestStorageAdapter_DeleteObject_Directory(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Directory delete request
if r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "testshare/mydir") &&
strings.Contains(r.URL.RawQuery, "restype=directory") {
w.WriteHeader(http.StatusAccepted)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.DeleteObject(context.Background(), "mydir/")
require.NoError(t, err)
}
// TestStorageAdapter_ListObjects_Success tests successful object listing using httptest server.
func TestStorageAdapter_ListObjects_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files and directories request
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
file1.txt
100
Mon, 01 Jan 2024 00:00:00 GMT
file2.txt
200
Mon, 01 Jan 2024 00:00:00 GMT
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, err := adapter.ListObjects(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, objects)
}
// TestStorageAdapter_ListDir_Success tests successful directory listing using httptest server.
func TestStorageAdapter_ListDir_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files and directories request
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
subdir
file1.txt
100
Mon, 01 Jan 2024 00:00:00 GMT
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, prefixes, err := adapter.ListDir(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, objects)
require.NotNil(t, prefixes)
}
// TestStorageAdapter_NewWriter_Success tests successful file writing using httptest server.
func TestStorageAdapter_NewWriter_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// File create request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/test.txt") {
w.WriteHeader(http.StatusCreated)
return
}
// File upload range request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/test.txt") &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "test.txt")
require.NotNil(t, writer)
// Write some data
n, err := writer.Write([]byte("test data"))
require.NoError(t, err)
assert.Equal(t, 9, n)
// Close will attempt to create/upload file (will fail due to handler limitations, but tests the path)
err = writer.Close()
// May fail due to handler not handling all Azure API calls, but we test the code path
_ = err
}
// TestStorageAdapter_NewWriter_ExistingFile tests writing to existing file using httptest server.
func TestStorageAdapter_NewWriter_ExistingFile(t *testing.T) {
tests := []struct {
name string
existingSize int64
newContentSize int
shouldResize bool
description string
}{
{
name: "resize_needed",
existingSize: 10,
newContentSize: 20,
shouldResize: true,
description: "Should resize when new content is larger",
},
{
name: "no_resize_needed",
existingSize: 20,
newContentSize: 10,
shouldResize: false,
description: "Should not resize when new content is smaller",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetProperties request (file exists)
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/existing.txt") {
w.Header().Set("Content-Length", strconv.FormatInt(tt.existingSize, 10))
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
return
}
// Resize request (if needed)
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/existing.txt") &&
strings.Contains(r.URL.RawQuery, "comp=properties") {
w.WriteHeader(http.StatusOK)
return
}
// Upload range request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/existing.txt") &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "existing.txt")
require.NotNil(t, writer)
// Write data
content := strings.Repeat("a", tt.newContentSize)
n, err := writer.Write([]byte(content))
require.NoError(t, err)
assert.Equal(t, tt.newContentSize, n)
// Close will attempt to resize and upload
err = writer.Close()
_ = err
})
}
}
// TestStorageAdapter_NewWriter_NewFile tests creating new file using httptest server.
func TestStorageAdapter_NewWriter_NewFile(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetProperties request (file doesn't exist - returns error)
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/newfile.txt") {
w.WriteHeader(http.StatusNotFound)
return
}
// File create request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/newfile.txt") &&
!strings.Contains(r.URL.RawQuery, "comp") {
w.WriteHeader(http.StatusCreated)
return
}
// Upload range request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/newfile.txt") &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "newfile.txt")
require.NotNil(t, writer)
// Write data
content := "new file content"
n, err := writer.Write([]byte(content))
require.NoError(t, err)
assert.Equal(t, len(content), n)
// Close will attempt to create new file
err = writer.Close()
_ = err
}
// TestStorageAdapter_ResizeAndUpload_NoResize tests resizeAndUpload when no resize is needed.
func TestStorageAdapter_ResizeAndUpload_NoResize(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetProperties request
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/file.txt") {
w.Header().Set("Content-Length", "20")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
return
}
// Upload range request (no resize needed)
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/file.txt") &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "file.txt")
require.NotNil(t, writer)
// Write data smaller than existing file
n, err := writer.Write([]byte("small content"))
require.NoError(t, err)
assert.Equal(t, 13, n)
// Close will attempt to upload without resize
err = writer.Close()
_ = err
}
// TestStorageAdapter_ResizeAndUpload_NilProps tests resizeAndUpload with nil props.
func TestStorageAdapter_ResizeAndUpload_NilProps(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetProperties request fails (returns nil props)
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/file.txt") {
w.WriteHeader(http.StatusNotFound)
return
}
// Upload range request (fallback when props are nil)
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/file.txt") &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "file.txt")
require.NotNil(t, writer)
// Write data
n, err := writer.Write([]byte("test data"))
require.NoError(t, err)
assert.Equal(t, 9, n)
// Close will attempt to get props, fail, then create new file
err = writer.Close()
_ = err
}
// TestStorageAdapter_ResizeAndUpload_ResizeError tests resizeAndUpload when resize fails.
func TestStorageAdapter_ResizeAndUpload_ResizeError(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// GetProperties request
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/file.txt") {
w.Header().Set("Content-Length", "5")
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
return
}
// Resize request fails
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/file.txt") &&
strings.Contains(r.URL.RawQuery, "comp=properties") {
w.WriteHeader(http.StatusForbidden)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
writer := adapter.NewWriter(context.Background(), "file.txt")
require.NotNil(t, writer)
// Write data larger than existing file (triggers resize)
content := "larger content"
n, err := writer.Write([]byte(content))
require.NoError(t, err)
assert.Equal(t, len(content), n)
// Close will attempt to resize, which will fail
err = writer.Close()
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to resize file")
}
// TestStorageAdapter_GetSourceFileData_GetPropertiesError tests error handling when GetProperties fails.
func TestStorageAdapter_GetSourceFileData_GetPropertiesError(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Source file download succeeds
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/source.txt") &&
!strings.Contains(r.URL.RawQuery, "restype") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("source data"))
return
}
// Source file properties fails
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/source.txt") {
w.WriteHeader(http.StatusForbidden)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.CopyObject(context.Background(), "source.txt", "dest.txt")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to copy object")
assert.Contains(t, err.Error(), "failed to get source properties")
}
// TestStorageAdapter_Connect_DefaultEndpoint tests Connect with default endpoint.
func TestStorageAdapter_Connect_DefaultEndpoint(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Mock GetProperties for share validation
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=share") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(` `))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
// Test with empty endpoint (should use default)
adapter := &storageAdapter{
cfg: &Config{
AccountName: "testaccount",
AccountKey: "dGVzdGtleQ==",
ShareName: "testshare",
Endpoint: "", // Empty endpoint should trigger default
},
}
// Manually set shareClient to test fast-path
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter.shareClient = shareClient
// Test fast-path
err = adapter.Connect(context.Background())
require.NoError(t, err)
assert.NotNil(t, adapter.shareClient)
}
// TestStorageAdapter_Connect_InvalidCredentials tests Connect with invalid credentials.
func TestStorageAdapter_Connect_InvalidCredentials(t *testing.T) {
adapter := &storageAdapter{
cfg: &Config{
AccountName: "testaccount",
AccountKey: "invalid-key-not-base64", // Invalid base64
ShareName: "testshare",
},
}
err := adapter.Connect(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to create shared key credential")
}
// TestStorageAdapter_Connect_ShareValidationError tests Connect when share validation fails.
func TestStorageAdapter_Connect_ShareValidationError(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Share validation fails
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=share") {
w.WriteHeader(http.StatusNotFound)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
adapter := &storageAdapter{
cfg: &Config{
AccountName: "testaccount",
AccountKey: "dGVzdGtleQ==",
ShareName: "testshare",
Endpoint: srv.URL,
},
}
// This will fail because we can't easily test the full Connect flow with httptest
// without modifying Connect to accept client options
// For now, we test the error paths we can test
_ = adapter
}
// handleCopyObjectSourceDownload handles source file download requests.
func handleCopyObjectSourceDownload(w http.ResponseWriter, r *http.Request, source string) bool {
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/"+source) &&
!strings.Contains(r.URL.RawQuery, "restype") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("source data"))
return true
}
return false
}
// handleCopyObjectSourceProperties handles source file properties requests.
func handleCopyObjectSourceProperties(w http.ResponseWriter, r *http.Request, source string, hasContentType bool) bool {
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/"+source) {
contentType := "text/plain"
if hasContentType {
contentType = "application/json"
}
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
return true
}
return false
}
// handleCopyObjectDirectoryCreate handles directory creation for nested paths.
func handleCopyObjectDirectoryCreate(w http.ResponseWriter, r *http.Request) bool {
if r.Method == http.MethodPut && strings.Contains(r.URL.RawQuery, "restype=directory") {
w.WriteHeader(http.StatusCreated)
return true
}
return false
}
// handleCopyObjectFileCreate handles destination file creation.
func handleCopyObjectFileCreate(w http.ResponseWriter, r *http.Request, destination string) bool {
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/"+destination) &&
!strings.Contains(r.URL.RawQuery, "comp") {
w.WriteHeader(http.StatusCreated)
return true
}
return false
}
// handleCopyObjectFileUpload handles destination file upload range.
func handleCopyObjectFileUpload(w http.ResponseWriter, r *http.Request, destination string) bool {
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/"+destination) &&
strings.Contains(r.URL.RawQuery, "comp=range") {
w.WriteHeader(http.StatusCreated)
return true
}
return false
}
// handleCopyObjectSetHeaders handles setting HTTP headers (content type).
func handleCopyObjectSetHeaders(w http.ResponseWriter, r *http.Request, destination string) bool {
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/"+destination) &&
strings.Contains(r.URL.RawQuery, "comp=properties") {
w.WriteHeader(http.StatusOK)
return true
}
return false
}
// handleCopyObjectDestination handles destination file operations.
func handleCopyObjectDestination(w http.ResponseWriter, r *http.Request, destination string) bool {
if handleCopyObjectDirectoryCreate(w, r) {
return true
}
if handleCopyObjectFileCreate(w, r, destination) {
return true
}
if handleCopyObjectFileUpload(w, r, destination) {
return true
}
if handleCopyObjectSetHeaders(w, r, destination) {
return true
}
return false
}
// createCopyObjectHandler creates an HTTP handler for CopyObject tests.
func createCopyObjectHandler(source, destination string, hasContentType bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if handleCopyObjectSourceDownload(w, r, source) {
return
}
if handleCopyObjectSourceProperties(w, r, source, hasContentType) {
return
}
if handleCopyObjectDestination(w, r, destination) {
return
}
http.NotFound(w, r)
}
}
// TestStorageAdapter_CopyObject_Success tests successful file copy using httptest server.
func TestStorageAdapter_CopyObject_Success(t *testing.T) {
tests := []struct {
name string
source string
destination string
hasContentType bool
description string
}{
{
name: "with_content_type",
source: "source.json",
destination: "dest.json",
hasContentType: true,
description: "Should copy file with content type",
},
{
name: "without_content_type",
source: "source.txt",
destination: "dest.txt",
hasContentType: false,
description: "Should copy file and detect content type from extension",
},
{
name: "nested_paths",
source: "dir1/source.txt",
destination: "dir2/dest.txt",
hasContentType: false,
description: "Should copy file with nested paths",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := createCopyObjectHandler(tt.source, tt.destination, tt.hasContentType)
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.CopyObject(context.Background(), tt.source, tt.destination)
// May fail due to handler not handling all Azure API calls perfectly, but tests the code path
_ = err
})
}
}
// TestStorageAdapter_CopyObject_GetSourceFileDataError tests error handling in getSourceFileData.
func TestStorageAdapter_CopyObject_GetSourceFileDataError(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Source file download fails
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/source.txt") {
w.WriteHeader(http.StatusNotFound)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.CopyObject(context.Background(), "source.txt", "dest.txt")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to copy object")
}
// TestStorageAdapter_CopyObject_CreateDestinationError tests error handling in createDestinationFile.
func TestStorageAdapter_CopyObject_CreateDestinationError(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Source file download succeeds
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare/source.txt") &&
!strings.Contains(r.URL.RawQuery, "restype") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("source data"))
return
}
// Source file properties succeed
if r.Method == http.MethodHead && strings.Contains(r.URL.Path, "testshare/source.txt") {
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("Content-Length", "11")
w.WriteHeader(http.StatusOK)
return
}
// Destination file create fails
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/dest.txt") &&
!strings.Contains(r.URL.RawQuery, "comp") {
w.WriteHeader(http.StatusForbidden)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.CopyObject(context.Background(), "source.txt", "dest.txt")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to copy object")
}
// TestStorageAdapter_CopyContentType_WithContentType tests copyContentType with content type.
func TestStorageAdapter_CopyContentType_WithContentType(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set HTTP headers request
if r.Method == http.MethodPut && strings.Contains(r.URL.Path, "testshare/dest.txt") &&
strings.Contains(r.URL.RawQuery, "comp=properties") {
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
// Get a file client for testing
fileClient, err := adapter.getFileClient("dest.txt")
require.NoError(t, err)
// Create a mock GetPropertiesResponse with content type
// We can't construct the actual type, so we test the nil/empty checks
err = adapter.copyContentType(context.Background(), fileClient, nil)
assert.NoError(t, err)
}
// TestStorageAdapter_ListObjects_EmptyPrefix tests ListObjects with empty prefix.
func TestStorageAdapter_ListObjects_EmptyPrefix(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files at root
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
file1.txt
100
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, err := adapter.ListObjects(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, objects)
}
// TestStorageAdapter_ListDir_EmptyPrefix tests ListDir with empty prefix.
func TestStorageAdapter_ListDir_EmptyPrefix(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files and directories at root
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
subdir
file1.txt
100
Mon, 01 Jan 2024 00:00:00 GMT
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, prefixes, err := adapter.ListDir(context.Background(), "")
require.NoError(t, err)
require.NotNil(t, objects)
require.NotNil(t, prefixes)
}
// TestStorageAdapter_GetFileClient_Success tests getFileClient with actual Azure client.
func TestStorageAdapter_GetFileClient_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
// Accept any request for this test
w.WriteHeader(http.StatusOK)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
tests := []struct {
name string
filePath string
expected bool
}{
{"root_file", "file.txt", true},
{"subdirectory_file", "dir/file.txt", true},
{"nested_path", "dir1/subdir/file.txt", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fileClient, err := adapter.getFileClient(tt.filePath)
require.NoError(t, err)
assert.NotNil(t, fileClient)
})
}
}
// TestStorageAdapter_EnsureParentDirectories_Success tests ensureParentDirectories with actual Azure client.
func TestStorageAdapter_EnsureParentDirectories_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Directory create request
if r.Method == http.MethodPut && strings.Contains(r.URL.RawQuery, "restype=directory") {
w.WriteHeader(http.StatusCreated)
return
}
// Directory already exists
if r.Method == http.MethodPut {
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte("DirectoryAlreadyExists"))
return
}
w.WriteHeader(http.StatusOK)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
tests := []struct {
name string
filePath string
expectError bool
description string
}{
{
name: "root_file",
filePath: "file.txt",
expectError: false,
description: "Should return nil for root file (no parent directories)",
},
{
name: "single_level",
filePath: "dir/file.txt",
expectError: false,
description: "Should create parent directory for single level",
},
{
name: "nested_path",
filePath: "dir1/subdir/file.txt",
expectError: false,
description: "Should create all parent directories for nested path",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := adapter.ensureParentDirectories(context.Background(), tt.filePath)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// TestStorageAdapter_ListObjects_WithPrefix tests ListObjects with prefix using httptest server.
func TestStorageAdapter_ListObjects_WithPrefix(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files with prefix
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
prefix/file1.txt
100
prefix/file2.txt
200
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, err := adapter.ListObjects(context.Background(), "prefix/")
require.NoError(t, err)
require.NotNil(t, objects)
}
// TestStorageAdapter_ListDir_WithPrefix tests ListDir with prefix using httptest server.
func TestStorageAdapter_ListDir_WithPrefix(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// List files and directories with prefix
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") &&
strings.Contains(r.URL.RawQuery, "comp=list") {
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`
prefix/subdir
prefix/file1.txt
100
Mon, 01 Jan 2024 00:00:00 GMT
`))
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
objects, prefixes, err := adapter.ListDir(context.Background(), "prefix/")
require.NoError(t, err)
require.NotNil(t, objects)
require.NotNil(t, prefixes)
}
// TestStorageAdapter_StatObject_RootDirectory tests stat for root directory using httptest server.
func TestStorageAdapter_StatObject_RootDirectory(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Root directory properties - Azure uses GET/HEAD on share with restype=directory
if (r.Method == http.MethodGet || r.Method == http.MethodHead) &&
strings.Contains(r.URL.Path, "testshare") &&
(strings.Contains(r.URL.RawQuery, "restype=directory") || r.URL.RawQuery == "") {
w.Header().Set("Content-Type", "application/x-directory")
w.Header().Set("Last-Modified", "Mon, 01 Jan 2024 00:00:00 GMT")
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
info, err := adapter.StatObject(context.Background(), "/")
require.NoError(t, err)
require.NotNil(t, info)
assert.True(t, info.IsDir)
}
// TestStorageAdapter_DeleteObject_RootDirectory tests delete for root directory using httptest server.
func TestStorageAdapter_DeleteObject_RootDirectory(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Root directory delete
if r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "testshare") &&
strings.Contains(r.URL.RawQuery, "restype=directory") {
w.WriteHeader(http.StatusAccepted)
return
}
http.NotFound(w, r)
})
srv := setupAzureTestServer(t, handler)
defer srv.Close()
shareClient, err := createTestShareClient(t, srv.URL)
require.NoError(t, err)
adapter := &storageAdapter{
cfg: &Config{ShareName: "testshare"},
shareClient: shareClient,
}
err = adapter.DeleteObject(context.Background(), "/")
require.NoError(t, err)
}
================================================
FILE: pkg/gofr/datasource/file/common_file.go
================================================
package file
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"time"
"gofr.dev/pkg/gofr/datasource"
)
// Error variables for common file operations.
var (
// errFileNotOpenForReading is returned when attempting to read from a file that wasn't opened for reading.
errFileNotOpenForReading = errors.New("file not open for reading")
// errFileNotOpenForWriting is returned when attempting to write to a file that wasn't opened for writing.
errFileNotOpenForWriting = errors.New("file not open for writing")
// errWriteAtNotSupported is returned when WriteAt is called on cloud storage.
errWriteAtNotSupported = errors.New("WriteAt not supported for cloud storage")
// errWriterNil is returned when NewWriter returns nil.
errWriterNil = errors.New("NewWriter returned nil")
)
// File permission constants.
const (
// DefaultFileMode is the standard file permission (0644 = rw-r--r--).
DefaultFileMode os.FileMode = 0644
// DefaultDirMode is the standard directory permission (0755 = rwxr-xr-x).
DefaultDirMode os.FileMode = 0755
)
// CommonFile implements FileInfo for all providers, eliminating redundant metadata getters.
// Providers instantiate this struct when returning file metadata.
type CommonFile struct {
provider StorageProvider
name string
size int64
contentType string
lastModified time.Time
isDir bool
body io.ReadCloser
writer io.WriteCloser
currentPos int64
logger datasource.Logger
metrics StorageMetrics
location string
}
// NewCommonFile creates a new file instance for reading.
func NewCommonFile(
provider StorageProvider,
name string,
info *ObjectInfo,
reader io.ReadCloser,
logger datasource.Logger,
metrics StorageMetrics,
location string,
) *CommonFile {
return &CommonFile{
provider: provider,
name: name,
size: info.Size,
contentType: info.ContentType,
lastModified: info.LastModified,
isDir: info.IsDir,
body: reader,
currentPos: 0,
logger: logger,
metrics: metrics,
location: location,
}
}
// NewCommonFileWriter creates a new file instance for writing.
func NewCommonFileWriter(
provider StorageProvider,
name string,
writer io.WriteCloser,
logger datasource.Logger,
metrics StorageMetrics,
location string,
) *CommonFile {
return &CommonFile{
provider: provider,
name: name,
writer: writer,
logger: logger,
metrics: metrics,
location: location,
}
}
// Read implements io.Reader.
func (f *CommonFile) Read(p []byte) (int, error) {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpRead, startTime, &st, &msg)
if f.body == nil {
return 0, errFileNotOpenForReading
}
n, err := f.body.Read(p)
f.currentPos += int64(n)
if err != nil && err != io.EOF {
msg = fmt.Sprintf("read failed: %v", err)
return n, err
}
st = StatusSuccess
msg = fmt.Sprintf("Read %d bytes from %q", n, f.name)
return n, err
}
// ReadAt implements io.ReaderAt.
func (f *CommonFile) ReadAt(p []byte, off int64) (int, error) {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpReadAt, startTime, &st, &msg)
if off < 0 || off >= f.size {
msg = fmt.Sprintf("offset %d out of range [0, %d]", off, f.size)
return 0, ErrOutOfRange
}
ctx := context.Background()
// Create range reader for this specific read
reader, err := f.provider.NewRangeReader(ctx, f.name, off, int64(len(p)))
if err != nil {
msg = fmt.Sprintf("failed to create range reader: %v", err)
return 0, err
}
defer reader.Close()
n, err := io.ReadFull(reader, p)
if err != nil && errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
msg = fmt.Sprintf("ReadAt failed: %v", err)
return n, err
}
st = StatusSuccess
msg = fmt.Sprintf("ReadAt %d bytes from %q at offset %d", n, f.name, off)
return n, nil
}
// Write implements io.Writer.
func (f *CommonFile) Write(p []byte) (int, error) {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpWrite, startTime, &st, &msg)
if f.writer == nil {
return 0, errFileNotOpenForWriting
}
n, err := f.writer.Write(p)
if err != nil {
msg = fmt.Sprintf("write failed: %v", err)
return n, err
}
st = StatusSuccess
msg = fmt.Sprintf("Wrote %d bytes to %q", n, f.name)
return n, nil
}
// WriteAt writes len(p) bytes from p to the file at offset off (supports local filesystem only).
func (f *CommonFile) WriteAt(p []byte, off int64) (int, error) {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpWriteAt, startTime, &st, &msg)
if f.writer == nil {
return 0, errFileNotOpenForWriting
}
// Check if writer is an *os.File (local filesystem)
if osFile, ok := f.writer.(*os.File); ok {
n, err := osFile.WriteAt(p, off)
if err != nil {
msg = fmt.Sprintf("failed to write at offset %d: %v", off, err)
return n, err
}
st = StatusSuccess
msg = fmt.Sprintf("WriteAt %d bytes to %q at offset %d", n, f.name, off)
return n, nil
}
// Cloud storage doesn't support WriteAt
return 0, errWriteAtNotSupported
}
// Seek implements io.Seeker.
func (f *CommonFile) Seek(offset int64, whence int) (int64, error) {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpSeek, startTime, &st, &msg)
// Calculate new position
newPos, err := ValidateSeekOffset(whence, offset, f.currentPos, f.size)
if err != nil {
msg = fmt.Sprintf("invalid seek offset: %v", err)
return 0, err
}
ctx := context.Background()
// Close old reader
if f.body != nil {
if closeErr := f.body.Close(); closeErr != nil {
msg = fmt.Sprintf("failed to close old reader: %v", closeErr)
}
}
// Create new range reader from new position
reader, err := f.provider.NewRangeReader(ctx, f.name, newPos, -1)
if err != nil {
msg = fmt.Sprintf("failed to create range reader: %v", err)
return 0, err
}
f.body = reader
f.currentPos = newPos
st = StatusSuccess
msg = fmt.Sprintf("Sought to position %d in %q", newPos, f.name)
return newPos, nil
}
// Close implements io.Closer.
func (f *CommonFile) Close() error {
var msg string
st := StatusError
startTime := time.Now()
defer f.observe(OpClose, startTime, &st, &msg)
var errs []error
// Close reader
if f.body != nil {
if err := f.body.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close reader: %w", err))
}
f.body = nil
}
// Close writer
if f.writer != nil {
if err := f.writer.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close writer: %w", err))
}
f.writer = nil
}
if len(errs) > 0 {
msg = fmt.Sprintf("close failed: %v", errs)
return errors.Join(errs...)
}
st = StatusSuccess
msg = fmt.Sprintf("Closed %q successfully", f.name)
return nil
}
// Name returns the base name of the file.
func (f *CommonFile) Name() string {
return f.name
}
// Size returns the file size in bytes. Returns 0 for directories.
func (f *CommonFile) Size() int64 {
return f.size
}
// ModTime returns the last modification time.
func (f *CommonFile) ModTime() time.Time {
return f.lastModified
}
// IsDir returns true if the object is a directory.
// Checks both explicit isDir flag and content type for compatibility.
func (f *CommonFile) IsDir() bool {
return f.isDir || f.contentType == "application/x-directory"
}
// Mode returns the file mode bits.
func (f *CommonFile) Mode() os.FileMode {
if f.isDir {
return DefaultDirMode
}
return DefaultFileMode
}
// ReadAll returns a reader for JSON/CSV files.
func (f *CommonFile) ReadAll() (RowReader, error) {
if f.body == nil {
return nil, errFileNotOpenForReading
}
if strings.HasSuffix(f.name, ".json") {
return NewJSONReader(f.body, f.logger)
}
// Default to text/CSV reader
return NewTextReader(f.body, f.logger), nil
}
// Sys returns nil (no underlying system-specific data for cloud storage).
func (*CommonFile) Sys() any {
return nil
}
// observe records metrics and logs for file operations.
func (f *CommonFile) observe(operation string, startTime time.Time, status, message *string) {
ObserveOperation(&OperationObservability{
Context: context.Background(),
Logger: f.logger,
Metrics: f.metrics,
Operation: operation,
Location: f.location,
Provider: "FILE",
StartTime: startTime,
Status: status,
Message: message,
})
}
================================================
FILE: pkg/gofr/datasource/file/common_file_test.go
================================================
package file
import (
"bytes"
"io"
"io/fs"
"os"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestCommonFile_Name(t *testing.T) {
tests := []struct {
name string
fileName string
}{
{
name: "simple file name",
fileName: "test.txt",
},
{
name: "file with path",
fileName: "path/to/file.txt",
},
{
name: "empty name",
fileName: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &CommonFile{name: tt.fileName}
assert.Equal(t, tt.fileName, f.Name())
})
}
}
func TestCommonFile_Size(t *testing.T) {
tests := []struct {
name string
size int64
expectedSize int64
}{
{
name: "zero size",
size: 0,
expectedSize: 0,
},
{
name: "small file",
size: 1024,
expectedSize: 1024,
},
{
name: "large file",
size: 1073741824, // 1GB
expectedSize: 1073741824,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &CommonFile{size: tt.size}
assert.Equal(t, tt.expectedSize, f.Size())
})
}
}
func TestCommonFile_ModTime(t *testing.T) {
now := time.Now()
past := now.Add(-24 * time.Hour)
tests := []struct {
name string
lastModified time.Time
}{
{
name: "current time",
lastModified: now,
},
{
name: "past time",
lastModified: past,
},
{
name: "zero time",
lastModified: time.Time{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &CommonFile{lastModified: tt.lastModified}
assert.Equal(t, tt.lastModified, f.ModTime())
})
}
}
func TestCommonFile_IsDir(t *testing.T) {
tests := []struct {
name string
isDir bool
contentType string
expected bool
}{
{
name: "explicit directory flag",
isDir: true,
expected: true,
},
{
name: "explicit file flag",
isDir: false,
expected: false,
},
{
name: "directory content type",
isDir: false,
contentType: "application/x-directory",
expected: true,
},
{
name: "file content type",
isDir: false,
contentType: "text/plain",
expected: false,
},
{
name: "both directory indicators",
isDir: true,
contentType: "application/x-directory",
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &CommonFile{
isDir: tt.isDir,
contentType: tt.contentType,
}
assert.Equal(t, tt.expected, f.IsDir())
})
}
}
func TestCommonFile_Mode(t *testing.T) {
tests := []struct {
name string
isDir bool
contentType string
expectedMode fs.FileMode
}{
{
name: "directory mode",
isDir: true,
expectedMode: DefaultDirMode,
},
{
name: "file mode",
isDir: false,
expectedMode: DefaultFileMode,
},
{
name: "directory by content type",
contentType: "application/x-directory",
expectedMode: DefaultFileMode,
},
{
name: "regular file content type",
contentType: "text/plain",
expectedMode: DefaultFileMode,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := &CommonFile{
isDir: tt.isDir,
contentType: tt.contentType,
}
assert.Equal(t, tt.expectedMode, f.Mode())
})
}
}
func TestCommonFile_Sys(t *testing.T) {
f := &CommonFile{
name: "test.txt",
size: 100,
isDir: false,
}
result := f.Sys()
assert.Nil(t, result, "Sys() should always return nil for cloud storage")
}
func TestCommonFile_CompleteFileInfo(t *testing.T) {
now := time.Now()
f := &CommonFile{
name: "document.pdf",
size: 2048,
contentType: "application/pdf",
lastModified: now,
isDir: false,
}
assert.Equal(t, "document.pdf", f.Name())
assert.Equal(t, int64(2048), f.Size())
assert.Equal(t, now, f.ModTime())
assert.False(t, f.IsDir())
assert.Equal(t, DefaultFileMode, f.Mode())
assert.Nil(t, f.Sys())
}
func TestCommonFile_CompleteDirectoryInfo(t *testing.T) {
now := time.Now()
f := &CommonFile{
name: "mydir",
size: 0,
contentType: "application/x-directory",
lastModified: now,
isDir: true,
}
assert.Equal(t, "mydir", f.Name())
assert.Equal(t, int64(0), f.Size())
assert.Equal(t, now, f.ModTime())
assert.True(t, f.IsDir())
assert.Equal(t, DefaultDirMode, f.Mode())
assert.Nil(t, f.Sys())
}
func TestCommonFile_Read_SuccessAndNoBody(t *testing.T) {
f := &CommonFile{body: io.NopCloser(strings.NewReader("hello")), name: "r.txt"}
buf := make([]byte, 10)
n, err := f.Read(buf)
assert.Equal(t, 5, n)
require.NoError(t, err)
// No body
f2 := &CommonFile{name: "no-body"}
n2, err2 := f2.Read(buf)
assert.Equal(t, 0, n2)
assert.Equal(t, errFileNotOpenForReading, err2)
}
func TestCommonFile_ReadAt_Various(t *testing.T) {
ctrl, mockProvider, _ := setupCommonFS(t)
defer ctrl.Finish()
// Success: provider.NewRangeReader returns a reader that has enough bytes.
f := &CommonFile{
provider: mockProvider,
name: "file.bin",
size: 100,
}
p := make([]byte, 4)
mockProvider.EXPECT().
NewRangeReader(gomock.Any(), "file.bin", int64(2), int64(len(p))).
Return(io.NopCloser(bytes.NewReader([]byte("abcdefgh"))), nil)
n, err := f.ReadAt(p, 2)
assert.Equal(t, 4, n)
require.NoError(t, err)
// Offset out of range
f2 := &CommonFile{size: 5, name: "oob.bin"}
_, err2 := f2.ReadAt(make([]byte, 2), 5)
assert.Equal(t, ErrOutOfRange, err2)
// NewRangeReader returns error.
f3 := &CommonFile{
provider: mockProvider,
name: "err.bin",
size: 10,
}
mockProvider.EXPECT().
NewRangeReader(gomock.Any(), "err.bin", int64(1), int64(2)).
Return(nil, errTest)
_, err3 := f3.ReadAt(make([]byte, 2), 1)
assert.Equal(t, errTest, err3)
}
func TestCommonFile_Seek_SuccessAndProviderError(t *testing.T) {
ctrl, mockProvider, _ := setupCommonFS(t)
defer ctrl.Finish()
// Success: seek to start.
reader := io.NopCloser(strings.NewReader("content"))
mockProvider.EXPECT().
NewRangeReader(gomock.Any(), "seekfile", int64(0), int64(-1)).
Return(reader, nil)
f := &CommonFile{
provider: mockProvider,
name: "seekfile",
currentPos: 5,
size: 100,
}
pos, err := f.Seek(0, io.SeekStart)
require.NoError(t, err)
assert.Equal(t, int64(0), pos)
assert.Equal(t, int64(0), f.currentPos)
// Provider NewRangeReader error.
mockProvider.EXPECT().
NewRangeReader(gomock.Any(), "badseek", int64(1), int64(-1)).
Return(nil, errTest)
f2 := &CommonFile{
provider: mockProvider,
name: "badseek",
currentPos: 0,
size: 10,
}
_, err2 := f2.Seek(1, io.SeekStart)
assert.Equal(t, errTest, err2)
}
func TestCommonFile_ReadAll_NoBody_JSON_Text(t *testing.T) {
// No body -> error.
f := &CommonFile{name: "no", body: nil}
_, err := f.ReadAll()
assert.Equal(t, errFileNotOpenForReading, err)
// JSON path.
bodyJSON := io.NopCloser(strings.NewReader(`[{"a":1}]`))
fj := &CommonFile{name: "data.json", body: bodyJSON, logger: nil}
rr, errj := fj.ReadAll()
require.NoError(t, errj)
assert.NotNil(t, rr)
// Text/CSV path.
bodyTxt := io.NopCloser(strings.NewReader("a,b\n1,2\n"))
ft := &CommonFile{name: "data.txt", body: bodyTxt, logger: nil}
r2, errt := ft.ReadAll()
require.NoError(t, errt)
assert.NotNil(t, r2)
}
// simple in-memory WriteCloser used by tests.
type bufWriteCloser struct {
bytes.Buffer
}
func (*bufWriteCloser) Close() error { return nil }
// ReadCloser whose Close returns an error (used to test Close error accumulation).
type badReadCloser struct{}
func (badReadCloser) Read([]byte) (int, error) { return 0, io.EOF }
func (badReadCloser) Close() error { return errTest }
// WriteCloser whose Close returns an error (used to test Close error accumulation).
type badWriteCloser struct{}
func (badWriteCloser) Write([]byte) (int, error) { return 0, nil }
func (badWriteCloser) Close() error { return errTest }
func TestCommonFile_Write_SuccessAndNoWriter(t *testing.T) {
// Success with simple in-memory WriteCloser.
bw := &bufWriteCloser{}
f := &CommonFile{writer: bw, name: "w.txt"}
n, err := f.Write([]byte("abc"))
require.NoError(t, err)
assert.Equal(t, 3, n)
// No writer
f2 := &CommonFile{name: "nopen"}
_, err2 := f2.Write([]byte("x"))
assert.Equal(t, errFileNotOpenForWriting, err2)
}
func TestCommonFile_WriteAt_LocalAndUnsupported(t *testing.T) {
f := &CommonFile{writer: &bufWriteCloser{}}
_, err := f.WriteAt([]byte("x"), 0)
assert.Equal(t, errWriteAtNotSupported, err)
tmp, err := os.CreateTemp(t.TempDir(), "writetest_*")
require.NoError(t, err)
defer os.Remove(tmp.Name())
defer tmp.Close()
f2 := &CommonFile{writer: tmp, name: "local"}
n, err2 := f2.WriteAt([]byte("xyz"), 0)
require.NoError(t, err2)
assert.Equal(t, 3, n)
}
func TestCommonFile_Close_SuccessAndErrors(t *testing.T) {
// Success case: both close fine.
body := io.NopCloser(bytes.NewReader([]byte("x")))
w := &bufWriteCloser{}
f := &CommonFile{body: body, writer: w, name: "cfile"}
err := f.Close()
require.NoError(t, err)
assert.Nil(t, f.body)
assert.Nil(t, f.writer)
// Both Close return errors -> returned error should wrap errTest
f2 := &CommonFile{
body: badReadCloser{},
writer: badWriteCloser{},
name: "cerr",
}
err2 := f2.Close()
require.Error(t, err2)
assert.ErrorIs(t, err2, errTest)
}
================================================
FILE: pkg/gofr/datasource/file/common_fs.go
================================================
package file
import (
"context"
"errors"
"fmt"
"os"
"path"
"strings"
"sync"
"time"
"gofr.dev/pkg/gofr/datasource"
)
var (
// ErrEmptyDirectoryName is returned when attempting to create a directory with an empty name.
errEmptyDirectoryName = errors.New("directory name cannot be empty")
// ErrChDirNotSupported is returned when changing directory is attempted on cloud storage.
errChDirNotSupported = errors.New("changing directory is not supported in cloud storage")
errUnsupportedFlags = errors.New("unsupported flag combination for OpenFile")
errProviderNil = errors.New("storage provider is not configured")
)
// CommonFileSystem provides shared implementations of FileSystem operations.
// Providers (GCS, S3, FTP, SFTP) embed this struct to inherit common directory operations,
// metadata handling, and observability patterns.
//
// Providers override only storage-specific methods like Create, Open, Remove, Rename.
//
// Note: This works with StorageProvider interface for cloud operations.
// For reading JSON/CSV files from cloud storage, providers should use:
// - file.NewTextReader(reader) for text/CSV files
// - file.NewJSONReader(reader) for JSON files
//
// These are separate from the local filesystem's ReadAll() implementation.
type CommonFileSystem struct {
Provider StorageProvider // Underlying storage implementation
Location string // Bucket name or connection identifier (e.g., "my-bucket", "ftp://host")
Logger datasource.Logger // Logger for operation tracking
Metrics StorageMetrics // Metrics for observability
registerHistogram sync.Once
connected bool
disableRetry bool
ProviderName string // Provider name for observability (e.g., "Azure", "GCS", "S3")
}
// Connect calls the provider's Connect and performs common bookkeeping (metrics / logs / observe).
func (c *CommonFileSystem) Connect(ctx context.Context) error {
start := time.Now()
st := StatusError
msg := ""
defer c.Observe(OpConnect, start, &st, &msg)
c.registerHistogram.Do(func() {
if c.Metrics != nil {
c.Metrics.NewHistogram(AppFileStats, "App File Stats - duration of file operations",
DefaultHistogramBuckets()...)
}
})
if c.Provider == nil {
return errProviderNil
}
// already connected fast-path
if c.connected {
st = StatusSuccess
msg = "already connected"
return nil
}
// delegate provider-specific connect
if err := c.Provider.Connect(ctx); err != nil {
return err
}
// success bookkeeping
c.connected = true
st = StatusSuccess
msg = "connected"
if c.Logger != nil {
c.Logger.Infof("connected to %s", c.Location)
}
return nil
}
// UseLogger sets the logger for the CommonFileSystem.
func (c *CommonFileSystem) UseLogger(logger any) {
if l, ok := logger.(datasource.Logger); ok {
c.Logger = l
}
}
// UseMetrics sets the metrics interface for the CommonFileSystem.
func (c *CommonFileSystem) UseMetrics(metrics any) {
if m, ok := metrics.(StorageMetrics); ok {
c.Metrics = m
}
}
// Mkdir creates a directory in cloud storage by creating a zero-byte object with "/" suffix.
// This follows cloud storage conventions where directories are represented as special markers.
func (c *CommonFileSystem) Mkdir(name string, _ os.FileMode) error {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpMkdir, startTime, &st, &msg)
if name == "" {
msg = "directory name cannot be empty"
return errEmptyDirectoryName
}
ctx := context.Background()
// Ensure directory marker ends with "/"
objName := name
if !strings.HasSuffix(objName, "/") {
objName += "/"
}
if _, err := c.Provider.StatObject(ctx, objName); err == nil {
st = StatusSuccess
msg = fmt.Sprintf("Directory %q already exists", name)
return nil
}
// Create empty object to represent directory
writer := c.Provider.NewWriter(ctx, objName)
if writer == nil {
return errWriterNil
}
defer writer.Close()
// Write minimal content for directory marker
_, err := writer.Write([]byte(""))
if err != nil {
if strings.Contains(err.Error(), "is a directory") {
st = StatusSuccess
msg = fmt.Sprintf("Directory %q already exists", name)
return nil
}
msg = fmt.Sprintf("failed to write directory marker: %v", err)
return err
}
if err := writer.Close(); err != nil {
msg = fmt.Sprintf("failed to close writer: %v", err)
return err
}
st = StatusSuccess
msg = fmt.Sprintf("Directory %q created successfully", name)
return nil
}
// MkdirAll creates nested directories by recursively calling Mkdir for each path component.
// Example: "a/b/c" creates "a/", "a/b/", and "a/b/c/".
// MkdirAll creates nested directories by recursively calling Mkdir for each path component.
func (c *CommonFileSystem) MkdirAll(dirPath string, perm os.FileMode) error {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpMkdirAll, startTime, &st, &msg)
if dirPath == "" {
msg = "directory path cannot be empty"
return errEmptyDirectoryName
}
// Split and filter path components
components := c.getPathComponents(dirPath)
// Create each directory in the path
currentPath := ""
for _, component := range components {
currentPath = path.Join(currentPath, component)
if err := c.Mkdir(currentPath, perm); err != nil && !os.IsExist(err) {
msg = fmt.Sprintf("failed to create %q: %v", currentPath, err)
return err
}
}
st = StatusSuccess
msg = fmt.Sprintf("Created directory %q successfully", dirPath)
return nil
}
// getPathComponents splits a path and filters out empty, ".", and ".." components.
func (*CommonFileSystem) getPathComponents(dirPath string) []string {
parts := strings.Split(strings.Trim(dirPath, "/"), "/")
components := make([]string, 0, len(parts))
for _, part := range parts {
if part != "" && part != "." && part != ".." {
components = append(components, part)
}
}
return components
}
// RemoveAll deletes a directory and all its contents by listing all objects with the prefix
// and deleting them individually.
func (c *CommonFileSystem) RemoveAll(dirPath string) error {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpRemoveAll, startTime, &st, &msg)
ctx := context.Background()
// List all objects under this directory
objects, err := c.Provider.ListObjects(ctx, dirPath)
if err != nil {
msg = fmt.Sprintf("failed to list objects: %v", err)
return err
}
// Delete each object
for _, obj := range objects {
if err := c.Provider.DeleteObject(ctx, obj); err != nil {
msg = fmt.Sprintf("failed to delete %q: %v", obj, err)
return err
}
}
st = StatusSuccess
msg = fmt.Sprintf("Directory %q and all contents deleted successfully", dirPath)
return nil
}
// ReadDir lists the contents of a directory, returning both subdirectories (prefixes) and files (objects).
func (c *CommonFileSystem) ReadDir(dir string) ([]FileInfo, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpReadDir, startTime, &st, &msg)
ctx := context.Background()
// List with delimiter to get immediate children only
objects, prefixes, err := c.Provider.ListDir(ctx, dir)
if err != nil {
msg = fmt.Sprintf("failed to list directory: %v", err)
return nil, err
}
fileInfos := make([]FileInfo, 0, len(prefixes)+len(objects))
// Add subdirectories (prefixes represent nested directories)
for _, p := range prefixes {
trimmedName := strings.TrimSuffix(p, "/")
dirName := path.Base(trimmedName)
fileInfos = append(fileInfos, &CommonFile{
name: dirName,
isDir: true,
})
}
// Add files (objects)
for _, o := range objects {
// Skip directory markers themselves
if strings.HasSuffix(o.Name, "/") {
continue
}
fileInfos = append(fileInfos, &CommonFile{
name: path.Base(o.Name),
size: o.Size,
contentType: o.ContentType,
lastModified: o.LastModified,
isDir: o.IsDir,
})
}
st = StatusSuccess
msg = fmt.Sprintf("ReadDir %q successful (%d items)", dir, len(fileInfos))
return fileInfos, nil
}
// Stat returns file/directory metadata by querying the storage provider.
func (c *CommonFileSystem) Stat(name string) (FileInfo, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpStat, startTime, &st, &msg)
ctx := context.Background()
// Try to get object metadata
info, err := c.Provider.StatObject(ctx, name)
if err == nil {
st = StatusSuccess
msg = fmt.Sprintf("Stat %q successful", name)
return &CommonFile{
name: name,
size: info.Size,
contentType: info.ContentType,
lastModified: info.LastModified,
isDir: info.IsDir,
}, nil
}
// If not found as file, check if it's a directory by listing with prefix
prefix := name
if !strings.HasSuffix(prefix, "/") {
prefix += "/"
}
objects, _, listErr := c.Provider.ListDir(ctx, prefix)
if listErr != nil {
msg = fmt.Sprintf("failed to stat %q: %v", name, listErr)
return nil, listErr
}
if len(objects) > 0 {
st = StatusSuccess
msg = fmt.Sprintf("Stat %q successful (directory)", name)
return &CommonFile{
name: name,
size: 0,
contentType: "application/x-directory",
lastModified: objects[0].LastModified,
isDir: true,
}, nil
}
msg = fmt.Sprintf("file or directory %q not found", name)
return nil, ErrFileNotFound
}
// ChDir is not supported for cloud storage (no concept of "current directory").
func (c *CommonFileSystem) ChDir(_ string) error {
st := StatusError
msg := "ChDir not supported in cloud storage"
startTime := time.Now()
defer c.Observe(OpChDir, startTime, &st, &msg)
return errChDirNotSupported
}
// Getwd returns the configured location (bucket name or connection identifier).
func (c *CommonFileSystem) Getwd() (string, error) {
st := StatusSuccess
msg := "Returning location"
startTime := time.Now()
defer c.Observe(OpGetwd, startTime, &st, &msg)
return c.Location, nil
}
// ============= File Operations (NEW) =============
// Create creates a new file for writing.
func (c *CommonFileSystem) Create(name string) (File, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpCreate, startTime, &st, &msg)
ctx := context.Background()
// Create writer
writer := c.Provider.NewWriter(ctx, name)
st = StatusSuccess
msg = fmt.Sprintf("Created %q for writing", name)
return NewCommonFileWriter(
c.Provider,
name,
writer,
c.Logger,
c.Metrics,
c.Location,
), nil
}
// Open opens a file for reading.
func (c *CommonFileSystem) Open(name string) (File, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpOpen, startTime, &st, &msg)
ctx := context.Background()
// Get metadata first (efficient HEAD request)
info, err := c.Provider.StatObject(ctx, name)
if err != nil {
msg = fmt.Sprintf("file %q not found: %v", name, err)
return nil, ErrFileNotFound
}
// Create reader
reader, err := c.Provider.NewReader(ctx, name)
if err != nil {
msg = fmt.Sprintf("failed to open %q: %v", name, err)
return nil, err
}
st = StatusSuccess
msg = fmt.Sprintf("Opened %q for reading", name)
return NewCommonFile(
c.Provider,
name,
info,
reader,
c.Logger,
c.Metrics,
c.Location,
), nil
}
// OpenFile opens a file with the specified flags and permissions.
func (c *CommonFileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpOpenFile, startTime, &st, &msg)
// Try different strategies based on flags
file, err := c.tryOpenStrategies(name, flag, perm)
if err != nil {
msg = fmt.Sprintf("failed to open %q: %v", name, err)
return nil, err
}
st = StatusSuccess
msg = fmt.Sprintf("Opened %q with flags %d", name, flag)
return file, nil
}
// tryOpenStrategies attempts different strategies to open a file based on flags.
func (c *CommonFileSystem) tryOpenStrategies(name string, flag int, perm os.FileMode) (File, error) {
// Strategy 1: Try local filesystem for O_RDWR
if flag&os.O_RDWR != 0 {
if localFile, err := c.tryLocalFileOpen(name, flag, perm); err == nil {
return localFile, nil
}
}
// Strategy 2: Handle read-only flags
if flag&(os.O_WRONLY|os.O_RDWR) == 0 {
return c.Open(name)
}
// Strategy 3: Handle write flags
return c.handleWriteFlags(name, flag)
}
// tryLocalFileOpen attempts to open a file using os.OpenFile (for local filesystem).
func (c *CommonFileSystem) tryLocalFileOpen(name string, flag int, perm os.FileMode) (File, error) {
osFile, err := os.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
info, err := osFile.Stat()
if err != nil {
osFile.Close()
return nil, err
}
return &CommonFile{
name: name,
body: osFile,
writer: osFile,
size: info.Size(),
logger: c.Logger,
metrics: c.Metrics,
location: c.Location,
}, nil
}
// handleWriteFlags handles O_WRONLY, O_APPEND, O_CREATE, O_TRUNC flags.
func (c *CommonFileSystem) handleWriteFlags(name string, flag int) (File, error) {
// O_CREATE or O_TRUNC: create new file
if flag&(os.O_CREATE|os.O_TRUNC) != 0 {
return c.Create(name)
}
// O_APPEND: open existing for append
if flag&os.O_APPEND != 0 {
ctx := context.Background()
writer := c.Provider.NewWriter(ctx, name)
if writer == nil {
return nil, errWriterNil
}
return &CommonFile{
name: name,
writer: writer,
logger: c.Logger,
metrics: c.Metrics,
location: c.Location,
}, nil
}
return nil, errUnsupportedFlags
}
// Remove deletes a file.
func (c *CommonFileSystem) Remove(name string) error {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpRemove, startTime, &st, &msg)
ctx := context.Background()
if err := c.Provider.DeleteObject(ctx, name); err != nil {
msg = fmt.Sprintf("failed to remove %q: %v", name, err)
return err
}
st = StatusSuccess
msg = fmt.Sprintf("Removed %q successfully", name)
return nil
}
// Rename renames a file (copy + delete).
func (c *CommonFileSystem) Rename(oldname, newname string) error {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpRename, startTime, &st, &msg)
ctx := context.Background()
// Copy to new location
if err := c.Provider.CopyObject(ctx, oldname, newname); err != nil {
msg = fmt.Sprintf("failed to copy %q to %q: %v", oldname, newname, err)
return err
}
// Delete old
if err := c.Provider.DeleteObject(ctx, oldname); err != nil {
msg = fmt.Sprintf("failed to delete old file %q: %v", oldname, err)
return err
}
st = StatusSuccess
msg = fmt.Sprintf("Renamed %q to %q successfully", oldname, newname)
return nil
}
// getProviderName returns the provider name for observability.
// If ProviderName is set, it returns that; otherwise returns "Common".
func (c *CommonFileSystem) getProviderName() string {
if c.ProviderName != "" {
return c.ProviderName
}
return "COMMON"
}
// Observe is a helper method to centralize observability for all operations.
func (c *CommonFileSystem) Observe(operation string, startTime time.Time, status, message *string) {
ObserveOperation(&OperationObservability{
Context: context.Background(),
Logger: c.Logger,
Metrics: c.Metrics,
Operation: operation,
Location: c.Location,
Provider: c.getProviderName(),
StartTime: startTime,
Status: status,
Message: message,
})
}
func (c *CommonFileSystem) IsConnected() bool {
return c.connected
}
// SetDisableRetry enables or disables background retry behavior.
func (c *CommonFileSystem) SetDisableRetry(disable bool) {
c.disableRetry = disable
}
// IsRetryDisabled returns true if background retry is disabled.
func (c *CommonFileSystem) IsRetryDisabled() bool {
return c.disableRetry
}
// SetConnected manually sets the connected state (for testing).
func (c *CommonFileSystem) SetConnected(connected bool) {
c.connected = connected
}
// CreateWithOptions creates a file with optional metadata (content-type, content-disposition,
// custom key-value pairs). If the underlying provider does not implement MetadataWriter,
// the file is created without metadata and a warning is logged.
func (c *CommonFileSystem) CreateWithOptions(ctx context.Context, name string, opts *FileOptions) (File, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpCreateWithOptions, startTime, &st, &msg)
// Try metadata-aware writer
if mw, ok := c.Provider.(MetadataWriter); ok {
writer := mw.NewWriterWithOptions(ctx, name, opts)
if writer == nil {
msg = "failed to create writer with options"
return nil, errWriterNil
}
st = StatusSuccess
msg = fmt.Sprintf("Created %q with metadata", name)
return NewCommonFileWriter(c.Provider, name, writer, c.Logger, c.Metrics, c.Location), nil
}
// Fallback: provider doesn't support metadata. Warn because the caller explicitly
// requested metadata that will be silently dropped.
if c.Logger != nil && opts != nil {
c.Logger.Warnf("provider %s does not support metadata; file %q will be created without the requested metadata", c.ProviderName, name)
}
writer := c.Provider.NewWriter(ctx, name)
if writer == nil {
msg = "failed to create writer"
return nil, errWriterNil
}
st = StatusSuccess
msg = fmt.Sprintf("Created %q (metadata not supported)", name)
return NewCommonFileWriter(c.Provider, name, writer, c.Logger, c.Metrics, c.Location), nil
}
// GenerateSignedURL produces a time-limited pre-signed URL for the named object.
// Returns ErrSignedURLsNotSupported if the underlying provider does not implement SignedURLProvider.
func (c *CommonFileSystem) GenerateSignedURL(ctx context.Context, name string, expiry time.Duration, opts *FileOptions) (string, error) {
var msg string
st := StatusError
startTime := time.Now()
defer c.Observe(OpSignedURL, startTime, &st, &msg)
signer, ok := c.Provider.(SignedURLProvider)
if !ok {
msg = fmt.Sprintf("provider %s does not support signed URLs", c.ProviderName)
return "", fmt.Errorf("%w: %s", ErrSignedURLsNotSupported, c.ProviderName)
}
url, err := signer.SignedURL(ctx, name, expiry, opts)
if err != nil {
msg = fmt.Sprintf("failed to generate signed URL: %v", err)
return "", fmt.Errorf("failed to generate signed URL for %q: %w", name, err)
}
st = StatusSuccess
msg = fmt.Sprintf("Generated signed URL for %q (expires in %v)", name, expiry)
return url, nil
}
================================================
FILE: pkg/gofr/datasource/file/common_fs_signedurl_test.go
================================================
package file_test
import (
"context"
"errors"
"io"
"testing"
"time"
"gofr.dev/pkg/gofr/datasource/file"
)
var (
errNotImplemented = errors.New("not implemented")
errSigningFailed = errors.New("signing failed")
)
// basicProviderWithoutSignedURL implements only StorageProvider, NOT SignedURLProvider.
type basicProviderWithoutSignedURL struct{}
func (*basicProviderWithoutSignedURL) Connect(_ context.Context) error { return nil }
func (*basicProviderWithoutSignedURL) NewReader(_ context.Context, _ string) (io.ReadCloser, error) {
return nil, errNotImplemented
}
func (*basicProviderWithoutSignedURL) NewRangeReader(_ context.Context, _ string, _, _ int64) (io.ReadCloser, error) {
return nil, errNotImplemented
}
func (*basicProviderWithoutSignedURL) NewWriter(_ context.Context, _ string) io.WriteCloser {
return &nopWriteCloser{}
}
func (*basicProviderWithoutSignedURL) DeleteObject(_ context.Context, _ string) error { return nil }
func (*basicProviderWithoutSignedURL) CopyObject(_ context.Context, _, _ string) error {
return nil
}
func (*basicProviderWithoutSignedURL) StatObject(_ context.Context, name string) (*file.ObjectInfo, error) {
return &file.ObjectInfo{Name: name, Size: 10}, nil
}
func (*basicProviderWithoutSignedURL) ListObjects(_ context.Context, _ string) ([]string, error) {
return nil, nil
}
func (*basicProviderWithoutSignedURL) ListDir(_ context.Context, _ string) ([]file.ObjectInfo, []string, error) {
return nil, nil, nil
}
// fakeProvider implements the subset of StorageProvider + SignedURLProvider + MetadataWriter
// so we can test CommonFileSystem behavior without real GCS.
type fakeProvider struct {
calledNewWriterWithOptions bool
calledSignedURL bool
signedURLError error
lastOpts *file.FileOptions
returnNilWriter bool
}
func (*fakeProvider) Connect(_ context.Context) error {
return nil
}
func (*fakeProvider) NewReader(_ context.Context, _ string) (io.ReadCloser, error) {
return nil, errNotImplemented
}
func (*fakeProvider) NewRangeReader(_ context.Context, _ string, _, _ int64) (io.ReadCloser, error) {
return nil, errNotImplemented
}
func (*fakeProvider) NewWriter(_ context.Context, _ string) io.WriteCloser {
return &nopWriteCloser{}
}
func (*fakeProvider) DeleteObject(_ context.Context, _ string) error { return nil }
func (*fakeProvider) CopyObject(_ context.Context, _, _ string) error { return nil }
func (*fakeProvider) StatObject(_ context.Context, name string) (*file.ObjectInfo, error) {
return &file.ObjectInfo{Name: name, Size: 10}, nil
}
func (*fakeProvider) ListObjects(_ context.Context, _ string) ([]string, error) { return nil, nil }
func (*fakeProvider) ListDir(_ context.Context, _ string) ([]file.ObjectInfo, []string, error) {
return nil, nil, nil
}
// MetadataWriter.
func (f *fakeProvider) NewWriterWithOptions(_ context.Context, _ string, opts *file.FileOptions) io.WriteCloser {
f.calledNewWriterWithOptions = true
f.lastOpts = opts
if f.returnNilWriter {
return nil
}
return &nopWriteCloser{}
}
func (f *fakeProvider) SignedURL(_ context.Context, _ string, _ time.Duration, _ *file.FileOptions) (string, error) {
f.calledSignedURL = true
if f.signedURLError != nil {
return "", f.signedURLError
}
return "https://signed.example/obj", nil
}
// nopWriteCloser is a simple write closer used for tests.
type nopWriteCloser struct{}
func (*nopWriteCloser) Write([]byte) (int, error) { return 0, nil }
func (*nopWriteCloser) Close() error { return nil }
func TestCreateWithOptions_UsesProviderNewWriterWithOptions(t *testing.T) {
fp := &fakeProvider{}
cfs := &file.CommonFileSystem{Provider: fp, Location: "b", ProviderName: "FAKE"}
opts := &file.FileOptions{ContentType: "text/csv"}
_, err := cfs.CreateWithOptions(context.Background(), "obj", opts)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !fp.calledNewWriterWithOptions {
t.Error("expected NewWriterWithOptions to be called")
}
if fp.lastOpts.ContentType != "text/csv" {
t.Errorf("expected ContentType 'text/csv', got %q", fp.lastOpts.ContentType)
}
}
func TestGenerateSignedURL_DelegatesToProvider(t *testing.T) {
fp := &fakeProvider{}
cfs := &file.CommonFileSystem{Provider: fp, Location: "b", ProviderName: "FAKE"}
signed, err := cfs.GenerateSignedURL(context.Background(), "obj", time.Hour, &file.FileOptions{})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if signed != "https://signed.example/obj" {
t.Fatalf("unexpected signed url: %s", signed)
}
}
func TestGenerateSignedURL_ProviderDoesNotSupport(t *testing.T) {
basicProvider := &basicProviderWithoutSignedURL{} // Doesn't implement SignedURLProvider
cfs := &file.CommonFileSystem{Provider: basicProvider, ProviderName: "BASIC"}
_, err := cfs.GenerateSignedURL(context.Background(), "obj", time.Hour, nil)
if !errors.Is(err, file.ErrSignedURLsNotSupported) {
t.Errorf("expected ErrSignedURLsNotSupported, got %v", err)
}
}
func TestGenerateSignedURL_ProviderError(t *testing.T) {
fp := &fakeProvider{signedURLError: errSigningFailed}
cfs := &file.CommonFileSystem{Provider: fp, ProviderName: "FAKE"}
_, err := cfs.GenerateSignedURL(context.Background(), "obj", time.Hour, nil)
if err == nil {
t.Fatal("expected error when provider returns signing error, got nil")
}
// GenerateSignedURL wraps the provider error; the original sentinel must be reachable.
if !errors.Is(err, errSigningFailed) {
t.Errorf("expected error to wrap %v, got: %v", errSigningFailed, err)
}
}
func TestCreateWithOptions_NilWriterReturnsError(t *testing.T) {
fp := &fakeProvider{returnNilWriter: true}
cfs := &file.CommonFileSystem{Provider: fp, Location: "b", ProviderName: "FAKE"}
_, err := cfs.CreateWithOptions(context.Background(), "obj", nil)
if err == nil {
t.Fatal("expected error when NewWriterWithOptions returns nil, got nil")
}
}
================================================
FILE: pkg/gofr/datasource/file/common_fs_test.go
================================================
package file
import (
"bytes"
"fmt"
"io"
"os"
"reflect"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/api/googleapi"
)
var errTest = fmt.Errorf("test error")
// setupCommonFS is a helper function to set up common test dependencies.
func setupCommonFS(t *testing.T) (*gomock.Controller, *MockStorageProvider, *CommonFileSystem) {
t.Helper()
ctrl := gomock.NewController(t)
mockProvider := NewMockStorageProvider(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Set up common expectations that all operations use
mockLogger.EXPECT().
Debug(gomock.Any()).
AnyTimes()
mockMetrics.EXPECT().
RecordHistogram(gomock.Any(), AppFileStats, gomock.Any(), gomock.Any()).
AnyTimes()
fs := &CommonFileSystem{
Provider: mockProvider,
Location: "test-bucket",
Logger: mockLogger,
Metrics: mockMetrics,
}
return ctrl, mockProvider, fs
}
func TestCommonFileSystem_Mkdir(t *testing.T) {
tests := []struct {
name string
dirName string
writeErr error
closeErr error
expectError bool
expectedErr error
}{
{
name: "successful directory creation",
dirName: "testdir",
expectError: false,
},
{
name: "directory with trailing slash",
dirName: "testdir/",
expectError: false,
},
{
name: "nested directory path",
dirName: "parent/child",
expectError: false,
},
{
name: "empty directory name",
dirName: "",
expectError: true,
expectedErr: errEmptyDirectoryName,
},
{
name: "write error",
dirName: "testdir",
writeErr: errTest,
expectError: true,
},
{
name: "close error",
dirName: "testdir",
closeErr: errTest,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
if tt.dirName != "" {
expectedName := tt.dirName
if !strings.HasSuffix(expectedName, "/") {
expectedName += "/"
}
// Mock StatObject to indicate directory doesn't exist
mockProvider.EXPECT().
StatObject(gomock.Any(), expectedName).
Return(nil, errTest)
mockWriter := NewMockWriteCloser(ctrl)
mockProvider.EXPECT().
NewWriter(gomock.Any(), expectedName).
Return(mockWriter)
mockWriter.EXPECT().
Write([]byte("")).
Return(0, tt.writeErr)
if tt.closeErr != nil {
mockWriter.EXPECT().Close().Return(tt.closeErr).AnyTimes()
} else {
mockWriter.EXPECT().Close().Return(nil).AnyTimes()
}
}
err := fs.Mkdir(tt.dirName, os.ModePerm)
if tt.expectError {
require.Error(t, err)
if tt.expectedErr != nil {
assert.Equal(t, tt.expectedErr, err)
}
} else {
require.NoError(t, err)
}
})
}
}
func TestCommonFileSystem_RemoveAll(t *testing.T) {
tests := []struct {
name string
dirPath string
objects []string
listErr error
deleteErr error
expectError bool
}{
{
name: "successful removal",
dirPath: "testdir",
objects: []string{
"testdir/file1.txt",
"testdir/file2.txt",
"testdir/subdir/",
},
expectError: false,
},
{
name: "empty directory",
dirPath: "emptydir",
objects: []string{},
expectError: false,
},
{
name: "list error",
dirPath: "testdir",
listErr: errTest,
expectError: true,
},
{
name: "delete error",
dirPath: "testdir",
objects: []string{
"testdir/file1.txt",
},
deleteErr: errTest,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
if tt.listErr != nil {
mockProvider.EXPECT().
ListObjects(gomock.Any(), tt.dirPath).
Return(nil, tt.listErr)
} else {
mockProvider.EXPECT().
ListObjects(gomock.Any(), tt.dirPath).
Return(tt.objects, nil)
if tt.deleteErr != nil {
mockProvider.EXPECT().
DeleteObject(gomock.Any(), tt.objects[0]).
Return(tt.deleteErr)
} else {
for _, obj := range tt.objects {
mockProvider.EXPECT().
DeleteObject(gomock.Any(), obj).
Return(nil)
}
}
}
err := fs.RemoveAll(tt.dirPath)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
func TestCommonFileSystem_ReadDir(t *testing.T) {
now := time.Now()
tests := []struct {
name string
dir string
objects []ObjectInfo
prefixes []string
listErr error
expectedCount int
expectError bool
}{
{
name: "successful read with files and directories",
dir: "testdir",
objects: []ObjectInfo{
{Name: "testdir/file1.txt", Size: 100, LastModified: now, IsDir: false},
{Name: "testdir/file2.pdf", Size: 200, LastModified: now, IsDir: false},
},
prefixes: []string{
"testdir/subdir1/",
"testdir/subdir2/",
},
expectedCount: 4,
expectError: false,
},
{
name: "empty directory",
dir: "emptydir",
objects: []ObjectInfo{},
prefixes: []string{},
expectedCount: 0,
expectError: false,
},
{
name: "directory with marker objects (should be skipped)",
dir: "testdir",
objects: []ObjectInfo{
{Name: "testdir/", Size: 0, IsDir: true},
{Name: "testdir/file.txt", Size: 100, LastModified: now, IsDir: false},
},
prefixes: []string{},
expectedCount: 1,
expectError: false,
},
{
name: "list error",
dir: "testdir",
listErr: errTest,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
if tt.listErr != nil {
mockProvider.EXPECT().
ListDir(gomock.Any(), tt.dir).
Return(nil, nil, tt.listErr)
} else {
mockProvider.EXPECT().
ListDir(gomock.Any(), tt.dir).
Return(tt.objects, tt.prefixes, nil)
}
fileInfos, err := fs.ReadDir(tt.dir)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.Len(t, fileInfos, tt.expectedCount)
}
})
}
}
func TestCommonFileSystem_Stat(t *testing.T) {
now := time.Now()
tests := []struct {
name string
fileName string
statInfo *ObjectInfo
statErr error
listObjects []ObjectInfo
listErr error
expectError bool
expectedDir bool
}{
{
name: "successful stat for file",
fileName: "file.txt",
statInfo: &ObjectInfo{
Name: "file.txt",
Size: 100,
ContentType: "text/plain",
LastModified: now,
IsDir: false,
},
expectError: false,
expectedDir: false,
},
{
name: "file not found, but directory exists",
fileName: "testdir",
statErr: errTest,
listObjects: []ObjectInfo{
{Name: "testdir/file.txt", Size: 100, LastModified: now, IsDir: false},
},
expectError: false,
expectedDir: true,
},
{
name: "file not found, directory also not found",
fileName: "nonexistent",
statErr: errTest,
listObjects: []ObjectInfo{},
expectError: true,
},
{
name: "stat error and list error",
fileName: "testfile",
statErr: errTest,
listErr: errTest,
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
if tt.statErr != nil {
mockProvider.EXPECT().
StatObject(gomock.Any(), tt.fileName).
Return(nil, tt.statErr)
prefix := tt.fileName
if !strings.HasSuffix(prefix, "/") {
prefix += "/"
}
if tt.listErr != nil {
mockProvider.EXPECT().
ListDir(gomock.Any(), prefix).
Return(nil, nil, tt.listErr)
} else {
mockProvider.EXPECT().
ListDir(gomock.Any(), prefix).
Return(tt.listObjects, nil, nil)
}
} else {
mockProvider.EXPECT().
StatObject(gomock.Any(), tt.fileName).
Return(tt.statInfo, nil)
}
fileInfo, err := fs.Stat(tt.fileName)
if tt.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
assert.NotNil(t, fileInfo)
assert.Equal(t, tt.expectedDir, fileInfo.IsDir())
}
})
}
}
func TestCommonFileSystem_ChDir(t *testing.T) {
ctrl, _, fs := setupCommonFS(t)
defer ctrl.Finish()
err := fs.ChDir("some/dir")
require.Error(t, err)
assert.Equal(t, errChDirNotSupported, err)
}
func TestCommonFileSystem_Getwd(t *testing.T) {
tests := []struct {
name string
location string
expectedPath string
}{
{
name: "bucket name",
location: "my-bucket",
expectedPath: "my-bucket",
},
{
name: "FTP connection",
location: "ftp://host",
expectedPath: "ftp://host",
},
{
name: "empty location",
location: "",
expectedPath: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockProvider := NewMockStorageProvider(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
// Set up observability expectations
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
fs := &CommonFileSystem{
Provider: mockProvider,
Location: tt.location,
Logger: mockLogger,
Metrics: mockMetrics,
}
path, err := fs.Getwd()
require.NoError(t, err)
assert.Equal(t, tt.expectedPath, path)
})
}
}
// Mock WriteCloser for testing.
type MockWriteCloser struct {
ctrl *gomock.Controller
recorder *MockWriteCloserMockRecorder
}
type MockWriteCloserMockRecorder struct {
mock *MockWriteCloser
}
func NewMockWriteCloser(ctrl *gomock.Controller) *MockWriteCloser {
mock := &MockWriteCloser{ctrl: ctrl}
mock.recorder = &MockWriteCloserMockRecorder{mock}
return mock
}
func (m *MockWriteCloser) EXPECT() *MockWriteCloserMockRecorder {
return m.recorder
}
func (m *MockWriteCloser) Write(p []byte) (n int, err error) {
ret := m.ctrl.Call(m, "Write", p)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
func (m *MockWriteCloserMockRecorder) Write(p any) *gomock.Call {
return m.mock.ctrl.RecordCallWithMethodType(m.mock, "Write", reflect.TypeOf((*MockWriteCloser)(nil).Write), p)
}
func (m *MockWriteCloser) Close() error {
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
func (m *MockWriteCloserMockRecorder) Close() *gomock.Call {
return m.mock.ctrl.RecordCallWithMethodType(m.mock, "Close", reflect.TypeOf((*MockWriteCloser)(nil).Close))
}
func TestIsAlreadyExistsError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "nil error",
err: nil,
expected: false,
},
{
name: "GCS error code 409",
err: &googleapi.Error{Code: 409},
expected: true,
},
{
name: "GCS error code 412",
err: &googleapi.Error{Code: 412},
expected: true,
},
{
name: "GCS error code 404",
err: &googleapi.Error{Code: 404},
expected: false,
},
{
name: "unrelated error",
err: errTest,
expected: false,
},
{
name: "wrapped GCS error 409",
err: &googleapi.Error{Code: 409, Message: "Object already exists"},
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := IsAlreadyExistsError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGenerateCopyName(t *testing.T) {
tests := []struct {
name string
original string
count int
expected string
}{
{
name: "simple file with extension",
original: "file.txt",
count: 1,
expected: "file copy 1.txt",
},
{
name: "file with multiple dots",
original: "document.backup.tar.gz",
count: 2,
expected: "document.backup.tar copy 2.gz",
},
{
name: "file without extension",
original: "README",
count: 1,
expected: "README copy 1",
},
{
name: "file with path",
original: "folder/subfolder/file.pdf",
count: 3,
expected: "folder/subfolder/file copy 3.pdf",
},
{
name: "file with high count number",
count: 999,
expected: " copy 999",
},
{
name: "file with spaces in name",
original: "my document.docx",
count: 1,
expected: "my document copy 1.docx",
},
{
name: "hidden file",
original: ".gitignore",
count: 1,
expected: " copy 1.gitignore",
},
{
name: "file with leading dot only",
original: ".config",
count: 2,
expected: " copy 2.config",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := GenerateCopyName(tt.original, tt.count)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCommonFileSystem_Create_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockWriter := NewMockWriteCloser(ctrl)
mockProvider.EXPECT().
NewWriter(gomock.Any(), "newfile.txt").
Return(mockWriter)
f, err := fs.Create("newfile.txt")
require.NoError(t, err)
assert.NotNil(t, f)
}
func TestCommonFileSystem_Open_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
now := time.Now()
info := &ObjectInfo{
Name: "file.txt",
Size: 123,
ContentType: "text/plain",
LastModified: now,
IsDir: false,
}
mockProvider.EXPECT().
StatObject(gomock.Any(), "file.txt").
Return(info, nil)
reader := io.NopCloser(strings.NewReader("hello"))
mockProvider.EXPECT().
NewReader(gomock.Any(), "file.txt").
Return(reader, nil)
f, err := fs.Open("file.txt")
require.NoError(t, err)
assert.NotNil(t, f)
}
func TestCommonFileSystem_Open_StatObjectError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
StatObject(gomock.Any(), "missing.txt").
Return(nil, errTest)
_, err := fs.Open("missing.txt")
require.Error(t, err)
assert.Equal(t, ErrFileNotFound, err)
}
func TestCommonFileSystem_Open_NewReaderError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
info := &ObjectInfo{
Name: "file.txt",
Size: 10,
ContentType: "text/plain",
LastModified: time.Now(),
IsDir: false,
}
mockProvider.EXPECT().
StatObject(gomock.Any(), "file.txt").
Return(info, nil)
mockProvider.EXPECT().
NewReader(gomock.Any(), "file.txt").
Return(nil, errTest)
_, err := fs.Open("file.txt")
require.Error(t, err)
assert.Equal(t, errTest, err)
}
func TestCommonFileSystem_OpenFile_LocalRDWR_Success(t *testing.T) {
ctrl, _, fs := setupCommonFS(t)
defer ctrl.Finish()
tmp, err := os.CreateTemp(t.TempDir(), "commonfs_test_*")
require.NoError(t, err)
_, err = tmp.WriteString("content")
require.NoError(t, err)
tmp.Close()
defer os.Remove(tmp.Name())
f, err := fs.OpenFile(tmp.Name(), os.O_RDWR, 0644)
require.NoError(t, err)
assert.NotNil(t, f)
}
func TestCommonFileSystem_OpenFile_Append_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockWriter := NewMockWriteCloser(ctrl)
mockProvider.EXPECT().
NewWriter(gomock.Any(), "append.txt").
Return(mockWriter)
f, err := fs.OpenFile("append.txt", os.O_APPEND|os.O_WRONLY, 0)
require.NoError(t, err)
assert.NotNil(t, f)
}
func TestCommonFileSystem_OpenFile_Create_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockWriter := NewMockWriteCloser(ctrl)
mockProvider.EXPECT().
NewWriter(gomock.Any(), "create.txt").
Return(mockWriter)
f, err := fs.OpenFile("create.txt", os.O_CREATE|os.O_WRONLY, 0644)
require.NoError(t, err)
assert.NotNil(t, f)
}
func TestCommonFileSystem_OpenFile_UnsupportedFlags_Error(t *testing.T) {
ctrl, _, fs := setupCommonFS(t)
defer ctrl.Finish()
_, err := fs.OpenFile("unsupported.txt", os.O_WRONLY, 0644)
require.Error(t, err)
assert.Equal(t, errUnsupportedFlags, err)
}
func TestCommonFileSystem_Remove_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
DeleteObject(gomock.Any(), "file.txt").
Return(nil)
err := fs.Remove("file.txt")
require.NoError(t, err)
}
func TestCommonFileSystem_Remove_DeleteError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
DeleteObject(gomock.Any(), "file.txt").
Return(errTest)
err := fs.Remove("file.txt")
require.Error(t, err)
assert.Equal(t, errTest, err)
}
func TestCommonFileSystem_Rename_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
CopyObject(gomock.Any(), "old.txt", "new.txt").
Return(nil)
mockProvider.EXPECT().
DeleteObject(gomock.Any(), "old.txt").
Return(nil)
err := fs.Rename("old.txt", "new.txt")
require.NoError(t, err)
}
func TestCommonFileSystem_Rename_CopyError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
CopyObject(gomock.Any(), "old.txt", "new.txt").
Return(errTest)
err := fs.Rename("old.txt", "new.txt")
require.Error(t, err)
assert.Equal(t, errTest, err)
}
func TestCommonFileSystem_Rename_DeleteError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
mockProvider.EXPECT().
CopyObject(gomock.Any(), "old.txt", "new.txt").
Return(nil)
mockProvider.EXPECT().
DeleteObject(gomock.Any(), "old.txt").
Return(errTest)
err := fs.Rename("old.txt", "new.txt")
require.Error(t, err)
assert.Equal(t, errTest, err)
}
func TestCommonFileSystem_MkdirAll_Success(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
dirPath := "a/b/c"
mockWriter := NewMockWriteCloser(ctrl)
// Each Mkdir call will:
// StatObject("x/") -> err (not exists)
// NewWriter("x/") -> mockWriter
// Write("") -> nil
// explicit Close() -> nil
// deferred Close() -> nil
gomock.InOrder(
mockProvider.EXPECT().StatObject(gomock.Any(), "a/").Return(nil, errTest),
mockProvider.EXPECT().NewWriter(gomock.Any(), "a/").Return(mockWriter),
mockWriter.EXPECT().Write([]byte("")).Return(0, nil),
mockWriter.EXPECT().Close().Return(nil), // explicit close
mockWriter.EXPECT().Close().Return(nil), // deferred close
mockProvider.EXPECT().StatObject(gomock.Any(), "a/b/").Return(nil, errTest),
mockProvider.EXPECT().NewWriter(gomock.Any(), "a/b/").Return(mockWriter),
mockWriter.EXPECT().Write([]byte("")).Return(0, nil),
mockWriter.EXPECT().Close().Return(nil),
mockWriter.EXPECT().Close().Return(nil),
mockProvider.EXPECT().StatObject(gomock.Any(), "a/b/c/").Return(nil, errTest),
mockProvider.EXPECT().NewWriter(gomock.Any(), "a/b/c/").Return(mockWriter),
mockWriter.EXPECT().Write([]byte("")).Return(0, nil),
mockWriter.EXPECT().Close().Return(nil),
mockWriter.EXPECT().Close().Return(nil),
)
err := fs.MkdirAll(dirPath, os.ModePerm)
require.NoError(t, err)
}
func TestCommonFileSystem_MkdirAll_EmptyPath_Error(t *testing.T) {
fs := &CommonFileSystem{}
err := fs.MkdirAll("", os.ModePerm)
require.Error(t, err)
assert.Equal(t, errEmptyDirectoryName, err)
}
func TestCommonFileSystem_MkdirAll_Mkdir_WriteError(t *testing.T) {
ctrl, mockProvider, fs := setupCommonFS(t)
defer ctrl.Finish()
dirPath := "x/y"
mockWriter := NewMockWriteCloser(ctrl)
// For first component "x/" simulate Write error
gomock.InOrder(
mockProvider.EXPECT().StatObject(gomock.Any(), "x/").Return(nil, errTest),
mockProvider.EXPECT().NewWriter(gomock.Any(), "x/").Return(mockWriter),
mockWriter.EXPECT().Write([]byte("")).Return(0, errTest),
mockWriter.EXPECT().Close().Return(nil), // deferred close after return
)
err := fs.MkdirAll(dirPath, os.ModePerm)
require.Error(t, err)
assert.Equal(t, errTest, err)
}
// Tests for UseLogger, UseMetrics, Connect
func TestCommonFileSystem_UseLogger_SetsWhenCorrectType(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
fs := &CommonFileSystem{}
fs.UseLogger(mockLogger)
assert.Equal(t, mockLogger, fs.Logger)
}
func TestCommonFileSystem_UseMetrics_SetsWhenCorrectType(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
fs := &CommonFileSystem{}
fs.UseMetrics(mockMetrics)
assert.Equal(t, mockMetrics, fs.Metrics)
}
func TestCommonFileSystem_Connect_UsesLoggerWhenPresent(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
// Expect Debugf to be called once. Use gomock.Any() for the format and args.
mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
fs := &CommonFileSystem{
Logger: mockLogger,
Location: "my-location",
}
// Should call Debugf on provided logger
err := fs.Connect(t.Context())
assert.Equal(t, err, errProviderNil)
}
func TestCommonFileSystem_Connect_NoLogger_NoPanic(t *testing.T) {
// No logger configured: Connect should be a no-op and not panic
fs := &CommonFileSystem{
Logger: nil,
Location: "no-logger",
}
err := fs.Connect(t.Context())
assert.Equal(t, err, errProviderNil)
}
// Test for PrettyPrint on OperationLog: verify output contains key fields.
func TestOperationLog_PrettyPrint(t *testing.T) {
var buf bytes.Buffer
status := "OK"
message := "done"
ol := &OperationLog{
Operation: "TestOp",
Provider: "local",
Duration: 123,
Status: &status,
Message: &message,
}
ol.PrettyPrint(&buf)
out := buf.String()
assert.Contains(t, out, "TestOp")
assert.Contains(t, out, "local")
// Duration printed as number (µs suffix present in format string)
assert.Contains(t, out, "123")
assert.Contains(t, out, "OK")
assert.Contains(t, out, "done")
}
// Test DefaultHistogramBuckets returns expected bucket values.
func TestDefaultHistogramBuckets(t *testing.T) {
exp := []float64{0.1, 1, 10, 100, 1000}
got := DefaultHistogramBuckets()
assert.Equal(t, exp, got)
}
func TestValidateSeekOffset_SeekStartValid(t *testing.T) {
got, err := ValidateSeekOffset(io.SeekStart, 10, 0, 100)
require.NoError(t, err)
assert.Equal(t, int64(10), got)
}
func TestValidateSeekOffset_SeekEndNegativeOffset(t *testing.T) {
got, err := ValidateSeekOffset(io.SeekEnd, -10, 0, 100)
require.NoError(t, err)
assert.Equal(t, int64(90), got)
}
func TestValidateSeekOffset_SeekCurrentValid(t *testing.T) {
got, err := ValidateSeekOffset(io.SeekCurrent, 5, 20, 100)
require.NoError(t, err)
assert.Equal(t, int64(25), got)
}
func TestValidateSeekOffset_InvalidWhence_ReturnsErrOutOfRange(t *testing.T) {
_, err := ValidateSeekOffset(999, 0, 0, 100)
require.Error(t, err)
assert.ErrorIs(t, err, ErrOutOfRange)
}
func TestValidateSeekOffset_NegativeResultingOffset_ReturnsErrOutOfRange(t *testing.T) {
_, err := ValidateSeekOffset(io.SeekStart, -1, 0, 100)
require.Error(t, err)
assert.ErrorIs(t, err, ErrOutOfRange)
}
func TestValidateSeekOffset_OffsetGreaterThanLength_ReturnsErrOutOfRange(t *testing.T) {
_, err := ValidateSeekOffset(io.SeekStart, 101, 0, 100)
require.Error(t, err)
assert.ErrorIs(t, err, ErrOutOfRange)
}
func TestValidateSeekOffset_SeekCurrentBeyondLength_ReturnsErrOutOfRange(t *testing.T) {
_, err := ValidateSeekOffset(io.SeekCurrent, 1000, 10, 100)
require.Error(t, err)
assert.ErrorIs(t, err, ErrOutOfRange)
}
================================================
FILE: pkg/gofr/datasource/file/ftp/fs.go
================================================
package ftp
import (
"context"
"errors"
"fmt"
"time"
"gofr.dev/pkg/gofr/datasource/file"
)
var (
errInvalidConfig = errors.New("invalid FTP configuration: host and port are required")
errInvalidProvider = errors.New("invalid FTP provider")
)
const defaultTimeout = 10 * time.Second
type fileSystem struct {
*file.CommonFileSystem
}
// New creates and validates a new FTP file system.
// Returns error if connection fails or configuration is invalid.
func New(config *Config) file.FileSystemProvider {
if config == nil {
config = &Config{}
}
// Set default dial timeout if not specified
if config.DialTimeout == 0 {
config.DialTimeout = 5 * time.Second
}
adapter := &storageAdapter{cfg: config}
location := buildLocation(config)
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: location,
ProviderName: "FTP",
},
}
return fs
}
// buildLocation creates the location string for metrics/logging.
func buildLocation(config *Config) string {
if config.Host == "" {
return "ftp://unconfigured"
}
port := config.Port
if port == 0 {
port = 21 // Default FTP port
}
location := fmt.Sprintf("%s:%d", config.Host, port)
if config.RemoteDir != "" && config.RemoteDir != "/" {
location = fmt.Sprintf("%s:%d%s", config.Host, port, config.RemoteDir)
}
return location
}
// Connect tries a single immediate connect via provider; on failure it starts a background retry.
func (f *fileSystem) Connect() {
if f.CommonFileSystem.IsConnected() {
return
}
// Validate configuration before attempting connection
if err := f.validateConfig(); err != nil {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Errorf("Invalid FTP configuration: %v", err)
}
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
err := f.CommonFileSystem.Connect(ctx)
if err != nil {
// Log warning if logger is available
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Warnf("FTP server %s not available, starting background retry: %v",
f.CommonFileSystem.Location, err)
}
// Start background retry
go f.startRetryConnect()
return
}
// Connected successfully
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Infof("FTP connection established to server %s", f.CommonFileSystem.Location)
}
}
// startRetryConnect retries connection every 30 seconds until success.
func (f *fileSystem) startRetryConnect() {
if f.CommonFileSystem.IsConnected() || f.CommonFileSystem.IsRetryDisabled() {
return
}
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
if f.CommonFileSystem.IsConnected() || f.CommonFileSystem.IsRetryDisabled() {
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
err := f.CommonFileSystem.Connect(ctx)
cancel()
if err == nil {
// Success - exit retry loop
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Infof("FTP connection restored to server %s", f.CommonFileSystem.Location)
}
return
}
// Still failing - log and continue retrying
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf("FTP retry failed, will try again: %v", err)
}
}
}
// validateConfig checks if the configuration is valid.
func (f *fileSystem) validateConfig() error {
adapter, ok := f.CommonFileSystem.Provider.(*storageAdapter)
if !ok {
return errInvalidProvider
}
cfg := adapter.cfg
if cfg == nil || cfg.Host == "" || cfg.Port <= 0 {
return errInvalidConfig
}
return nil
}
================================================
FILE: pkg/gofr/datasource/file/ftp/fs_test.go
================================================
package ftp
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/file"
)
func TestNew_NilConfig(t *testing.T) {
fs := New(nil)
require.NotNil(t, fs)
assert.Equal(t, "ftp://unconfigured", fs.(*fileSystem).CommonFileSystem.Location)
}
func TestNew_EmptyHost(t *testing.T) {
config := &Config{Host: "", Port: 2121}
fs := New(config)
require.NotNil(t, fs)
assert.Equal(t, "ftp://unconfigured", fs.(*fileSystem).CommonFileSystem.Location)
}
func TestNew_PortZero(t *testing.T) {
config := &Config{Host: "localhost", Port: 0}
fs := New(config)
require.NotNil(t, fs)
// Port 0 will default to 21 in buildLocation
assert.Equal(t, "localhost:21", fs.(*fileSystem).CommonFileSystem.Location)
}
func TestNew_ConnectionFailure_StartsRetry(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "non-existent-host",
Port: 9999,
User: "testuser",
Password: "testpass",
}
fs := New(config)
require.NotNil(t, fs)
// Inject logger and metrics (mimicking AddFileStore behavior)
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockLogger.EXPECT().Warnf(
"FTP server %s not available, starting background retry: %v",
gomock.Any(),
gomock.Any(),
)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
fs.Connect()
time.Sleep(100 * time.Millisecond)
fs.(*fileSystem).CommonFileSystem.SetDisableRetry(true)
}
func TestNew_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
}
fs := New(config)
require.NotNil(t, fs)
assert.Equal(t, "localhost:2121", fs.(*fileSystem).CommonFileSystem.Location)
// Inject logger and metrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Infof("connected to %s", "localhost:2121").MaxTimes(1)
mockLogger.EXPECT().Warnf(
"FTP server %s not available, starting background retry: %v",
gomock.Any(),
gomock.Any(),
).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
fs.Connect()
}
func TestNew_WithRemoteDir(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
RemoteDir: "/uploads",
}
fs := New(config)
require.NotNil(t, fs)
assert.Equal(t, "localhost:2121/uploads", fs.(*fileSystem).CommonFileSystem.Location)
// Inject logger and metrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Infof("connected to %s", "localhost:2121/uploads").MaxTimes(1)
mockLogger.EXPECT().Warnf(
"FTP server %s not available, starting background retry: %v",
gomock.Any(),
gomock.Any(),
).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
fs.Connect()
}
func TestNew_WithRootRemoteDir(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
RemoteDir: "/",
}
fs := New(config)
require.NotNil(t, fs)
assert.Equal(t, "localhost:2121", fs.(*fileSystem).CommonFileSystem.Location)
// Inject logger and metrics
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Infof("connected to %s", "localhost:2121").MaxTimes(1)
mockLogger.EXPECT().Warnf(
"FTP server %s not available, starting background retry: %v",
gomock.Any(),
gomock.Any(),
).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
fs.Connect()
}
func TestNew_WithCustomDialTimeout(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
DialTimeout: 3 * time.Second,
}
fs := New(config)
require.NotNil(t, fs)
adapter := fs.(*fileSystem).CommonFileSystem.Provider.(*storageAdapter)
assert.Equal(t, 3*time.Second, adapter.cfg.DialTimeout)
}
func TestConnect_AlreadyConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
}
adapter := &storageAdapter{cfg: config}
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: "localhost:2121",
Logger: mockLogger,
Metrics: mockMetrics,
},
}
fs.CommonFileSystem.SetConnected(true)
fs.Connect()
assert.True(t, fs.CommonFileSystem.IsConnected())
}
func TestStartRetryConnect_ExitWhenRetryDisabled(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
Host: "localhost",
Port: 2121,
User: "testuser",
Password: "testpass",
}
adapter := &storageAdapter{cfg: config}
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: "localhost:2121",
Logger: mockLogger,
Metrics: mockMetrics,
},
}
// Disable retry before starting
fs.CommonFileSystem.SetDisableRetry(true)
// Should exit immediately without any expectations
done := make(chan bool)
go func() {
fs.startRetryConnect()
done <- true
}()
select {
case <-done:
// Success: function exited immediately
case <-time.After(100 * time.Millisecond):
t.Fatal("startRetryConnect did not exit when retry is disabled")
}
}
================================================
FILE: pkg/gofr/datasource/file/ftp/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/file/ftp
go 1.25.0
require (
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42
github.com/jlaffaye/ftp v0.2.0
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
google.golang.org/api v0.270.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/file/ftp/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9 h1:cC0Hbb+18DJ4i6ybqDybvj4wdIDS4vnD0QEci98PgM8=
github.com/goftp/file-driver v0.0.0-20180502053751-5d604a0fc0c9/go.mod h1:GpOj6zuVBG3Inr9qjEnuVTgBlk2lZ1S9DcoFiXWyKss=
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42 h1:JdOp2qR5PF4O75tzHeqrwnDDv8oHDptWyTbyYS4fD8E=
github.com/goftp/server v0.0.0-20200708154336-f64f7c2d8a42/go.mod h1:k/SS6VWkxY7dHPhoMQ8IdRu8L4lQtmGbhyXGg+vCnXE=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/file/ftp/storage_adapter.go
================================================
package ftp
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"path"
"strings"
"time"
"github.com/jlaffaye/ftp"
"gofr.dev/pkg/gofr/datasource/file"
)
var (
// Storage adapter errors.
errFTPConfigNil = errors.New("FTP config is nil")
errFTPClientNotInitialized = errors.New("FTP client is not initialized")
errEmptyObjectName = errors.New("object name is empty")
errInvalidOffset = errors.New("invalid offset: must be >= 0")
errEmptySourceOrDest = errors.New("source and destination names cannot be empty")
errSameSourceAndDest = errors.New("source and destination are the same")
errFailedToCreateReader = errors.New("failed to create reader")
errFailedToCreateWriter = errors.New("failed to create writer")
errObjectNotFound = errors.New("object not found")
errFailedToGetObjectAttrs = errors.New("failed to get object attrs")
errFailedToDeleteObject = errors.New("failed to delete object")
errFailedToListObjects = errors.New("failed to list objects")
errFailedToListDirectory = errors.New("failed to list directory")
errWriterAlreadyClosed = errors.New("writer already closed")
errFTPConfigInvalid = errors.New("invalid FTP configuration: host and port are required")
)
// Config represents the FTP configuration.
type Config struct {
Host string // FTP server hostname
User string // FTP username
Password string // FTP password
Port int // FTP port
RemoteDir string // Remote directory path. Base Path for all FTP Operations.
DialTimeout time.Duration // FTP connection timeout
}
// storageAdapter adapts FTP client to implement file.StorageProvider.
type storageAdapter struct {
cfg *Config
conn *ftp.ServerConn
}
// Connect initializes the FTP client and logs in to the server.
func (s *storageAdapter) Connect(_ context.Context) error {
// fast-path: already connected
if s.conn != nil {
return nil
}
if s.cfg == nil {
return errFTPConfigNil
}
if s.cfg.Host == "" || s.cfg.Port <= 0 {
return errFTPConfigInvalid
}
// Set default timeout if not specified
dialTimeout := s.cfg.DialTimeout
if dialTimeout == 0 {
dialTimeout = 5 * time.Second
}
ftpServer := fmt.Sprintf("%s:%d", s.cfg.Host, s.cfg.Port)
conn, err := ftp.Dial(ftpServer, ftp.DialWithTimeout(dialTimeout))
if err != nil {
return fmt.Errorf("failed to dial FTP server %q: %w", ftpServer, err)
}
if err := conn.Login(s.cfg.User, s.cfg.Password); err != nil {
_ = conn.Quit()
return fmt.Errorf("FTP login failed for user %q: %w", s.cfg.User, err)
}
s.conn = conn
return nil
}
// NewReader creates a reader for the given object.
func (s *storageAdapter) NewReader(_ context.Context, name string) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
if s.conn == nil {
return nil, errFTPClientNotInitialized
}
objectPath := s.buildPath(name)
reader, err := s.conn.Retr(objectPath)
if err != nil {
if isFTPNotFoundError(err) {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateReader, name, err)
}
return reader, nil
}
// NewRangeReader creates a range reader for the given object.
func (s *storageAdapter) NewRangeReader(_ context.Context, name string, offset, length int64) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
if offset < 0 {
return nil, fmt.Errorf("%w (got: %d)", errInvalidOffset, offset)
}
if s.conn == nil {
return nil, errFTPClientNotInitialized
}
objectPath := s.buildPath(name)
reader, err := s.conn.RetrFrom(objectPath, uint64(offset))
if err != nil {
if isFTPNotFoundError(err) {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return nil, fmt.Errorf("%w for %q at offset %d: %w", errFailedToCreateReader, name, offset, err)
}
// FTP doesn't support length limit in RetrFrom, so wrap in LimitReader
if length > 0 {
return &limitedReadCloser{
Reader: io.LimitReader(reader, length),
Closer: reader,
}, nil
}
return reader, nil
}
// limitedReadCloser combines io.LimitReader with Close capability.
type limitedReadCloser struct {
io.Reader
io.Closer
}
// NewWriter creates a writer for the given object.
func (s *storageAdapter) NewWriter(_ context.Context, name string) io.WriteCloser {
if name == "" {
return &failWriter{err: errEmptyObjectName}
}
if s.conn == nil {
return &failWriter{err: errFTPClientNotInitialized}
}
objectPath := s.buildPath(name)
return &ftpWriter{
conn: s.conn,
objectPath: objectPath,
buffer: &bytes.Buffer{},
}
}
// ftpWriter buffers writes and uploads on Close.
type ftpWriter struct {
conn *ftp.ServerConn
objectPath string
buffer *bytes.Buffer
closed bool
}
func (fw *ftpWriter) Write(p []byte) (int, error) {
if fw.closed {
return 0, errWriterAlreadyClosed
}
return fw.buffer.Write(p)
}
func (fw *ftpWriter) Close() error {
if fw.closed {
return nil
}
fw.closed = true
if err := fw.conn.Stor(fw.objectPath, fw.buffer); err != nil {
return fmt.Errorf("%w for %q: %w", errFailedToCreateWriter, fw.objectPath, err)
}
return nil
}
// failWriter is a helper for NewWriter validation errors.
type failWriter struct {
err error
}
func (fw *failWriter) Write([]byte) (int, error) {
return 0, fw.err
}
func (fw *failWriter) Close() error {
return fw.err
}
// DeleteObject deletes the object with the given name.
func (s *storageAdapter) DeleteObject(_ context.Context, name string) error {
if name == "" {
return errEmptyObjectName
}
if s.conn == nil {
return errFTPClientNotInitialized
}
objectPath := s.buildPath(name)
if err := s.conn.Delete(objectPath); err != nil {
if isFTPNotFoundError(err) {
return fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return fmt.Errorf("%w %q: %w", errFailedToDeleteObject, name, err)
}
return nil
}
// CopyObject copies an object from src to dst.
func (s *storageAdapter) CopyObject(_ context.Context, source, dest string) error {
if source == "" || dest == "" {
return errEmptySourceOrDest
}
if source == dest {
return errSameSourceAndDest
}
if s.conn == nil {
return errFTPClientNotInitialized
}
// Read source file
sourcePath := s.buildPath(source)
resp, err := s.conn.Retr(sourcePath)
if err != nil {
if isFTPNotFoundError(err) {
return fmt.Errorf("%w: %q", errObjectNotFound, source)
}
return fmt.Errorf("failed to read source object %q: %w", source, err)
}
// Read all data from source into memory
data, err := io.ReadAll(resp)
closeErr := resp.Close()
if err != nil {
return fmt.Errorf("failed to read source object data %q: %w", source, err)
}
if closeErr != nil {
return fmt.Errorf("failed to close source reader for %q: %w", source, closeErr)
}
// Write to destination
destPath := s.buildPath(dest)
if err := s.conn.Stor(destPath, bytes.NewReader(data)); err != nil {
return fmt.Errorf("failed to write destination object %q: %w", dest, err)
}
return nil
}
// StatObject returns metadata for the given object.
func (s *storageAdapter) StatObject(_ context.Context, name string) (*file.ObjectInfo, error) {
if name == "" {
return nil, errEmptyObjectName
}
if s.conn == nil {
return nil, errFTPClientNotInitialized
}
objectPath := s.buildPath(name)
entries, err := s.conn.List(objectPath)
if err != nil {
if isFTPNotFoundError(err) {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return nil, fmt.Errorf("%w for %q: %w", errFailedToGetObjectAttrs, name, err)
}
if len(entries) == 0 {
return nil, fmt.Errorf("%w %q", errObjectNotFound, name)
}
entry := entries[0]
return &file.ObjectInfo{
Name: entry.Name,
Size: safeUint64ToInt64(entry.Size),
ContentType: getContentType(entry),
LastModified: entry.Time,
IsDir: entry.Type == ftp.EntryTypeFolder,
}, nil
}
// ListObjects lists all objects with the given prefix.
func (s *storageAdapter) ListObjects(_ context.Context, prefix string) ([]string, error) {
if s.conn == nil {
return nil, errFTPClientNotInitialized
}
dirPath := s.buildPath(prefix)
// If prefix is empty or is the base directory, list from RemoteDir
if prefix == "" || prefix == "." {
dirPath = s.cfg.RemoteDir
}
entries, err := s.conn.List(dirPath)
if err != nil {
if isFTPNotFoundError(err) {
return []string{}, nil // Return empty list for non-existent directories
}
return nil, fmt.Errorf("%w with prefix %q: %w", errFailedToListObjects, prefix, err)
}
var objects []string
for _, entry := range entries {
if entry.Type != ftp.EntryTypeFolder {
// Build relative path from RemoteDir
relativePath := entry.Name
if prefix != "" && prefix != "." {
relativePath = path.Join(prefix, entry.Name)
}
objects = append(objects, relativePath)
}
}
return objects, nil
}
// ListDir lists objects and prefixes (directories) under the given prefix.
func (s *storageAdapter) ListDir(_ context.Context, prefix string) ([]file.ObjectInfo, []string, error) {
if s.conn == nil {
return nil, nil, errFTPClientNotInitialized
}
dirPath := s.resolveDirPath(prefix)
entries, err := s.conn.List(dirPath)
if err != nil {
return s.handleListError(err, prefix)
}
return s.processEntries(entries, prefix)
}
// resolveDirPath resolves the directory path for listing.
func (s *storageAdapter) resolveDirPath(prefix string) string {
if prefix == "" || prefix == "." {
return s.cfg.RemoteDir
}
return s.buildPath(prefix)
}
// handleListError handles errors from List operation.
func (*storageAdapter) handleListError(err error, prefix string) ([]file.ObjectInfo, []string, error) {
if isFTPNotFoundError(err) {
return []file.ObjectInfo{}, []string{}, nil
}
return nil, nil, fmt.Errorf("%w %q: %w", errFailedToListDirectory, prefix, err)
}
// processEntries processes FTP entries into objects and prefixes.
func (s *storageAdapter) processEntries(entries []*ftp.Entry, prefix string) ([]file.ObjectInfo, []string, error) {
var (
objects []file.ObjectInfo
prefixes []string
)
for _, entry := range entries {
if entry.Type == ftp.EntryTypeFolder {
prefixes = append(prefixes, s.buildDirPrefix(entry.Name, prefix))
} else {
objects = append(objects, s.buildObjectInfo(entry, prefix))
}
}
return objects, prefixes, nil
}
// buildDirPrefix constructs a directory prefix.
func (*storageAdapter) buildDirPrefix(name, prefix string) string {
if prefix != "" && prefix != "." {
return path.Join(prefix, name) + "/"
}
return name + "/"
}
// buildObjectInfo constructs file.ObjectInfo from FTP entry.
func (*storageAdapter) buildObjectInfo(entry *ftp.Entry, prefix string) file.ObjectInfo {
objectName := entry.Name
if prefix != "" && prefix != "." {
objectName = path.Join(prefix, entry.Name)
}
return file.ObjectInfo{
Name: objectName,
Size: safeUint64ToInt64(entry.Size),
ContentType: getContentType(entry),
LastModified: entry.Time,
IsDir: false,
}
}
// buildPath constructs the full path by joining RemoteDir with the given name.
func (s *storageAdapter) buildPath(name string) string {
if s.cfg.RemoteDir == "" || s.cfg.RemoteDir == "/" {
return name
}
return path.Join(s.cfg.RemoteDir, name)
}
// isFTPNotFoundError checks if the error indicates a "not found" condition.
func isFTPNotFoundError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// Common FTP "not found" error patterns
return strings.Contains(errStr, "550") || // File not found
strings.Contains(errStr, "551") || // File not available
strings.Contains(errStr, "No such file") ||
strings.Contains(errStr, "not found")
}
// getContentType determines content type based on file extension or type.
func getContentType(entry *ftp.Entry) string {
if entry.Type == ftp.EntryTypeFolder {
return "application/x-directory"
}
ext := strings.ToLower(path.Ext(entry.Name))
contentTypes := map[string]string{
".json": "application/json",
".xml": "application/xml",
".txt": "text/plain; charset=utf-8",
".csv": "text/csv",
".html": "text/html",
".htm": "text/html",
".pdf": "application/pdf",
".zip": "application/zip",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".png": "image/png",
".gif": "image/gif",
}
if contentType, ok := contentTypes[ext]; ok {
return contentType
}
return "application/octet-stream"
}
func safeUint64ToInt64(u uint64) int64 {
const maxInt64 = 1<<63 - 1
if u > maxInt64 {
return maxInt64
}
return int64(u)
}
================================================
FILE: pkg/gofr/datasource/file/ftp/storage_adapter_test.go
================================================
package ftp
import (
"bytes"
"context"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
filedriver "github.com/goftp/file-driver"
ftpserver "github.com/goftp/server"
"github.com/jlaffaye/ftp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
errTest550 = errors.New("550 file not found")
errTest551 = errors.New("551 File not available")
errTestNotFound = errors.New("requested file not found")
errTestTimeout = errors.New("connection timeout")
errTestGeneric = errors.New("test error")
)
// Test helpers
func setupTestFTPServer(t *testing.T) (server *ftpserver.Server, tmpDir string, cleanup func()) {
t.Helper()
tmpDir = t.TempDir()
factory := &filedriver.FileDriverFactory{
RootPath: tmpDir,
Perm: ftpserver.NewSimplePerm("test", "test"),
}
opts := &ftpserver.ServerOpts{
Factory: factory,
Port: 0,
Hostname: "127.0.0.1",
Auth: &ftpserver.SimpleAuth{Name: "test", Password: "test"},
}
server = ftpserver.NewServer(opts)
go func() {
_ = server.ListenAndServe()
}()
time.Sleep(100 * time.Millisecond)
cleanup = func() {
_ = server.Shutdown()
}
return server, tmpDir, cleanup
}
func getTestConfig(port int) *Config {
return &Config{
Host: "127.0.0.1",
Port: port,
User: "test",
Password: "test",
RemoteDir: "",
}
}
func createTestFile(t *testing.T, dir, name string, content []byte) string {
t.Helper()
filePath := filepath.Join(dir, name)
require.NoError(t, os.WriteFile(filePath, content, 0600))
return filePath
}
// Connect Tests
func TestStorageAdapter_Connect_NilConfig(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.Connect(context.Background())
require.Error(t, err)
require.ErrorIs(t, err, errFTPConfigNil)
}
func TestStorageAdapter_Connect_AlreadyConnected(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
err := adapter.Connect(context.Background())
require.NoError(t, err)
}
func TestStorageAdapter_Connect_Success(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
err := adapter.Connect(context.Background())
require.NoError(t, err)
assert.NotNil(t, adapter.conn)
}
// NewReader Tests
func TestStorageAdapter_NewReader_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewReader(context.Background(), "")
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewReader_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
reader, err := adapter.NewReader(context.Background(), "test.txt")
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_NewReader_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
testData := []byte("hello world")
createTestFile(t, tmpDir, "test.txt", testData)
reader, err := adapter.NewReader(context.Background(), "test.txt")
require.NoError(t, err)
require.NotNil(t, reader)
defer reader.Close()
data, readErr := io.ReadAll(reader)
require.NoError(t, readErr)
assert.Equal(t, testData, data)
}
func TestStorageAdapter_NewReader_NotFound(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
reader, err := adapter.NewReader(context.Background(), "missing.txt")
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errObjectNotFound)
}
// NewRangeReader Tests
func TestStorageAdapter_NewRangeReader_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewRangeReader(context.Background(), "", 0, 10)
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewRangeReader_NegativeOffset(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewRangeReader(context.Background(), "test.txt", -1, 10)
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errInvalidOffset)
}
func TestStorageAdapter_NewRangeReader_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
reader, err := adapter.NewRangeReader(context.Background(), "test.txt", 0, 10)
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_NewRangeReader_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
testData := []byte("hello world")
createTestFile(t, tmpDir, "range.txt", testData)
reader, err := adapter.NewRangeReader(context.Background(), "range.txt", 6, 5)
require.NoError(t, err)
require.NotNil(t, reader)
defer reader.Close()
data, readErr := io.ReadAll(reader)
require.NoError(t, readErr)
assert.Equal(t, "world", string(data))
}
func TestStorageAdapter_NewRangeReader_NotFound(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
reader, err := adapter.NewRangeReader(context.Background(), "missing.txt", 0, 10)
require.Error(t, err)
assert.Nil(t, reader)
require.ErrorIs(t, err, errObjectNotFound)
}
// NewWriter Tests
func TestStorageAdapter_NewWriter_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
writer := adapter.NewWriter(context.Background(), "")
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewWriter_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
writer := adapter.NewWriter(context.Background(), "test.txt")
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_NewWriter_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
testData := []byte("hello world")
writer := adapter.NewWriter(context.Background(), "test.txt")
n, err := writer.Write(testData)
require.NoError(t, err)
assert.Equal(t, len(testData), n)
require.NoError(t, writer.Close())
filePath := filepath.Join(tmpDir, "test.txt")
assert.FileExists(t, filePath)
data, readErr := os.ReadFile(filePath)
require.NoError(t, readErr)
assert.Equal(t, testData, data)
}
// FTPWriter Tests
func TestFTPWriter_Write_Success(t *testing.T) {
writer := &ftpWriter{buffer: &bytes.Buffer{}}
data := []byte("test data")
n, err := writer.Write(data)
require.NoError(t, err)
assert.Equal(t, len(data), n)
assert.Equal(t, data, writer.buffer.Bytes())
}
func TestFTPWriter_Write_AfterClose(t *testing.T) {
writer := &ftpWriter{buffer: &bytes.Buffer{}, closed: true}
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
require.ErrorIs(t, err, errWriterAlreadyClosed)
}
func TestFTPWriter_Close_AlreadyClosed(t *testing.T) {
writer := &ftpWriter{buffer: &bytes.Buffer{}, closed: true}
err := writer.Close()
require.NoError(t, err)
}
// FailWriter Tests
func TestFailWriter_Write(t *testing.T) {
fw := &failWriter{err: errTestGeneric}
n, err := fw.Write([]byte("test"))
assert.Equal(t, 0, n)
require.ErrorIs(t, err, errTestGeneric)
}
func TestFailWriter_Close(t *testing.T) {
fw := &failWriter{err: errTestGeneric}
err := fw.Close()
require.ErrorIs(t, err, errTestGeneric)
}
// DeleteObject Tests
func TestStorageAdapter_DeleteObject_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.DeleteObject(context.Background(), "")
require.Error(t, err)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_DeleteObject_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
err := adapter.DeleteObject(context.Background(), "test.txt")
require.Error(t, err)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_DeleteObject_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
testFile := createTestFile(t, tmpDir, "delete-me.txt", []byte("test"))
err := adapter.DeleteObject(context.Background(), "delete-me.txt")
require.NoError(t, err)
assert.NoFileExists(t, testFile)
}
func TestStorageAdapter_DeleteObject_NotFound(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
err := adapter.DeleteObject(context.Background(), "missing.txt")
require.Error(t, err)
require.ErrorIs(t, err, errObjectNotFound)
}
// CopyObject Tests
func TestStorageAdapter_CopyObject_EmptySource(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "", "dest.txt")
require.Error(t, err)
require.ErrorIs(t, err, errEmptySourceOrDest)
}
func TestStorageAdapter_CopyObject_EmptyDestination(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "source.txt", "")
require.Error(t, err)
require.ErrorIs(t, err, errEmptySourceOrDest)
}
func TestStorageAdapter_CopyObject_SameSourceAndDest(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "file.txt", "file.txt")
require.Error(t, err)
require.ErrorIs(t, err, errSameSourceAndDest)
}
func TestStorageAdapter_CopyObject_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
err := adapter.CopyObject(context.Background(), "source.txt", "dest.txt")
require.Error(t, err)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_CopyObject_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
sourceData := []byte("copy me")
createTestFile(t, tmpDir, "source.txt", sourceData)
err := adapter.CopyObject(context.Background(), "source.txt", "dest.txt")
require.NoError(t, err)
destFile := filepath.Join(tmpDir, "dest.txt")
assert.FileExists(t, destFile)
data, readErr := os.ReadFile(destFile)
require.NoError(t, readErr)
assert.Equal(t, sourceData, data)
}
func TestStorageAdapter_CopyObject_SourceNotFound(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
err := adapter.CopyObject(context.Background(), "missing.txt", "dest.txt")
require.Error(t, err)
require.ErrorIs(t, err, errObjectNotFound)
}
// StatObject Tests
func TestStorageAdapter_StatObject_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
info, err := adapter.StatObject(context.Background(), "")
require.Error(t, err)
assert.Nil(t, info)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_StatObject_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
info, err := adapter.StatObject(context.Background(), "test.txt")
require.Error(t, err)
assert.Nil(t, info)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_StatObject_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
testData := []byte("test content")
createTestFile(t, tmpDir, "stat-test.txt", testData)
info, err := adapter.StatObject(context.Background(), "stat-test.txt")
require.NoError(t, err)
assert.Equal(t, "stat-test.txt", info.Name)
assert.Equal(t, int64(len(testData)), info.Size)
assert.False(t, info.IsDir)
}
func TestStorageAdapter_StatObject_NotFound(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
info, err := adapter.StatObject(context.Background(), "missing.txt")
require.Error(t, err)
assert.Nil(t, info)
require.ErrorIs(t, err, errObjectNotFound)
}
// ListObjects Tests
func TestStorageAdapter_ListObjects_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
objects, err := adapter.ListObjects(context.Background(), "prefix/")
require.Error(t, err)
assert.Nil(t, objects)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_ListObjects_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
createTestFile(t, tmpDir, "file1.txt", []byte("1"))
createTestFile(t, tmpDir, "file2.txt", []byte("2"))
objects, err := adapter.ListObjects(context.Background(), "")
require.NoError(t, err)
assert.Len(t, objects, 2)
assert.Contains(t, objects, "file1.txt")
assert.Contains(t, objects, "file2.txt")
}
func TestStorageAdapter_ListObjects_EmptyDirectory(t *testing.T) {
server, _, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
objects, err := adapter.ListObjects(context.Background(), "")
require.NoError(t, err)
assert.Empty(t, objects)
}
// ListDir Tests
func TestStorageAdapter_ListDir_NilClient(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{}}
files, dirs, err := adapter.ListDir(context.Background(), "prefix/")
require.Error(t, err)
assert.Nil(t, files)
assert.Nil(t, dirs)
require.ErrorIs(t, err, errFTPClientNotInitialized)
}
func TestStorageAdapter_ListDir_Success(t *testing.T) {
server, tmpDir, cleanup := setupTestFTPServer(t)
defer cleanup()
adapter := &storageAdapter{cfg: getTestConfig(server.Port)}
require.NoError(t, adapter.Connect(context.Background()))
createTestFile(t, tmpDir, "file.txt", []byte("test"))
files, dirs, err := adapter.ListDir(context.Background(), "")
require.NoError(t, err)
assert.Len(t, files, 1)
assert.Equal(t, "file.txt", files[0].Name)
assert.Empty(t, dirs)
}
// Helper Function Tests - Table Driven
func TestBuildPath(t *testing.T) {
tests := []struct {
name string
remoteDir string
path string
expected string
}{
{
name: "with_remote_dir",
remoteDir: "/uploads",
path: "file.txt",
expected: "/uploads/file.txt",
},
{
name: "root_remote_dir",
remoteDir: "/",
path: "file.txt",
expected: "file.txt",
},
{
name: "empty_remote_dir",
remoteDir: "",
path: "file.txt",
expected: "file.txt",
},
{
name: "nested_path",
remoteDir: "/base",
path: "subdir/file.txt",
expected: "/base/subdir/file.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{RemoteDir: tt.remoteDir}}
result := adapter.buildPath(tt.path)
assert.Equal(t, tt.expected, result)
})
}
}
func TestResolveDirPath(t *testing.T) {
tests := []struct {
name string
remoteDir string
prefix string
expected string
}{
{
name: "empty_prefix",
remoteDir: "/uploads",
prefix: "",
expected: "/uploads",
},
{
name: "dot_prefix",
remoteDir: "/uploads",
prefix: ".",
expected: "/uploads",
},
{
name: "with_prefix",
remoteDir: "/uploads",
prefix: "subdir",
expected: "/uploads/subdir",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{RemoteDir: tt.remoteDir}}
result := adapter.resolveDirPath(tt.prefix)
assert.Equal(t, tt.expected, result)
})
}
}
func TestBuildDirPrefix(t *testing.T) {
tests := []struct {
name string
dirName string
prefix string
expected string
}{
{
name: "with_prefix",
dirName: "subdir",
prefix: "parent",
expected: "parent/subdir/",
},
{
name: "empty_prefix",
dirName: "subdir",
prefix: "",
expected: "subdir/",
},
{
name: "dot_prefix",
dirName: "subdir",
prefix: ".",
expected: "subdir/",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
adapter := &storageAdapter{}
result := adapter.buildDirPrefix(tt.dirName, tt.prefix)
assert.Equal(t, tt.expected, result)
})
}
}
func TestProcessEntries(t *testing.T) {
adapter := &storageAdapter{}
tests := []struct {
name string
entries []*ftp.Entry
prefix string
expectedFiles int
expectedDirs int
}{
{
name: "mixed_content",
entries: []*ftp.Entry{
{Name: "file.txt", Type: ftp.EntryTypeFile, Size: 1024},
{Name: "subdir", Type: ftp.EntryTypeFolder},
},
prefix: "",
expectedFiles: 1,
expectedDirs: 1,
},
{
name: "empty",
entries: []*ftp.Entry{},
prefix: "",
expectedFiles: 0,
expectedDirs: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
files, dirs, err := adapter.processEntries(tt.entries, tt.prefix)
require.NoError(t, err)
assert.Len(t, files, tt.expectedFiles)
assert.Len(t, dirs, tt.expectedDirs)
})
}
}
func TestHandleListError(t *testing.T) {
adapter := &storageAdapter{}
tests := []struct {
name string
err error
expectNoError bool
expectEmptyRes bool
}{
{
name: "not_found_error",
err: errTest550,
expectNoError: true,
expectEmptyRes: true,
},
{
name: "other_error",
err: errTestTimeout,
expectNoError: false,
expectEmptyRes: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
files, dirs, err := adapter.handleListError(tt.err, "prefix")
if tt.expectNoError {
require.NoError(t, err)
assert.Empty(t, files)
assert.Empty(t, dirs)
} else {
require.Error(t, err)
assert.Nil(t, files)
assert.Nil(t, dirs)
}
})
}
}
func TestIsFTPNotFoundError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{
name: "550_error",
err: errTest550,
expected: true,
},
{
name: "551_error",
err: errTest551,
expected: true,
},
{
name: "not_found_text",
err: errTestNotFound,
expected: true,
},
{
name: "other_error",
err: errTestTimeout,
expected: false,
},
{
name: "nil_error",
err: nil,
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isFTPNotFoundError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSafeUint64ToInt64(t *testing.T) {
const maxInt64 = 1<<63 - 1
tests := []struct {
name string
input uint64
expected int64
}{
{
name: "valid_value",
input: 1000,
expected: 1000,
},
{
name: "max_int64",
input: uint64(maxInt64),
expected: maxInt64,
},
{
name: "overflow",
input: uint64(maxInt64) + 1,
expected: maxInt64,
},
{
name: "zero",
input: 0,
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := safeUint64ToInt64(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestGetContentType(t *testing.T) {
tests := []struct {
name string
entry *ftp.Entry
contentType string
}{
{
name: "folder",
entry: &ftp.Entry{Name: "folder", Type: ftp.EntryTypeFolder},
contentType: "application/x-directory",
},
{
name: "json",
entry: &ftp.Entry{Name: "data.json", Type: ftp.EntryTypeFile},
contentType: "application/json",
},
{
name: "xml",
entry: &ftp.Entry{Name: "config.xml", Type: ftp.EntryTypeFile},
contentType: "application/xml",
},
{
name: "text",
entry: &ftp.Entry{Name: "readme.txt", Type: ftp.EntryTypeFile},
contentType: "text/plain; charset=utf-8",
},
{
name: "csv",
entry: &ftp.Entry{Name: "data.csv", Type: ftp.EntryTypeFile},
contentType: "text/csv",
},
{
name: "html",
entry: &ftp.Entry{Name: "index.html", Type: ftp.EntryTypeFile},
contentType: "text/html",
},
{
name: "htm",
entry: &ftp.Entry{Name: "page.htm", Type: ftp.EntryTypeFile},
contentType: "text/html",
},
{
name: "pdf",
entry: &ftp.Entry{Name: "document.pdf", Type: ftp.EntryTypeFile},
contentType: "application/pdf",
},
{
name: "zip",
entry: &ftp.Entry{Name: "archive.zip", Type: ftp.EntryTypeFile},
contentType: "application/zip",
},
{
name: "jpeg",
entry: &ftp.Entry{Name: "photo.jpeg", Type: ftp.EntryTypeFile},
contentType: "image/jpeg",
},
{
name: "jpg",
entry: &ftp.Entry{Name: "photo.jpg", Type: ftp.EntryTypeFile},
contentType: "image/jpeg",
},
{
name: "png",
entry: &ftp.Entry{Name: "image.png", Type: ftp.EntryTypeFile},
contentType: "image/png",
},
{
name: "gif",
entry: &ftp.Entry{Name: "animation.gif", Type: ftp.EntryTypeFile},
contentType: "image/gif",
},
{
name: "unknown",
entry: &ftp.Entry{Name: "file.unknown", Type: ftp.EntryTypeFile},
contentType: "application/octet-stream",
},
{
name: "no_extension",
entry: &ftp.Entry{Name: "README", Type: ftp.EntryTypeFile},
contentType: "application/octet-stream",
},
{
name: "case_insensitive",
entry: &ftp.Entry{Name: "FILE.TXT", Type: ftp.EntryTypeFile},
contentType: "text/plain; charset=utf-8",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getContentType(tt.entry)
assert.Equal(t, tt.contentType, result)
})
}
}
// LimitedReadCloser Tests
func TestLimitedReadCloser_Read(t *testing.T) {
data := []byte("hello world")
reader := strings.NewReader(string(data))
limited := &limitedReadCloser{
Reader: io.LimitReader(reader, 5),
Closer: io.NopCloser(reader),
}
buf := make([]byte, 10)
n, err := limited.Read(buf)
require.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, "hello", string(buf[:n]))
}
func TestLimitedReadCloser_Close(t *testing.T) {
reader := io.NopCloser(strings.NewReader("test"))
limited := &limitedReadCloser{
Reader: reader,
Closer: reader,
}
err := limited.Close()
require.NoError(t, err)
}
================================================
FILE: pkg/gofr/datasource/file/gcs/fs.go
================================================
package gcs
import (
"context"
"time"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/file"
)
const defaultTimeout = 10 * time.Second
type fileSystem struct {
*file.CommonFileSystem
adapter *storageAdapter
}
// Config represents the gcs configuration.
type Config struct {
EndPoint string
BucketName string
CredentialsJSON string
ProjectID string
}
// New creates a new GCS filesystem and returns it as a CloudFileSystem.
// CloudFileSystem is a superset of FileSystemProvider so it can be passed directly to
// app.AddFileStore() without any conversion, while also giving callers compile-time
// access to cloud-specific methods (CreateWithOptions, GenerateSignedURL) without
// requiring a type assertion.
func New(config *Config) file.CloudFileSystem {
if config == nil {
config = &Config{}
}
adapter := &storageAdapter{cfg: config}
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: config.BucketName,
ProviderName: "GCS", // Set provider name for observability
},
adapter: adapter,
}
return fs
}
// UseLogger sets the logger on both the common file system and the storage adapter.
func (f *fileSystem) UseLogger(logger any) {
f.CommonFileSystem.UseLogger(logger)
if l, ok := logger.(datasource.Logger); ok {
f.adapter.logger = l
}
}
// Connect tries a single immediate connect via provider; on failure it starts a background retry.
func (f *fileSystem) Connect() {
if f.CommonFileSystem.IsConnected() {
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
err := f.CommonFileSystem.Connect(ctx)
if err != nil {
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Errorf("GCS bucket %s not available, starting background retry: %v",
f.CommonFileSystem.Location, err)
}
// Start background retry
go f.startRetryConnect()
return
}
// Connected successfully
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Infof("GCS connection established to bucket %s", f.CommonFileSystem.Location)
}
}
// startRetryConnect retries connection every 30 seconds until success.
func (f *fileSystem) startRetryConnect() {
if f.CommonFileSystem.IsConnected() || f.CommonFileSystem.IsRetryDisabled() {
return
}
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
if f.CommonFileSystem.IsConnected() || f.CommonFileSystem.IsRetryDisabled() {
return
}
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
err := f.CommonFileSystem.Connect(ctx)
cancel()
if err == nil {
// Success - exit retry loop
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Infof("GCS connection restored to bucket %s", f.CommonFileSystem.Location)
}
return
}
// Still failing - log and continue retrying
if f.CommonFileSystem.Logger != nil {
f.CommonFileSystem.Logger.Debugf("GCS retry failed, will try again: %v", err)
}
}
}
================================================
FILE: pkg/gofr/datasource/file/gcs/fs_test.go
================================================
package gcs
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/file"
)
func TestNew_NilConfig(t *testing.T) {
fs := New(nil)
require.NotNil(t, fs)
assert.Empty(t, fs.(*fileSystem).CommonFileSystem.Location)
}
func TestNew_EmptyBucketName(t *testing.T) {
config := &Config{BucketName: ""}
fs := New(config)
require.NotNil(t, fs)
assert.Empty(t, fs.(*fileSystem).CommonFileSystem.Location)
}
func TestNew_ConnectionFailure_StartsRetry(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
BucketName: "non-existent-bucket",
CredentialsJSON: `{"type":"service_account","project_id":"test"}`,
}
fs := New(config)
require.NotNil(t, fs)
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
// Expect warning about background retry (with error in format string)
mockLogger.EXPECT().Errorf(
"GCS bucket %s not available, starting background retry: %v",
"non-existent-bucket",
gomock.Any(), // Error message varies
)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// Now connect
fs.Connect()
time.Sleep(100 * time.Millisecond)
fs.(*fileSystem).CommonFileSystem.SetDisableRetry(true)
}
func TestNew_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{
BucketName: "test-bucket",
EndPoint: "http://localhost:4443",
}
fs := New(config)
require.NotNil(t, fs)
// Inject logger and metrics (mimicking AddFileStore behavior)
fs.UseLogger(mockLogger)
fs.UseMetrics(mockMetrics)
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any())
mockLogger.EXPECT().Infof("GCS connection established to bucket %s", "test-bucket").MaxTimes(1)
mockLogger.EXPECT().Errorf(
"GCS bucket %s not available, starting background retry: %v",
"test-bucket",
gomock.Any(),
).MaxTimes(1)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any())
// Now connect
fs.Connect()
// If connected, verify state
if fs.(*fileSystem).CommonFileSystem.IsConnected() {
t.Log("Successfully connected to GCS emulator")
} else {
t.Log("GCS emulator not available, retry started")
fs.(*fileSystem).CommonFileSystem.SetDisableRetry(true)
}
}
func TestGCSFileSystem_Observe_ProviderName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Location: "test-bucket",
Logger: mockLogger,
Metrics: mockMetrics,
ProviderName: "GCS",
},
}
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
file.AppFileStats,
gomock.Any(),
"type", gomock.Any(),
"status", gomock.Any(),
"provider", "GCS",
)
mockLogger.EXPECT().Debug(gomock.Any()).Do(func(log any) {
opLog, ok := log.(*file.OperationLog)
require.True(t, ok)
assert.Equal(t, "GCS", opLog.Provider)
})
operation := file.OpConnect
startTime := time.Now()
status := "SUCCESS"
message := "test message"
fs.Observe(operation, startTime, &status, &message)
}
func TestConnect_AlreadyConnected(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{BucketName: "test-bucket", EndPoint: "http://localhost:4443"}
adapter := &storageAdapter{cfg: config}
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: config.BucketName,
Logger: mockLogger,
Metrics: mockMetrics,
},
}
// Manually mark as connected
fs.CommonFileSystem.SetConnected(true)
// Should not call any logger methods (fast-path)
fs.Connect()
assert.True(t, fs.CommonFileSystem.IsConnected())
}
func TestStartRetryConnect_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := file.NewMockLogger(ctrl)
mockMetrics := file.NewMockMetrics(ctrl)
config := &Config{BucketName: "test-bucket", EndPoint: "http://localhost:4443"}
adapter := &storageAdapter{cfg: config}
fs := &fileSystem{
CommonFileSystem: &file.CommonFileSystem{
Provider: adapter,
Location: config.BucketName,
Logger: mockLogger,
Metrics: mockMetrics,
},
}
// Expect histogram registration
mockMetrics.EXPECT().NewHistogram(file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
// Expect debug logs for observe
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), file.AppFileStats, gomock.Any(), gomock.Any()).AnyTimes()
// If connection succeeds, expect success log
mockLogger.EXPECT().Infof("connected to %s", config.BucketName).MaxTimes(1)
mockLogger.EXPECT().Infof("GCS connection restored to bucket %s", config.BucketName).MaxTimes(1)
// If connection fails, expect debug retry log
mockLogger.EXPECT().Debugf("GCS retry attempt failed, will try again in 30s").AnyTimes()
// Start retry with short interval
done := make(chan bool)
go func() {
time.Sleep(2 * time.Second)
fs.CommonFileSystem.SetDisableRetry(true)
done <- true
}()
go fs.startRetryConnect()
<-done
}
================================================
FILE: pkg/gofr/datasource/file/gcs/gcs_cloud_test.go
================================================
package gcs_test
import (
"testing"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/datasource/file/gcs"
)
// TestNew_ReturnsCloudFileSystem verifies that New() returns a value that satisfies
// file.CloudFileSystem at compile time (implicit) and at runtime via AsCloud.
func TestNew_ReturnsCloudFileSystem(t *testing.T) {
cfg := &gcs.Config{BucketName: "test-bucket"}
cfs := gcs.New(cfg)
if cfs == nil {
t.Fatal("expected non-nil CloudFileSystem from New()")
}
// AsCloud must succeed because New() explicitly declares CloudFileSystem.
if _, ok := file.AsCloud(cfs); !ok {
t.Fatal("AsCloud should succeed for a value returned by gcs.New()")
}
}
================================================
FILE: pkg/gofr/datasource/file/gcs/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/file/gcs
go 1.25.0
require (
cloud.google.com/go/storage v1.59.1
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
golang.org/x/oauth2 v0.36.0
google.golang.org/api v0.272.0
)
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.18.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/file/gcs/go.sum
================================================
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
cloud.google.com/go/storage v1.59.1 h1:DXAZLcTimtiXdGqDSnebROVPd9QvRsFVVlptz02Wk58=
cloud.google.com/go/storage v1.59.1/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.18.0 h1:jxP5Uuo3bxm3M6gGtV94P4lliVetoCB4Wk2x8QA86LI=
github.com/googleapis/gax-go/v2 v2.18.0/go.mod h1:uSzZN4a356eRG985CzJ3WfbFSpqkLTjsnhWGJR6EwrE=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
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/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d h1:vsOm753cOAMkt76efriTCDKjpCbK18XGHMJHo0JUKhc=
google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:0oz9d7g9QLSdv9/lgbIjowW1JoxMbxmBVNe8i6tORJI=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d h1:EocjzKLywydp5uZ5tJ79iP6Q0UjDnyiHkGRWxuPBP8s=
google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:48U2I+QQUYhsFrg2SY6r+nJzeOtjey7j//WBESw+qyQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/file/gcs/storage_adapter.go
================================================
package gcs
import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/url"
"strings"
"time"
"cloud.google.com/go/storage"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/file"
"golang.org/x/oauth2/google"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
)
var (
// Storage adapter errors.
errGCSConfigNil = errors.New("GCS config is nil")
errGCSClientNotInitialized = errors.New("GCS client or bucket is not initialized")
errEmptyObjectName = errors.New("object name is empty")
errInvalidOffset = errors.New("invalid offset: must be >= 0")
errEmptySourceOrDest = errors.New("source and destination names cannot be empty")
errSameSourceAndDest = errors.New("source and destination are the same")
errFailedToCreateReader = errors.New("failed to create reader")
errFailedToCreateRangeReader = errors.New("failed to create range reader")
errObjectNotFound = errors.New("object not found")
errFailedToGetObjectAttrs = errors.New("failed to get object attrs")
errFailedToDeleteObject = errors.New("failed to delete object")
errFailedToCopyObject = errors.New("failed to copy object")
errFailedToListObjects = errors.New("failed to list objects")
errFailedToListDirectory = errors.New("failed to list directory")
// Signed URL errors.
errGCSBucketNotConfigured = errors.New("GCS bucket name is not configured")
errInvalidPrivateKeyPEM = errors.New("invalid private key PEM")
errInvalidPrivateKeyFormat = errors.New("invalid private key format")
errExpiryMustBePositive = errors.New("expiry duration must be positive")
errInvalidContentType = errors.New("invalid Content-Type format")
)
const (
contentTypeDirectory = "application/x-directory"
contentTypePartsCount = 2
)
// storageAdapter adapts GCS client to implement file.StorageProvider.
type storageAdapter struct {
cfg *Config
client *storage.Client
bucket *storage.BucketHandle
// saEmail and saPrivateKey hold parsed service-account credentials for signed URL
// generation. They are populated once during Connect() and reused on every call to
// SignedURL(), avoiding repeated JSON+PEM parsing per request.
// When empty, bucket.SignedURL falls back to the client's ambient credentials
// (Workload Identity, Application Default Credentials, etc.).
saEmail string
saPrivateKey []byte
logger datasource.Logger
}
// Connect initializes the GCS client and validates bucket access.
func (s *storageAdapter) Connect(ctx context.Context) error {
// fast-path
if s.client != nil && s.bucket != nil {
return nil
}
if s.cfg == nil {
return errGCSConfigNil
}
client, err := s.createStorageClient(ctx)
if err != nil {
return err
}
bucket := client.Bucket(s.cfg.BucketName)
if _, err := bucket.Attrs(ctx); err != nil {
_ = client.Close()
return fmt.Errorf("bucket validation failed: %w", err)
}
// Parse and cache service-account credentials for signed URL signing.
// Done once at connect time so every SignedURL() call reuses the result instead of
// repeating JSON unmarshal + PEM decode + key parse on the hot path.
// When CredentialsJSON is absent (Workload Identity / ADC), saEmail and saPrivateKey
// remain empty and bucket.SignedURL will use the client's ambient credentials.
// If parsing fails we log a warning and continue — the GCS client is already valid and
// all non-signed-URL operations will work normally. Signed URL calls will fall back to
// IAM-based signing via ambient credentials.
if s.cfg.CredentialsJSON != "" {
email, privateKey, parseErr := parseServiceAccountCredentials(s.cfg.CredentialsJSON)
if parseErr != nil {
if s.logger != nil {
s.logger.Errorf("credentials cannot be used for signed URLs: %v; signed URL calls will use ambient credentials", parseErr)
}
} else {
s.saEmail = email
s.saPrivateKey = privateKey
}
}
s.client = client
s.bucket = bucket
return nil
}
// createStorageClient creates a GCS storage client based on the configured authentication method.
func (s *storageAdapter) createStorageClient(ctx context.Context) (*storage.Client, error) {
switch {
case s.cfg.EndPoint != "":
return storage.NewClient(ctx, option.WithEndpoint(s.cfg.EndPoint), option.WithoutAuthentication())
case s.cfg.CredentialsJSON != "":
conf, err := google.JWTConfigFromJSON([]byte(s.cfg.CredentialsJSON), storage.ScopeFullControl)
if err != nil {
return nil, fmt.Errorf("failed to create storage client: %w", err)
}
return storage.NewClient(ctx, option.WithTokenSource(conf.TokenSource(ctx)))
default:
return storage.NewClient(ctx)
}
}
// Health checks if the GCS connection is healthy by verifying bucket access.
func (s *storageAdapter) Health(ctx context.Context) error {
if s.client == nil || s.bucket == nil {
return errGCSClientNotInitialized
}
_, err := s.bucket.Attrs(ctx)
if err != nil {
return fmt.Errorf("GCS health check failed: %w", err)
}
return nil
}
// Close closes the GCS client connection.
func (s *storageAdapter) Close() error {
if s.client != nil {
return s.client.Close()
}
return nil
}
// NewReader creates a reader for the given object.
func (s *storageAdapter) NewReader(ctx context.Context, name string) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
reader, err := s.bucket.Object(name).NewReader(ctx)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateReader, name, err)
}
return reader, nil
}
// NewRangeReader creates a range reader for the given object.
func (s *storageAdapter) NewRangeReader(ctx context.Context, name string, offset, length int64) (io.ReadCloser, error) {
if name == "" {
return nil, errEmptyObjectName
}
if offset < 0 {
return nil, fmt.Errorf("%w (got: %d)", errInvalidOffset, offset)
}
reader, err := s.bucket.Object(name).NewRangeReader(ctx, offset, length)
if err != nil {
return nil, fmt.Errorf("%w for %q: %w", errFailedToCreateRangeReader, name, err)
}
return reader, nil
}
// NewWriter creates a writer for the given object.
// Note: GCS NewWriter never returns an error synchronously; errors are deferred until Write/Close.
func (s *storageAdapter) NewWriter(ctx context.Context, name string) io.WriteCloser {
if name == "" {
return &failWriter{err: errEmptyObjectName}
}
if s.bucket == nil {
return &failWriter{err: errGCSClientNotInitialized}
}
return s.bucket.Object(name).NewWriter(ctx)
}
// NewWriterWithOptions implements MetadataWriter.
// Note: ContentType is passed to GCS as-is without format validation; GCS itself accepts any
// string as content-type on upload. Strict type/subtype format validation only applies to
// SignedURL via validateSignedURLInput, where the value is included in the request signature.
func (s *storageAdapter) NewWriterWithOptions(ctx context.Context, name string, opts *file.FileOptions) io.WriteCloser {
if name == "" {
return &failWriter{err: errEmptyObjectName}
}
if s.bucket == nil {
return &failWriter{err: errGCSClientNotInitialized}
}
w := s.bucket.Object(name).NewWriter(ctx)
if opts != nil {
if opts.ContentType != "" {
w.ContentType = opts.ContentType
}
if opts.ContentDisposition != "" {
w.ContentDisposition = opts.ContentDisposition
}
if opts.Metadata != nil {
w.Metadata = opts.Metadata
}
}
return w
}
// failWriter is a helper for NewWriter validation errors.
type failWriter struct {
err error
}
func (fw *failWriter) Write([]byte) (int, error) {
return 0, fw.err
}
func (fw *failWriter) Close() error {
return fw.err
}
// StatObject returns metadata for the given object.
func (s *storageAdapter) StatObject(ctx context.Context, name string) (*file.ObjectInfo, error) {
if name == "" {
return nil, errEmptyObjectName
}
attrs, err := s.bucket.Object(name).Attrs(ctx)
if err != nil {
if errors.Is(err, storage.ErrObjectNotExist) {
return nil, fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return nil, fmt.Errorf("%w for %q: %w", errFailedToGetObjectAttrs, name, err)
}
return &file.ObjectInfo{
Name: attrs.Name,
Size: attrs.Size,
ContentType: attrs.ContentType,
LastModified: attrs.Updated,
IsDir: attrs.ContentType == contentTypeDirectory,
}, nil
}
// DeleteObject deletes the object with the given name.
func (s *storageAdapter) DeleteObject(ctx context.Context, name string) error {
if name == "" {
return errEmptyObjectName
}
attrs, err := s.bucket.Object(name).Attrs(ctx)
if err != nil {
if errors.Is(err, storage.ErrObjectNotExist) {
return fmt.Errorf("%w %q: %w", errObjectNotFound, name, err)
}
return fmt.Errorf("%w for %q: %w", errFailedToGetObjectAttrs, name, err)
}
err = s.bucket.Object(name).If(storage.Conditions{GenerationMatch: attrs.Generation}).Delete(ctx)
if err != nil {
return fmt.Errorf("%w %q: %w", errFailedToDeleteObject, name, err)
}
return nil
}
// CopyObject copies an object from src to dst.
func (s *storageAdapter) CopyObject(ctx context.Context, src, dst string) error {
if src == "" || dst == "" {
return errEmptySourceOrDest
}
if src == dst {
return errSameSourceAndDest
}
srcObj := s.bucket.Object(src)
dstObj := s.bucket.Object(dst)
_, err := dstObj.CopierFrom(srcObj).Run(ctx)
if err != nil {
return fmt.Errorf("%w from %q to %q: %w", errFailedToCopyObject, src, dst, err)
}
return nil
}
// ListObjects lists all objects with the given prefix.
func (s *storageAdapter) ListObjects(ctx context.Context, prefix string) ([]string, error) {
var objects []string
it := s.bucket.Objects(ctx, &storage.Query{Prefix: prefix})
for {
obj, err := it.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
return nil, fmt.Errorf("%w with prefix %q: %w", errFailedToListObjects, prefix, err)
}
objects = append(objects, obj.Name)
}
return objects, nil
}
// ListDir lists objects and prefixes (directories) under the given prefix.
func (s *storageAdapter) ListDir(ctx context.Context, prefix string) ([]file.ObjectInfo, []string, error) {
var objects []file.ObjectInfo
var prefixes []string
it := s.bucket.Objects(ctx, &storage.Query{
Prefix: prefix,
Delimiter: "/",
})
for {
obj, err := it.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
return nil, nil, fmt.Errorf("%w %q: %w", errFailedToListDirectory, prefix, err)
}
if obj.Prefix != "" {
prefixes = append(prefixes, obj.Prefix)
continue
}
objects = append(objects, file.ObjectInfo{
Name: obj.Name,
Size: obj.Size,
ContentType: obj.ContentType,
LastModified: obj.Updated,
IsDir: false,
})
}
return objects, prefixes, nil
}
// validateSignedURLInput validates input parameters for signed URL generation.
func validateSignedURLInput(name string, expiry time.Duration, opts *file.FileOptions) error {
if name == "" {
return errEmptyObjectName
}
if expiry <= 0 {
return errExpiryMustBePositive
}
if opts != nil && opts.ContentType != "" {
parts := strings.SplitN(opts.ContentType, "/", contentTypePartsCount)
if len(parts) != contentTypePartsCount || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("%w: %q", errInvalidContentType, opts.ContentType)
}
}
return nil
}
// parseServiceAccountCredentials extracts email and private key from credentials JSON.
func parseServiceAccountCredentials(credentialsJSON string) (email string, privateKey []byte, err error) {
var cred struct {
ClientEmail string `json:"client_email"`
PrivateKey string `json:"private_key"`
}
if err := json.Unmarshal([]byte(credentialsJSON), &cred); err != nil {
return "", nil, fmt.Errorf("failed to parse credentials: %w", err)
}
block, _ := pem.Decode([]byte(cred.PrivateKey))
if block == nil {
return "", nil, errInvalidPrivateKeyPEM
}
// Validate key format
if err := validatePrivateKey(block.Bytes); err != nil {
return "", nil, err
}
return cred.ClientEmail, block.Bytes, nil
}
// validatePrivateKey checks if the private key can be parsed as PKCS8 or PKCS1.
func validatePrivateKey(keyBytes []byte) error {
if _, err := x509.ParsePKCS8PrivateKey(keyBytes); err != nil {
if _, err2 := x509.ParsePKCS1PrivateKey(keyBytes); err2 != nil {
return fmt.Errorf("%w: PKCS8: %s, PKCS1: %s", errInvalidPrivateKeyFormat, err.Error(), err2.Error())
}
}
return nil
}
// buildSignedURLOptions constructs GCS SignedURLOptions with optional metadata.
// When email and privateKey are empty (Workload Identity / ADC deployments), the
// GoogleAccessID and PrivateKey fields are intentionally left unset so that
// bucket.SignedURL falls back to IAM-based signing via the client's credentials.
func buildSignedURLOptions(email string, privateKey []byte, expiry time.Duration, opts *file.FileOptions) *storage.SignedURLOptions {
signedOpts := &storage.SignedURLOptions{
Method: "GET",
Expires: time.Now().Add(expiry),
// V4 is the current recommended signing scheme; it supports additional
// query parameters (e.g., response-content-disposition) and is required
// for credentials that delegate to IAM signBlob (Workload Identity).
Scheme: storage.SigningSchemeV4,
}
// Only populate explicit HMAC credentials when available.
if email != "" {
signedOpts.GoogleAccessID = email
}
if len(privateKey) > 0 {
signedOpts.PrivateKey = privateKey
}
if opts == nil {
return signedOpts
}
if opts.ContentType != "" {
signedOpts.ContentType = opts.ContentType
}
if opts.ContentDisposition != "" {
if signedOpts.QueryParameters == nil {
signedOpts.QueryParameters = make(url.Values)
}
signedOpts.QueryParameters.Set("response-content-disposition", sanitizeContentDisposition(opts.ContentDisposition))
}
return signedOpts
}
// sanitizeContentDisposition removes newline characters to prevent header injection.
func sanitizeContentDisposition(value string) string {
sanitized := strings.ReplaceAll(value, "\r", "")
sanitized = strings.ReplaceAll(sanitized, "\n", "")
return sanitized
}
// SignedURL generates a time-limited signed URL for the given object.
//
// Signing strategy (in priority order):
// 1. Explicit HMAC: when CredentialsJSON was provided at construction time, the
// parsed private key cached during Connect() is used for local RSA signing — no
// additional network calls are needed.
// 2. Ambient credentials: when no CredentialsJSON is provided (e.g., Workload Identity
// on GKE/Cloud Run, Application Default Credentials), bucket.SignedURL delegates
// signing to the IAM signBlob API using the client's existing auth token.
//
// The signed URL always uses V4 signing and defaults to the GET method.
func (s *storageAdapter) SignedURL(ctx context.Context, name string, expiry time.Duration, opts *file.FileOptions) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
if s.cfg == nil || s.cfg.BucketName == "" {
return "", errGCSBucketNotConfigured
}
if s.bucket == nil {
return "", errGCSClientNotInitialized
}
if err := validateSignedURLInput(name, expiry, opts); err != nil {
return "", err
}
// saEmail and saPrivateKey are populated during Connect() when CredentialsJSON is
// provided. When empty, bucket.SignedURL uses IAM-based signing (Workload Identity).
signedOpts := buildSignedURLOptions(s.saEmail, s.saPrivateKey, expiry, opts)
signedURL, err := s.bucket.SignedURL(name, signedOpts)
if err != nil {
return "", fmt.Errorf("failed to generate signed URL: %w", err)
}
return rewriteSignedURLEndpoint(signedURL, s.cfg.EndPoint), nil
}
// rewriteSignedURLEndpoint replaces the scheme and host of a GCS signed URL with those
// of a custom endpoint. This is used to redirect signed URLs to a local emulator
// (e.g., fake-gcs-server) during integration tests without changing the path or query.
func rewriteSignedURLEndpoint(signedURL, endpoint string) string {
if endpoint == "" {
return signedURL
}
ep, err := url.Parse(endpoint)
if err != nil || ep.Scheme == "" || ep.Host == "" {
return signedURL
}
parsed, err := url.Parse(signedURL)
if err != nil {
return signedURL
}
parsed.Scheme = ep.Scheme
parsed.Host = ep.Host
return parsed.String()
}
================================================
FILE: pkg/gofr/datasource/file/gcs/storage_adapter_test.go
================================================
package gcs
import (
"bytes"
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"cloud.google.com/go/storage"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/file"
"google.golang.org/api/option"
)
var errTest = errors.New("test error")
func TestStorageAdapter_Connect(t *testing.T) {
// Test 1: Nil config should return error
adapter := &storageAdapter{}
err := adapter.Connect(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), "GCS config is nil")
}
func TestStorageAdapter_Health_NilClient(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.Health(context.Background())
require.Error(t, err)
assert.ErrorIs(t, err, errGCSClientNotInitialized)
}
func TestStorageAdapter_Close_NilClient(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.Close()
assert.NoError(t, err)
}
func TestStorageAdapter_NewReader_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewReader(context.Background(), "")
require.Error(t, err)
assert.Nil(t, reader)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewRangeReader_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewRangeReader(context.Background(), "", 0, 100)
require.Error(t, err)
assert.Nil(t, reader)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewRangeReader_InvalidOffset(t *testing.T) {
adapter := &storageAdapter{}
reader, err := adapter.NewRangeReader(context.Background(), "file.txt", -1, 100)
require.Error(t, err)
assert.Nil(t, reader)
assert.ErrorIs(t, err, errInvalidOffset)
}
func TestStorageAdapter_NewWriter_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
writer := adapter.NewWriter(context.Background(), "")
require.NotNil(t, writer)
// Verify it's a fail writer
n, err := writer.Write([]byte("test"))
assert.Equal(t, 0, n)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewWriter_NilBucket(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{BucketName: "bucket"}}
writer := adapter.NewWriter(context.Background(), "obj.csv")
require.NotNil(t, writer)
_, err := writer.Write([]byte("data"))
assert.ErrorIs(t, err, errGCSClientNotInitialized)
}
func TestStorageAdapter_StatObject_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
info, err := adapter.StatObject(context.Background(), "")
require.Error(t, err)
assert.Nil(t, info)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_DeleteObject_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.DeleteObject(context.Background(), "")
require.Error(t, err)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_CopyObject_EmptySource(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "", "dest.txt")
require.Error(t, err)
assert.ErrorIs(t, err, errEmptySourceOrDest)
}
func TestStorageAdapter_CopyObject_EmptyDestination(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "source.txt", "")
require.Error(t, err)
assert.ErrorIs(t, err, errEmptySourceOrDest)
}
func TestStorageAdapter_CopyObject_BothEmpty(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "", "")
require.Error(t, err)
assert.ErrorIs(t, err, errEmptySourceOrDest)
}
func TestStorageAdapter_CopyObject_SameSourceAndDest(t *testing.T) {
adapter := &storageAdapter{}
err := adapter.CopyObject(context.Background(), "file.txt", "file.txt")
require.Error(t, err)
assert.ErrorIs(t, err, errSameSourceAndDest)
}
func TestFailWriter_Write(t *testing.T) {
testErr := errTest
fw := &failWriter{err: testErr}
n, err := fw.Write([]byte("test"))
assert.Equal(t, 0, n)
assert.Equal(t, testErr, err)
}
func TestFailWriter_Close(t *testing.T) {
testErr := errTest
fw := &failWriter{err: testErr}
err := fw.Close()
assert.Equal(t, testErr, err)
}
func TestFailWriter_WriteEmptyObjectNameError(t *testing.T) {
fw := &failWriter{err: errEmptyObjectName}
n, err := fw.Write([]byte("data"))
assert.Equal(t, 0, n)
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestFailWriter_CloseEmptyObjectNameError(t *testing.T) {
fw := &failWriter{err: errEmptyObjectName}
err := fw.Close()
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestMockStorageProvider_NewReader_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockProvider := file.NewMockStorageProvider(ctrl)
expectedReader := io.NopCloser(strings.NewReader("test data"))
mockProvider.EXPECT().
NewReader(gomock.Any(), "test.txt").
Return(expectedReader, nil)
reader, err := mockProvider.NewReader(context.Background(), "test.txt")
require.NoError(t, err)
assert.Equal(t, expectedReader, reader)
}
func TestStorageAdapter_ListObjects_Success(t *testing.T) {
// Fake GCS server returning two objects for the given prefix
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
// Expect path like /storage/v1/b//o
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"kind":"storage#objects",
"items":[
{"name":"prefix/file1.txt"},
{"name":"prefix/file2.txt"}
]
}`))
})
srv := httptest.NewServer(handler)
defer srv.Close()
// Create client pointing to our fake server, and without auth
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{
client: client,
bucket: client.Bucket("test-bucket"),
}
objects, err := adapter.ListObjects(t.Context(), "prefix/")
require.NoError(t, err)
assert.Equal(t, []string{"prefix/file1.txt", "prefix/file2.txt"}, objects)
}
func TestStorageAdapter_ListDir_Success(t *testing.T) {
// Response with one file item and one prefix (subdir)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"kind":"storage#objects",
"prefixes":["prefix/subdir/"],
"items":[
{
"name":"prefix/file1.txt",
"size":"123",
"contentType":"text/plain",
"updated":"2020-01-01T00:00:00.000Z"
}
]
}`))
})
srv := httptest.NewServer(handler)
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{
client: client,
bucket: client.Bucket("test-bucket"),
}
files, dirs, err := adapter.ListDir(t.Context(), "prefix/")
require.NoError(t, err)
// Expect one file with matching fields and one dir prefix
require.Len(t, files, 1)
assert.Equal(t, "prefix/file1.txt", files[0].Name)
assert.Equal(t, int64(123), files[0].Size)
assert.Equal(t, "text/plain", files[0].ContentType)
require.Len(t, dirs, 1)
assert.Equal(t, "prefix/subdir/", dirs[0])
}
func TestStorageAdapter_ListDir_Error(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "forbidden", http.StatusForbidden)
})
srv := httptest.NewServer(handler)
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{
client: client,
bucket: client.Bucket("test-bucket"),
}
_, _, err = adapter.ListDir(t.Context(), "prefix/")
require.Error(t, err)
assert.Contains(t, err.Error(), errFailedToListDirectory.Error())
}
func TestStorageAdapter_NewReader_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Respond with raw bytes for any request that references the object name
// or explicitly asks for media/range. This covers variations of client requests.
if r.URL.Query().Get("alt") == "media" ||
r.Header.Get("Range") != "" ||
strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/octet-stream") ||
strings.Contains(r.URL.Path, "file.txt") ||
strings.Contains(r.URL.RawQuery, "name=file.txt") {
w.Header().Set("Content-Type", "application/octet-stream")
_, _ = w.Write([]byte("hello world"))
return
}
// fallback: return JSON metadata for other calls
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"name":"file.txt","size":"11","contentType":"text/plain","updated":"2020-01-01T00:00:00.000Z"}`))
})
srv := httptest.NewServer(handler)
defer srv.Close()
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
rc, err := adapter.NewReader(ctx, "file.txt")
require.NoError(t, err)
defer rc.Close()
data, err := io.ReadAll(rc)
require.NoError(t, err)
assert.Equal(t, "hello world", string(data))
}
func TestStorageAdapter_StatObject_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// metadata path (JSON)
if strings.Contains(r.URL.Path, "/o/") && !strings.Contains(r.URL.RawQuery, "alt=media") {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"name":"prefix/file1.txt",
"size":"123",
"contentType":"text/plain",
"updated":"2020-01-01T00:00:00.000Z",
"generation":"42"
}`))
return
}
http.NotFound(w, r)
})
srv := httptest.NewServer(handler)
defer srv.Close()
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
info, err := adapter.StatObject(ctx, "prefix/file1.txt")
require.NoError(t, err)
assert.Equal(t, "prefix/file1.txt", info.Name)
assert.Equal(t, int64(123), info.Size)
assert.Equal(t, "text/plain", info.ContentType)
assert.False(t, info.IsDir)
}
func TestStorageAdapter_DeleteObject_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// For attr check: GET metadata
if r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/o/") && !strings.Contains(r.URL.RawQuery, "alt=media") {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"name":"to-delete.txt",
"size":"10",
"contentType":"text/plain",
"updated":"2020-01-01T00:00:00.000Z",
"generation":"99"
}`))
return
}
// For delete calls the client issues DELETE (may include ifGenerationMatch param)
if r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "/o/") {
w.WriteHeader(http.StatusNoContent)
return
}
http.NotFound(w, r)
})
srv := httptest.NewServer(handler)
defer srv.Close()
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
err = adapter.DeleteObject(ctx, "to-delete.txt")
require.NoError(t, err)
}
func TestStorageAdapter_NewRangeReader_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// treat as download if Range header present or alt=media query
if r.Header.Get("Range") != "" || r.URL.Query().Get("alt") == "media" {
const totalSize = int64(20)
start, end, err := parseRangeHeader(r.Header.Get("Range"), totalSize)
if err != nil {
http.Error(w, "invalid range", http.StatusRequestedRangeNotSatisfiable)
return
}
length := end - start + 1
if length < 0 {
http.Error(w, "invalid range", http.StatusRequestedRangeNotSatisfiable)
return
}
// build body that begins with "partial"
body := []byte("partial")
if int64(len(body)) < length {
body = append(body, bytes.Repeat([]byte("x"), int(length)-len(body))...)
} else if int64(len(body)) > length {
body = body[:length]
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, totalSize))
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(body)), 10))
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write(body)
return
}
// metadata fallback
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"name":"file.txt","size":"20","contentType":"text/plain","updated":"2020-01-01T00:00:00.000Z"}`))
})
srv := httptest.NewServer(handler)
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
rc, err := adapter.NewRangeReader(t.Context(), "file.txt", 5, 10)
require.NoError(t, err)
defer rc.Close()
b := make([]byte, 7)
n, err := rc.Read(b)
require.NoError(t, err)
assert.Equal(t, 7, n)
assert.Equal(t, "partial", string(b))
}
func TestStorageAdapter_CopyObject_Success(t *testing.T) {
// Minimal handler that returns a completed rewrite response for copier.Run
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/rewriteTo/") {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"kind":"storage#rewriteResponse","done":true,"resource":{}}`))
return
}
// unexpected -> 404
http.NotFound(w, r)
})
srv := httptest.NewServer(handler)
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
err = adapter.CopyObject(t.Context(), "source.txt", "dest.txt")
require.NoError(t, err)
}
// parseRangeHeader parses a "Range" header like "bytes=5-14".
// Returns start,end (inclusive) or an error for invalid values.
func parseRangeHeader(rangeHdr string, total int64) (first, last int64, err error) {
start := int64(0)
end := total - 1
if rangeHdr == "" {
return start, end, nil
}
p0, p1, err := splitRange(rangeHdr)
if err != nil {
return 0, 0, err
}
if p0 != "" {
v, err := parseNonNegativeInt(p0)
if err != nil {
return 0, 0, err
}
start = v
}
if p1 != "" {
v, err := parseNonNegativeInt(p1)
if err != nil {
return 0, 0, err
}
end = v
}
if start < 0 {
start = 0
}
if end >= total {
end = total - 1
}
if end < start {
return 0, 0, errTest
}
return start, end, nil
}
// splitRange validates the header prefix and returns the two side strings.
// e.g. "bytes=5-14" -> "5","14".
func splitRange(rangeHdr string) (firstString, secondString string, err error) {
const prefix = "bytes="
if !strings.HasPrefix(rangeHdr, prefix) {
return "", "", errTest
}
parts := strings.SplitN(strings.TrimPrefix(rangeHdr, prefix), "-", 2)
if len(parts) != 2 {
return "", "", errTest
}
return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), nil
}
// parseNonNegativeInt parses a base-10 integer and rejects negatives.
func parseNonNegativeInt(s string) (int64, error) {
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return 0, err
}
if v < 0 {
return 0, errTest
}
return v, nil
}
func TestStorageAdapter_StatObject_NotFound(t *testing.T) {
// Handler returns 404 for object metadata requests to simulate object-not-exist.
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/o/") && !strings.Contains(r.URL.RawQuery, "alt=media") {
http.Error(w, "not found", http.StatusNotFound)
return
}
http.NotFound(w, r)
})
srv := httptest.NewServer(handler)
defer srv.Close()
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("bucket")}
_, err = adapter.StatObject(ctx, "missing.txt")
require.Error(t, err)
require.ErrorIs(t, err, errObjectNotFound)
assert.Contains(t, err.Error(), "missing.txt")
}
func TestStorageAdapter_Health_Success(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/b/") {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"name":"test-bucket","id":"1"}`))
return
}
http.NotFound(w, r)
})
srv := httptest.NewServer(handler)
defer srv.Close()
ctx := context.Background()
client, err := storage.NewClient(ctx, option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{client: client, bucket: client.Bucket("test-bucket")}
err = adapter.Health(ctx)
require.NoError(t, err)
}
func TestValidateSignedURLInput(t *testing.T) {
tests := []struct {
name string
objName string
expiry time.Duration
opts *file.FileOptions
wantErr error
}{
{
name: "valid input",
objName: "file.txt",
expiry: time.Hour,
opts: &file.FileOptions{ContentType: "text/plain"},
wantErr: nil,
},
{
name: "empty object name",
objName: "",
expiry: time.Hour,
wantErr: errEmptyObjectName,
},
{
name: "negative expiry",
objName: "file.txt",
expiry: -time.Hour,
wantErr: errExpiryMustBePositive,
},
{
name: "invalid content type",
objName: "file.txt",
expiry: time.Hour,
opts: &file.FileOptions{ContentType: "invalid"},
wantErr: errInvalidContentType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateSignedURLInput(tt.objName, tt.expiry, tt.opts)
if !errors.Is(err, tt.wantErr) {
t.Errorf("validateSignedURLInput() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
// ─── helpers for signed-URL tests ───────────────────────────────────────────
// generateTestPrivateKeyPEM generates a fresh RSA-2048 private key encoded as
// PKCS#8 PEM. Used only in tests; never use this key in production.
func generateTestPrivateKeyPEM(t *testing.T) string {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
var buf bytes.Buffer
require.NoError(t, pem.Encode(&buf, &pem.Block{Type: "PRIVATE KEY", Bytes: der}))
return buf.String()
}
// testCredentialsJSON returns a minimal service-account JSON string that
// parseServiceAccountCredentials can parse successfully.
func testCredentialsJSON(t *testing.T) string {
t.Helper()
keyPEM := generateTestPrivateKeyPEM(t)
encoded, err := json.Marshal(keyPEM)
require.NoError(t, err)
return fmt.Sprintf(`{"client_email":"test-sa@project.iam.gserviceaccount.com","private_key":%s}`, encoded)
}
// bucketAttrsHandler returns an HTTP handler that serves minimal GCS bucket-attrs
// responses so that storageAdapter.Connect() can pass its bucket validation step.
func bucketAttrsHandler(bucketName string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "/b/"+bucketName) && !strings.Contains(r.URL.Path, "/o/") {
w.Header().Set("Content-Type", "application/json")
_, _ = fmt.Fprintf(w, `{"name":%q}`, bucketName)
return
}
http.NotFound(w, r)
})
}
// ─── parseServiceAccountCredentials ─────────────────────────────────────────
func TestParseServiceAccountCredentials_Valid(t *testing.T) {
credJSON := testCredentialsJSON(t)
email, key, err := parseServiceAccountCredentials(credJSON)
require.NoError(t, err)
assert.Equal(t, "test-sa@project.iam.gserviceaccount.com", email)
assert.NotEmpty(t, key)
}
func TestParseServiceAccountCredentials_InvalidJSON(t *testing.T) {
_, _, err := parseServiceAccountCredentials("not-json")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to parse credentials")
}
func TestParseServiceAccountCredentials_EmptyPrivateKey(t *testing.T) {
credJSON := `{"client_email":"sa@project.iam.gserviceaccount.com","private_key":""}` //nolint:gosec // G101: test credentials
_, _, err := parseServiceAccountCredentials(credJSON)
require.ErrorIs(t, err, errInvalidPrivateKeyPEM)
}
func TestParseServiceAccountCredentials_InvalidPEM(t *testing.T) {
credJSON := `{"client_email":"sa@project.iam.gserviceaccount.com","private_key":"not-a-pem-block"}` //nolint:gosec // G101: test data
_, _, err := parseServiceAccountCredentials(credJSON)
require.ErrorIs(t, err, errInvalidPrivateKeyPEM)
}
func TestParseServiceAccountCredentials_InvalidKeyBytes(t *testing.T) {
// Valid PEM structure but garbage DER bytes that are not a parseable key.
garbage := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("garbage-key-bytes")})
encoded, err := json.Marshal(string(garbage))
require.NoError(t, err)
credJSON := fmt.Sprintf(`{"client_email":"sa@project.iam.gserviceaccount.com","private_key":%s}`, encoded)
_, _, err = parseServiceAccountCredentials(credJSON)
require.ErrorIs(t, err, errInvalidPrivateKeyFormat)
}
// ─── validatePrivateKey ──────────────────────────────────────────────────────
func TestValidatePrivateKey_PKCS8(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
require.NoError(t, validatePrivateKey(der))
}
func TestValidatePrivateKey_PKCS1(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der := x509.MarshalPKCS1PrivateKey(key)
require.NoError(t, validatePrivateKey(der))
}
func TestValidatePrivateKey_Invalid(t *testing.T) {
err := validatePrivateKey([]byte("not-a-key"))
require.ErrorIs(t, err, errInvalidPrivateKeyFormat)
}
// ─── buildSignedURLOptions ───────────────────────────────────────────────────
func TestBuildSignedURLOptions_NilOpts(t *testing.T) {
opts := buildSignedURLOptions("", nil, time.Hour, nil)
assert.Equal(t, "GET", opts.Method)
assert.Empty(t, opts.GoogleAccessID)
assert.Nil(t, opts.PrivateKey)
assert.Empty(t, opts.ContentType)
assert.Nil(t, opts.QueryParameters)
}
func TestBuildSignedURLOptions_WithExplicitCredentials(t *testing.T) {
opts := buildSignedURLOptions("sa@project.iam", []byte("key"), time.Hour, nil)
assert.Equal(t, "sa@project.iam", opts.GoogleAccessID)
assert.Equal(t, []byte("key"), opts.PrivateKey)
}
func TestBuildSignedURLOptions_WithContentType(t *testing.T) {
opts := buildSignedURLOptions("", nil, time.Hour, &file.FileOptions{ContentType: "image/png"})
assert.Equal(t, "image/png", opts.ContentType)
}
func TestBuildSignedURLOptions_WithContentDisposition(t *testing.T) {
opts := buildSignedURLOptions("", nil, time.Hour, &file.FileOptions{
ContentDisposition: "attachment; filename=report.csv",
})
require.NotNil(t, opts.QueryParameters)
assert.Equal(t, "attachment; filename=report.csv", opts.QueryParameters.Get("response-content-disposition"))
}
func TestBuildSignedURLOptions_ContentDispositionInjectionSanitised(t *testing.T) {
malicious := "attachment\r\nX-Injected: evil"
opts := buildSignedURLOptions("", nil, time.Hour, &file.FileOptions{
ContentDisposition: malicious,
})
got := opts.QueryParameters.Get("response-content-disposition")
assert.NotContains(t, got, "\r")
assert.NotContains(t, got, "\n")
}
// ─── sanitizeContentDisposition ─────────────────────────────────────────────
func TestSanitizeContentDisposition_Clean(t *testing.T) {
assert.Equal(t, "attachment; filename=file.pdf", sanitizeContentDisposition("attachment; filename=file.pdf"))
}
func TestSanitizeContentDisposition_StripsCRLF(t *testing.T) {
assert.Equal(t, "attachmentX-Evil: hdr", sanitizeContentDisposition("attachment\r\nX-Evil: hdr"))
}
func TestSanitizeContentDisposition_StripsCR(t *testing.T) {
assert.Equal(t, "attachmentX-Evil: hdr", sanitizeContentDisposition("attachment\rX-Evil: hdr"))
}
func TestSanitizeContentDisposition_StripsLF(t *testing.T) {
assert.Equal(t, "attachmentX-Evil: hdr", sanitizeContentDisposition("attachment\nX-Evil: hdr"))
}
// ─── rewriteSignedURLEndpoint ────────────────────────────────────────────────
func TestRewriteSignedURLEndpoint_NoEndpoint(t *testing.T) {
orig := "https://storage.googleapis.com/bucket/obj?sig=abc"
assert.Equal(t, orig, rewriteSignedURLEndpoint(orig, ""))
}
func TestRewriteSignedURLEndpoint_Rewrites(t *testing.T) {
orig := "https://storage.googleapis.com/bucket/obj?sig=abc"
result := rewriteSignedURLEndpoint(orig, "http://localhost:4443")
assert.Contains(t, result, "http://localhost:4443")
assert.Contains(t, result, "/bucket/obj")
assert.Contains(t, result, "sig=abc")
}
func TestRewriteSignedURLEndpoint_InvalidEndpoint(t *testing.T) {
orig := "https://storage.googleapis.com/bucket/obj"
// Pass a URL that url.Parse will fail on — the function should return the original.
result := rewriteSignedURLEndpoint(orig, "://bad-url")
assert.Equal(t, orig, result)
}
// ─── NewWriterWithOptions ────────────────────────────────────────────────────
func TestStorageAdapter_NewWriterWithOptions_EmptyName(t *testing.T) {
adapter := &storageAdapter{}
w := adapter.NewWriterWithOptions(context.Background(), "", nil)
require.NotNil(t, w)
_, err := w.Write([]byte("data"))
assert.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_NewWriterWithOptions_NilBucket(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{BucketName: "bucket"}}
w := adapter.NewWriterWithOptions(context.Background(), "obj.csv", nil)
require.NotNil(t, w)
_, err := w.Write([]byte("data"))
assert.ErrorIs(t, err, errGCSClientNotInitialized)
}
func TestStorageAdapter_NewWriterWithOptions_PropagatesOptions(t *testing.T) {
// We need a valid *storage.BucketHandle to construct a GCS writer; a fake server
// that never responds to anything is sufficient since NewWriterWithOptions itself
// does not make any network calls — the GCS writer only contacts the server on
// the first Write() / Close().
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{
cfg: &Config{BucketName: "bucket"},
client: client,
bucket: client.Bucket("bucket"),
}
opts := &file.FileOptions{
ContentType: "text/csv",
ContentDisposition: "attachment; filename=report.csv",
Metadata: map[string]string{"env": "test"},
}
// NewWriterWithOptions must return a non-nil writer without panicking.
// (Internal field propagation is verified by reading the *storage.Writer fields.)
w := adapter.NewWriterWithOptions(t.Context(), "report.csv", opts)
require.NotNil(t, w)
// The GCS writer buffers writes locally; Write() should not return an error
// before any network call is attempted.
_, err = w.Write([]byte("data"))
require.NoError(t, err)
// We do NOT call Close() here because that would initiate a real upload to the
// fake server and would fail with a 404. The purpose of this test is to verify
// that option fields are accepted without error, not that the upload succeeds.
}
// ─── SignedURL ───────────────────────────────────────────────────────────────
func TestStorageAdapter_SignedURL_NilBucket(t *testing.T) {
adapter := &storageAdapter{cfg: &Config{BucketName: "bucket"}}
_, err := adapter.SignedURL(context.Background(), "obj", time.Hour, nil)
require.ErrorIs(t, err, errGCSClientNotInitialized)
}
func TestStorageAdapter_SignedURL_NilOrEmptyConfig(t *testing.T) {
tests := []struct {
name string
adapter *storageAdapter
}{
{"nil config", &storageAdapter{}},
{"empty bucket", &storageAdapter{cfg: &Config{}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := tt.adapter.SignedURL(context.Background(), "obj", time.Hour, nil)
require.ErrorIs(t, err, errGCSBucketNotConfigured)
})
}
}
func TestStorageAdapter_SignedURL_CanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // already canceled
adapter := &storageAdapter{cfg: &Config{BucketName: "bucket"}}
_, err := adapter.SignedURL(ctx, "obj", time.Hour, nil)
require.ErrorIs(t, err, context.Canceled)
}
func TestStorageAdapter_SignedURL_InvalidInput(t *testing.T) {
// A fully initialized adapter; we only want to exercise the input validation path.
srv := httptest.NewServer(bucketAttrsHandler("bucket"))
defer srv.Close()
client, err := storage.NewClient(t.Context(), option.WithEndpoint(srv.URL), option.WithoutAuthentication())
require.NoError(t, err)
defer client.Close()
adapter := &storageAdapter{
cfg: &Config{BucketName: "bucket"},
client: client,
bucket: client.Bucket("bucket"),
}
_, err = adapter.SignedURL(context.Background(), "", time.Hour, nil)
require.ErrorIs(t, err, errEmptyObjectName)
}
func TestStorageAdapter_SignedURL_WithExplicitCredentials(t *testing.T) {
// Start a fake server that accepts bucket attrs (needed by Connect).
srv := httptest.NewServer(bucketAttrsHandler("test-bucket"))
defer srv.Close()
credJSON := testCredentialsJSON(t)
cfg := &Config{
BucketName: "test-bucket",
CredentialsJSON: credJSON,
EndPoint: srv.URL,
}
adapter := &storageAdapter{cfg: cfg}
require.NoError(t, adapter.Connect(t.Context()))
signedURL, err := adapter.SignedURL(t.Context(), "reports/q1.csv", time.Hour, nil)
require.NoError(t, err)
assert.Contains(t, signedURL, "test-bucket")
assert.Contains(t, signedURL, "reports")
assert.Contains(t, signedURL, "q1.csv")
// Signed URL must carry a signature parameter (V4 signing).
assert.Contains(t, signedURL, "X-Goog-Signature")
// Endpoint rewriting must have replaced the googleapis.com host.
assert.Contains(t, signedURL, "127.0.0.1")
}
func TestStorageAdapter_SignedURL_WithOptions(t *testing.T) {
srv := httptest.NewServer(bucketAttrsHandler("test-bucket"))
defer srv.Close()
credJSON := testCredentialsJSON(t)
cfg := &Config{
BucketName: "test-bucket",
CredentialsJSON: credJSON,
EndPoint: srv.URL,
}
adapter := &storageAdapter{cfg: cfg}
require.NoError(t, adapter.Connect(t.Context()))
opts := &file.FileOptions{
ContentType: "text/csv",
ContentDisposition: "attachment; filename=data.csv",
}
signedURL, err := adapter.SignedURL(t.Context(), "data.csv", time.Hour, opts)
require.NoError(t, err)
assert.Contains(t, signedURL, "data.csv")
assert.Contains(t, signedURL, "response-content-disposition")
}
// ─── Connect with credentials caching ────────────────────────────────────────
func TestStorageAdapter_Connect_CachesCredentials(t *testing.T) {
srv := httptest.NewServer(bucketAttrsHandler("test-bucket"))
defer srv.Close()
credJSON := testCredentialsJSON(t)
cfg := &Config{
BucketName: "test-bucket",
CredentialsJSON: credJSON,
EndPoint: srv.URL,
}
adapter := &storageAdapter{cfg: cfg}
require.NoError(t, adapter.Connect(t.Context()))
assert.NotEmpty(t, adapter.saEmail, "saEmail should be cached after successful connect")
assert.NotEmpty(t, adapter.saPrivateKey, "saPrivateKey should be cached after successful connect")
}
func TestStorageAdapter_Connect_InvalidCredentials_DoesNotFailConnect(t *testing.T) {
srv := httptest.NewServer(bucketAttrsHandler("test-bucket"))
defer srv.Close()
// Valid GCS credentials structure but with a non-parseable private key.
// Connect() must succeed — the GCS client works fine. The parse warning is logged and
// saEmail/saPrivateKey remain empty; signed URL calls fall back to ambient credentials.
garbage := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("garbage")})
encoded, err := json.Marshal(string(garbage))
require.NoError(t, err)
credJSON := fmt.Sprintf(`{"client_email":"sa@project.iam.gserviceaccount.com","private_key":%s}`, encoded)
cfg := &Config{
BucketName: "test-bucket",
CredentialsJSON: credJSON,
EndPoint: srv.URL,
}
adapter := &storageAdapter{cfg: cfg}
// Connect must succeed even with un-parseable signing credentials.
require.NoError(t, adapter.Connect(t.Context()))
// saEmail and saPrivateKey remain empty when credential parsing fails.
assert.Empty(t, adapter.saEmail)
assert.Empty(t, adapter.saPrivateKey)
}
func TestStorageAdapter_Connect_NoCredentials_DoesNotCache(t *testing.T) {
srv := httptest.NewServer(bucketAttrsHandler("test-bucket"))
defer srv.Close()
cfg := &Config{
BucketName: "test-bucket",
EndPoint: srv.URL,
}
adapter := &storageAdapter{cfg: cfg}
require.NoError(t, adapter.Connect(t.Context()))
// When no credentials JSON is set, the cached fields stay empty (Workload Identity path).
assert.Empty(t, adapter.saEmail)
assert.Empty(t, adapter.saPrivateKey)
}
================================================
FILE: pkg/gofr/datasource/file/helper.go
================================================
package file
import (
"errors"
"fmt"
"io"
"path"
"strings"
"google.golang.org/api/googleapi"
)
// ValidateSeekOffset validates and calculates the new offset for Seek operations.
func ValidateSeekOffset(whence int, offset, currentPos, length int64) (int64, error) {
var newOffset int64
switch whence {
case io.SeekStart:
newOffset = offset
case io.SeekEnd:
newOffset = length + offset
case io.SeekCurrent:
newOffset = currentPos + offset
default:
return 0, ErrOutOfRange
}
if newOffset < 0 || newOffset > length {
return 0, fmt.Errorf("%w: offset %d out of bounds [0, %d]", ErrOutOfRange, newOffset, length)
}
return newOffset, nil
}
// IsAlreadyExistsError checks if an error indicates an object already exists.
// Handles provider-specific error codes (e.g., GCS 409/412, S3 ResourceExists).
func IsAlreadyExistsError(err error) bool {
if err == nil {
return false
}
// GCS-specific error codes
var gErr *googleapi.Error
if errors.As(err, &gErr) {
return gErr.Code == 409 || gErr.Code == 412
}
// Fallback: check error message
errMsg := strings.ToLower(err.Error())
return strings.Contains(errMsg, "already exists") ||
strings.Contains(errMsg, "resource exists") ||
strings.Contains(errMsg, "409") ||
strings.Contains(errMsg, "412")
}
// GenerateCopyName creates a new file name with " copy N" suffix.
// Example: "file.txt" -> "file copy 1.txt".
func GenerateCopyName(original string, count int) string {
ext := path.Ext(original)
base := strings.TrimSuffix(original, ext)
return fmt.Sprintf("%s copy %d%s", base, count, ext)
}
================================================
FILE: pkg/gofr/datasource/file/interface.go
================================================
package file
import (
"context"
"errors"
"io"
"os"
"time"
)
// File represents a file in the filesystem.
type File interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
io.Writer
io.WriterAt
ReadAll() (RowReader, error)
Name() string
Size() int64
ModTime() time.Time
IsDir() bool
Mode() os.FileMode
Sys() any
}
// FileInfo : Any simulated or real file should implement this interface.
//
//nolint:revive // let's consider file.FileInfo doesn't sound repetitive
type FileInfo interface {
Name() string // base name of the file
Size() int64 // length in bytes for regular files; system-dependent for others
ModTime() time.Time // modification time
Mode() os.FileMode // file mode bits
IsDir() bool // abbreviation for Mode().IsDir()
}
type RowReader interface {
Next() bool
Scan(any) error
}
// StorageProvider abstracts low-level storage operations (stateless).
// This is the interface that each provider (GCS, S3, FTP, SFTP) must implement.
type StorageProvider interface {
Connect(ctx context.Context) error
NewReader(ctx context.Context, name string) (io.ReadCloser, error)
NewRangeReader(ctx context.Context, name string, offset, length int64) (io.ReadCloser, error)
NewWriter(ctx context.Context, name string) io.WriteCloser
DeleteObject(ctx context.Context, name string) error
CopyObject(ctx context.Context, src, dst string) error
StatObject(ctx context.Context, name string) (*ObjectInfo, error)
ListObjects(ctx context.Context, prefix string) ([]string, error)
ListDir(ctx context.Context, prefix string) (objects []ObjectInfo, prefixes []string, err error)
}
// MetadataWriter is an optional extension for StorageProvider.
type MetadataWriter interface {
NewWriterWithOptions(ctx context.Context, name string, opts *FileOptions) io.WriteCloser
}
// SignedURLProvider is an optional extension for StorageProvider.
type SignedURLProvider interface {
SignedURL(ctx context.Context, name string, expiry time.Duration, opts *FileOptions) (string, error)
}
// ObjectInfo represents cloud storage object metadata.
type ObjectInfo struct {
Name string
Size int64
ContentType string
LastModified time.Time
IsDir bool
}
// FileSystem : Any simulated or real filesystem should implement this interface.
//
//nolint:revive // let's consider file.FileSystem doesn't sound repetitive
type FileSystem interface {
// Create creates a file in the filesystem, returning the file and an
// error, if any happens.
Create(name string) (File, error)
// TODO - Lets make bucket constant for MkdirAll as well, we might create buckets from migrations
// Mkdir creates a directory in the filesystem, return an error if any
// happens.
Mkdir(name string, perm os.FileMode) error
// MkdirAll creates a directory path and all parents that does not exist
// yet.
MkdirAll(path string, perm os.FileMode) error
// Open opens a file, returning it or an error, if any happens.
Open(name string) (File, error)
// OpenFile opens a file using the given flags and the given mode.
OpenFile(name string, flag int, perm os.FileMode) (File, error)
// Remove removes a file identified by name, returning an error, if any
// happens.
Remove(name string) error
// RemoveAll removes a directory path and any children it contains. It
// does not fail if the path does not exist (return nil).
RemoveAll(path string) error
// Rename renames a file.
Rename(oldname, newname string) error
// ReadDir returns a list of files/directories present in the directory.
ReadDir(dir string) ([]FileInfo, error)
// Stat returns the file/directory information in the directory.
Stat(name string) (FileInfo, error)
// ChDir changes the current directory.
ChDir(dirname string) error
// Getwd returns the path of the current directory.
Getwd() (string, error)
}
// FileSystemProvider : Any simulated or real filesystem provider should implement this interface.
//
//nolint:revive // let's consider file.FileSystemProvider doesn't sound repetitive
type FileSystemProvider interface {
FileSystem
// UseLogger sets the logger for the FileSystem client.
UseLogger(logger any)
// UseMetrics sets the metrics for the FileSystem client.
UseMetrics(metrics any)
// Connect establishes a connection to FileSystem and registers metrics using the provided configuration when the client was Created.
Connect()
}
// ============================================================
// OPTIONAL CAPABILITY INTERFACES
// ============================================================
// FileOptions represents optional file metadata for Create/Sign operations.
//
//nolint:revive // keep name FileOptions for clarity across packages
type FileOptions struct {
ContentType string `json:"content_type,omitempty"`
ContentDisposition string `json:"content_disposition,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// AdvancedFileOperations extends FileSystem with metadata support.
type AdvancedFileOperations interface {
CreateWithOptions(ctx context.Context, name string, opts *FileOptions) (File, error)
}
// SignedURLGenerator provides secure, time-limited URL generation.
type SignedURLGenerator interface {
GenerateSignedURL(ctx context.Context, name string, expiry time.Duration, opts *FileOptions) (string, error)
}
// CloudFileSystem combines common cloud storage capabilities.
// It is a convenience interface for type assertions by consumers who need
// cloud-specific features like metadata support and signed URLs.
type CloudFileSystem interface {
FileSystemProvider
AdvancedFileOperations
SignedURLGenerator
}
// AsCloud is a convenience helper for checking if a FileSystemProvider
// supports cloud-specific features. It's equivalent to type assertion
// but provides a clearer intent in code.
//
// Example:
//
// if cfs, ok := file.AsCloud(fs); ok {
// url, _ := cfs.GenerateSignedURL(ctx, "file.csv", time.Hour, nil)
// }
func AsCloud(fs FileSystem) (CloudFileSystem, bool) {
cfs, ok := fs.(CloudFileSystem)
return cfs, ok
}
var (
// ErrSignedURLsNotSupported is returned when a provider does not implement signed URLs.
ErrSignedURLsNotSupported = errors.New("signed URLs not supported by provider")
ErrOutOfRange = errors.New("out of range")
ErrFileNotFound = os.ErrNotExist
)
const (
StatusSuccess = "SUCCESS"
StatusError = "ERROR"
MsgWriterClosed = "Writer closed successfully"
MsgReaderClosed = "Reader closed successfully"
)
================================================
FILE: pkg/gofr/datasource/file/local_fs.go
================================================
package file
import (
"context"
"io"
"os"
"path/filepath"
"gofr.dev/pkg/gofr/datasource"
)
const dirPerm = 0755
// localProvider implements StorageProvider for local filesystem.
type localProvider struct{}
// localFileSystem is a small adapter that exposes the no-arg Connect()
// required by FileSystemProvider and delegates to CommonFileSystem.
type localFileSystem struct {
*CommonFileSystem
}
// NewLocalFileSystem creates a FileSystem for local filesystem operations.
// It returns a FileSystemProvider so callers can treat it uniformly with other providers.
func NewLocalFileSystem(logger datasource.Logger) FileSystemProvider {
provider := &localProvider{}
cfs := &CommonFileSystem{
Provider: provider,
Location: "local",
Logger: logger,
Metrics: nil,
}
return &localFileSystem{CommonFileSystem: cfs}
}
// Connect implements the no-arg Connect for FileSystemProvider compatibility.
func (l *localFileSystem) Connect() {
// Local filesystem connect is a no-op but we call CommonFileSystem.Connect
// with a background context to set up any bookkeeping.
_ = l.CommonFileSystem.Connect(context.Background())
}
func (*localProvider) Connect(_ context.Context) error {
return nil
}
func (*localProvider) Health(_ context.Context) error {
return nil // Local FS is always healthy
}
func (*localProvider) Close() error {
return nil // Nothing to close
}
func (*localProvider) NewReader(_ context.Context, name string) (io.ReadCloser, error) {
return os.Open(name)
}
func (*localProvider) NewRangeReader(_ context.Context, name string, offset, length int64) (io.ReadCloser, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
if _, err := f.Seek(offset, io.SeekStart); err != nil {
_ = f.Close()
return nil, err
}
if length > 0 {
return &limitedReadCloser{f, length}, nil
}
return f, nil
}
func (*localProvider) NewWriter(_ context.Context, name string) io.WriteCloser {
// Create parent directories if needed
if err := os.MkdirAll(filepath.Dir(name), dirPerm); err != nil {
return &failWriter{err: err}
}
f, err := os.Create(name)
if err != nil {
return &failWriter{err: err}
}
return f
}
func (*localProvider) StatObject(_ context.Context, name string) (*ObjectInfo, error) {
info, err := os.Stat(name)
if err != nil {
return nil, err
}
return &ObjectInfo{
Name: info.Name(),
Size: info.Size(),
ContentType: "", // Local FS doesn't store content type
LastModified: info.ModTime(),
IsDir: info.IsDir(),
}, nil
}
func (*localProvider) DeleteObject(_ context.Context, name string) error {
return os.Remove(name)
}
func (*localProvider) CopyObject(_ context.Context, src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer func() { _ = srcFile.Close() }()
if mkdirErr := os.MkdirAll(filepath.Dir(dst), DefaultDirMode); mkdirErr != nil {
return mkdirErr
}
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer func() { _ = dstFile.Close() }()
_, err = io.Copy(dstFile, srcFile)
return err
}
func (*localProvider) ListObjects(_ context.Context, prefix string) ([]string, error) {
entries, err := os.ReadDir(prefix)
if err != nil {
return nil, err
}
var objects []string
for _, entry := range entries {
if !entry.IsDir() {
objects = append(objects, entry.Name())
}
}
return objects, nil
}
func (*localProvider) ListDir(_ context.Context, prefix string) ([]ObjectInfo, []string, error) {
entries, err := os.ReadDir(prefix)
if err != nil {
return nil, nil, err
}
var (
objects []ObjectInfo
dirs []string
)
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
continue
}
if entry.IsDir() {
dirs = append(dirs, entry.Name()+"/")
} else {
objects = append(objects, ObjectInfo{
Name: entry.Name(),
Size: info.Size(),
LastModified: info.ModTime(),
IsDir: false,
})
}
}
return objects, dirs, nil
}
// Deprecated: New is a backward-compatible wrapper for NewLocalFileSystem and will be
// removed in a future major version. Use NewLocalFileSystem instead.
func New(logger datasource.Logger) FileSystem {
return NewLocalFileSystem(logger)
}
// ============= Helper Types =============
// limitedReadCloser wraps a ReadCloser with a byte limit.
type limitedReadCloser struct {
rc io.ReadCloser
remaining int64
}
func (l *limitedReadCloser) Read(p []byte) (int, error) {
if l.remaining <= 0 {
return 0, io.EOF
}
if int64(len(p)) > l.remaining {
p = p[:l.remaining]
}
n, err := l.rc.Read(p)
l.remaining -= int64(n)
return n, err
}
func (l *limitedReadCloser) Close() error {
return l.rc.Close()
}
// failWriter is a WriteCloser that always returns an error.
type failWriter struct {
err error
}
func (f *failWriter) Write([]byte) (int, error) {
return 0, f.err
}
func (f *failWriter) Close() error {
return f.err
}
================================================
FILE: pkg/gofr/datasource/file/local_fs_test.go
================================================
package file
import (
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLocalProvider_NewReaderAndNewRangeReader_LimitedAndFull(t *testing.T) {
provider := &localProvider{}
tmp := t.TempDir()
filePath := filepath.Join(tmp, "data.bin")
err := os.WriteFile(filePath, []byte("abcdefgh"), 0o600)
require.NoError(t, err)
// NewReader reads full file
r, err := provider.NewReader(t.Context(), filePath)
require.NoError(t, err)
all, err := io.ReadAll(r)
require.NoError(t, err)
require.NoError(t, r.Close())
assert.Equal(t, []byte("abcdefgh"), all)
// NewRangeReader with offset and length > 0 returns limitedReadCloser
rr, err := provider.NewRangeReader(t.Context(), filePath, 2, 4)
require.NoError(t, err)
data, err := io.ReadAll(rr)
require.NoError(t, err)
require.NoError(t, rr.Close())
assert.Equal(t, []byte("cdef"), data)
// NewRangeReader with length <= 0 returns underlying file starting at offset
rr2, err := provider.NewRangeReader(t.Context(), filePath, 5, -1)
require.NoError(t, err)
rest, err := io.ReadAll(rr2)
require.NoError(t, err)
require.NoError(t, rr2.Close())
assert.Equal(t, []byte("fgh"), rest)
}
func TestLocalProvider_NewWriter_CreateAndFailWriter(t *testing.T) {
provider := &localProvider{}
tmp := t.TempDir()
// Successful writer: write file and verify contents
dst := filepath.Join(tmp, "out", "file.txt")
w := provider.NewWriter(t.Context(), dst)
n, err := w.Write([]byte("hello"))
require.NoError(t, err)
require.Equal(t, 5, n)
require.NoError(t, w.Close())
content, err := os.ReadFile(dst)
require.NoError(t, err)
assert.Equal(t, []byte("hello"), content)
// Simulate MkdirAll failure by making parent path a file
parentFile := filepath.Join(tmp, "parentfile")
err = os.WriteFile(parentFile, []byte("I am a file"), 0o600)
require.NoError(t, err)
badPath := filepath.Join(parentFile, "child") // filepath.Dir(badPath) == parentFile (a file)
w2 := provider.NewWriter(t.Context(), badPath)
// Expect a failWriter (Write/Close return error)
_, w2Err := w2.Write([]byte("x"))
require.Error(t, w2Err)
require.Error(t, w2.Close())
}
func TestLocalProvider_Stat_Delete_Copy_List_ListDir(t *testing.T) {
provider := &localProvider{}
tmp := t.TempDir()
// create files and directories
err := os.MkdirAll(filepath.Join(tmp, "subdir"), 0o755)
require.NoError(t, err)
aPath := filepath.Join(tmp, "a.txt")
bPath := filepath.Join(tmp, "subdir", "b.txt")
err = os.WriteFile(aPath, []byte("A"), 0o600)
require.NoError(t, err)
err = os.WriteFile(bPath, []byte("B"), 0o600)
require.NoError(t, err)
// StatObject on file
info, err := provider.StatObject(t.Context(), aPath)
require.NoError(t, err)
assert.Equal(t, "a.txt", info.Name)
assert.False(t, info.IsDir)
assert.Equal(t, int64(1), info.Size)
// DeleteObject removes file
err = provider.DeleteObject(t.Context(), aPath)
require.NoError(t, err)
_, statErr := os.Stat(aPath)
require.Error(t, statErr)
// CopyObject copies file to nested dst (creating dirs)
src := bPath
dst := filepath.Join(tmp, "nested", "copied.txt")
err = provider.CopyObject(t.Context(), src, dst)
require.NoError(t, err)
copied, err := os.ReadFile(dst)
require.NoError(t, err)
assert.Equal(t, []byte("B"), copied)
// ListObjects returns file names only (not directories)
objs, err := provider.ListObjects(t.Context(), tmp)
require.NoError(t, err)
// Ensure entries do not refer to directories
for _, name := range objs {
info, sErr := os.Stat(filepath.Join(tmp, name))
require.NoError(t, sErr)
assert.False(t, info.IsDir())
}
// ListDir returns object infos and trailing-slash dirs
objsInfo, dirs, err := provider.ListDir(t.Context(), tmp)
require.NoError(t, err)
// Validate dirs contain "subdir/" or "nested/"
foundDir := false
for _, d := range dirs {
if d == "subdir/" || d == "nested/" {
foundDir = true
}
}
assert.True(t, foundDir)
// Validate any returned object info entries have non-empty names (it's valid for objsInfo to be empty)
for _, oi := range objsInfo {
assert.NotEmpty(t, oi.Name)
}
}
func TestLimitedReadCloser_ReadsTillLimitAndEOF(t *testing.T) {
tmp := t.TempDir()
path := filepath.Join(tmp, "limit.txt")
err := os.WriteFile(path, []byte("1234567890"), 0o600)
require.NoError(t, err)
f, err := os.Open(path)
require.NoError(t, err)
lrc := &limitedReadCloser{rc: f, remaining: 4}
buf := make([]byte, 10)
n, err := lrc.Read(buf)
require.NoError(t, err)
assert.Equal(t, 4, n)
assert.Equal(t, []byte("1234"), buf[:4])
n2, err2 := lrc.Read(buf)
assert.Equal(t, 0, n2)
assert.Equal(t, io.EOF, err2)
require.NoError(t, lrc.Close())
}
func TestFailWriter_WriteAndCloseReturnError(t *testing.T) {
errExample := os.ErrPermission
fw := &failWriter{err: errExample}
_, wErr := fw.Write([]byte("x"))
require.Error(t, wErr)
require.ErrorIs(t, wErr, errExample)
require.Error(t, fw.Close())
require.ErrorIs(t, fw.Close(), errExample)
}
================================================
FILE: pkg/gofr/datasource/file/logger.go
================================================
package file
import (
"fmt"
"io"
"regexp"
"strings"
)
// OperationLog represents a standardized log entry for file operations.
type OperationLog struct {
Operation string `json:"operation"`
Duration int64 `json:"duration"`
Status *string `json:"status"`
Location string `json:"location,omitempty"`
Message *string `json:"message,omitempty"`
Provider string `json:"provider"` // Identifies the storage provider
}
var regexpSpaces = regexp.MustCompile(`\s+`)
// cleanString standardizes string formatting for logs/metrics.
func cleanString(query *string) string {
if query == nil {
return ""
}
return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " "))
}
// PrettyPrint formats and prints the log entry to the provided writer with proper column alignment.
func (fl *OperationLog) PrettyPrint(writer io.Writer) {
operation := cleanString(&fl.Operation)
provider := fl.Provider
status := cleanString(fl.Status)
message := cleanString(fl.Message)
fmt.Fprintf(writer, "\u001B[38;5;8m%-24s \u001B[38;5;148m%-13s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-10s %s\n",
operation, provider, fl.Duration, status, message)
}
================================================
FILE: pkg/gofr/datasource/file/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=file
//
// Package file is a generated GoMock package.
package file
import (
context "context"
io "io"
os "os"
reflect "reflect"
time "time"
gomock "go.uber.org/mock/gomock"
)
// MockFile is a mock of File interface.
type MockFile struct {
ctrl *gomock.Controller
recorder *MockFileMockRecorder
isgomock struct{}
}
// MockFileMockRecorder is the mock recorder for MockFile.
type MockFileMockRecorder struct {
mock *MockFile
}
// NewMockFile creates a new mock instance.
func NewMockFile(ctrl *gomock.Controller) *MockFile {
mock := &MockFile{ctrl: ctrl}
mock.recorder = &MockFileMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFile) EXPECT() *MockFileMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockFile) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockFileMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockFile)(nil).Close))
}
// IsDir mocks base method.
func (m *MockFile) IsDir() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsDir")
ret0, _ := ret[0].(bool)
return ret0
}
// IsDir indicates an expected call of IsDir.
func (mr *MockFileMockRecorder) IsDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDir", reflect.TypeOf((*MockFile)(nil).IsDir))
}
// ModTime mocks base method.
func (m *MockFile) ModTime() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ModTime")
ret0, _ := ret[0].(time.Time)
return ret0
}
// ModTime indicates an expected call of ModTime.
func (mr *MockFileMockRecorder) ModTime() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModTime", reflect.TypeOf((*MockFile)(nil).ModTime))
}
// Mode mocks base method.
func (m *MockFile) Mode() os.FileMode {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mode")
ret0, _ := ret[0].(os.FileMode)
return ret0
}
// Mode indicates an expected call of Mode.
func (mr *MockFileMockRecorder) Mode() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mode", reflect.TypeOf((*MockFile)(nil).Mode))
}
// Name mocks base method.
func (m *MockFile) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockFileMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockFile)(nil).Name))
}
// Read mocks base method.
func (m *MockFile) Read(p []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", p)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockFileMockRecorder) Read(p any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockFile)(nil).Read), p)
}
// ReadAll mocks base method.
func (m *MockFile) ReadAll() (RowReader, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadAll")
ret0, _ := ret[0].(RowReader)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadAll indicates an expected call of ReadAll.
func (mr *MockFileMockRecorder) ReadAll() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadAll", reflect.TypeOf((*MockFile)(nil).ReadAll))
}
// ReadAt mocks base method.
func (m *MockFile) ReadAt(p []byte, off int64) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadAt", p, off)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadAt indicates an expected call of ReadAt.
func (mr *MockFileMockRecorder) ReadAt(p, off any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadAt", reflect.TypeOf((*MockFile)(nil).ReadAt), p, off)
}
// Seek mocks base method.
func (m *MockFile) Seek(offset int64, whence int) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Seek", offset, whence)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Seek indicates an expected call of Seek.
func (mr *MockFileMockRecorder) Seek(offset, whence any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Seek", reflect.TypeOf((*MockFile)(nil).Seek), offset, whence)
}
// Size mocks base method.
func (m *MockFile) Size() int64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Size")
ret0, _ := ret[0].(int64)
return ret0
}
// Size indicates an expected call of Size.
func (mr *MockFileMockRecorder) Size() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Size", reflect.TypeOf((*MockFile)(nil).Size))
}
// Sys mocks base method.
func (m *MockFile) Sys() any {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Sys")
ret0, _ := ret[0].(any)
return ret0
}
// Sys indicates an expected call of Sys.
func (mr *MockFileMockRecorder) Sys() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sys", reflect.TypeOf((*MockFile)(nil).Sys))
}
// Write mocks base method.
func (m *MockFile) Write(p []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", p)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Write indicates an expected call of Write.
func (mr *MockFileMockRecorder) Write(p any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockFile)(nil).Write), p)
}
// WriteAt mocks base method.
func (m *MockFile) WriteAt(p []byte, off int64) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WriteAt", p, off)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WriteAt indicates an expected call of WriteAt.
func (mr *MockFileMockRecorder) WriteAt(p, off any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAt", reflect.TypeOf((*MockFile)(nil).WriteAt), p, off)
}
// MockFileInfo is a mock of FileInfo interface.
type MockFileInfo struct {
ctrl *gomock.Controller
recorder *MockFileInfoMockRecorder
isgomock struct{}
}
// MockFileInfoMockRecorder is the mock recorder for MockFileInfo.
type MockFileInfoMockRecorder struct {
mock *MockFileInfo
}
// NewMockFileInfo creates a new mock instance.
func NewMockFileInfo(ctrl *gomock.Controller) *MockFileInfo {
mock := &MockFileInfo{ctrl: ctrl}
mock.recorder = &MockFileInfoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFileInfo) EXPECT() *MockFileInfoMockRecorder {
return m.recorder
}
// IsDir mocks base method.
func (m *MockFileInfo) IsDir() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsDir")
ret0, _ := ret[0].(bool)
return ret0
}
// IsDir indicates an expected call of IsDir.
func (mr *MockFileInfoMockRecorder) IsDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsDir", reflect.TypeOf((*MockFileInfo)(nil).IsDir))
}
// ModTime mocks base method.
func (m *MockFileInfo) ModTime() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ModTime")
ret0, _ := ret[0].(time.Time)
return ret0
}
// ModTime indicates an expected call of ModTime.
func (mr *MockFileInfoMockRecorder) ModTime() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ModTime", reflect.TypeOf((*MockFileInfo)(nil).ModTime))
}
// Mode mocks base method.
func (m *MockFileInfo) Mode() os.FileMode {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mode")
ret0, _ := ret[0].(os.FileMode)
return ret0
}
// Mode indicates an expected call of Mode.
func (mr *MockFileInfoMockRecorder) Mode() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mode", reflect.TypeOf((*MockFileInfo)(nil).Mode))
}
// Name mocks base method.
func (m *MockFileInfo) Name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Name")
ret0, _ := ret[0].(string)
return ret0
}
// Name indicates an expected call of Name.
func (mr *MockFileInfoMockRecorder) Name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockFileInfo)(nil).Name))
}
// Size mocks base method.
func (m *MockFileInfo) Size() int64 {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Size")
ret0, _ := ret[0].(int64)
return ret0
}
// Size indicates an expected call of Size.
func (mr *MockFileInfoMockRecorder) Size() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Size", reflect.TypeOf((*MockFileInfo)(nil).Size))
}
// MockRowReader is a mock of RowReader interface.
type MockRowReader struct {
ctrl *gomock.Controller
recorder *MockRowReaderMockRecorder
isgomock struct{}
}
// MockRowReaderMockRecorder is the mock recorder for MockRowReader.
type MockRowReaderMockRecorder struct {
mock *MockRowReader
}
// NewMockRowReader creates a new mock instance.
func NewMockRowReader(ctrl *gomock.Controller) *MockRowReader {
mock := &MockRowReader{ctrl: ctrl}
mock.recorder = &MockRowReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRowReader) EXPECT() *MockRowReaderMockRecorder {
return m.recorder
}
// Next mocks base method.
func (m *MockRowReader) Next() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Next")
ret0, _ := ret[0].(bool)
return ret0
}
// Next indicates an expected call of Next.
func (mr *MockRowReaderMockRecorder) Next() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockRowReader)(nil).Next))
}
// Scan mocks base method.
func (m *MockRowReader) Scan(arg0 any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Scan", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Scan indicates an expected call of Scan.
func (mr *MockRowReaderMockRecorder) Scan(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockRowReader)(nil).Scan), arg0)
}
// MockStorageProvider is a mock of StorageProvider interface.
type MockStorageProvider struct {
ctrl *gomock.Controller
recorder *MockStorageProviderMockRecorder
isgomock struct{}
}
// MockStorageProviderMockRecorder is the mock recorder for MockStorageProvider.
type MockStorageProviderMockRecorder struct {
mock *MockStorageProvider
}
// NewMockStorageProvider creates a new mock instance.
func NewMockStorageProvider(ctrl *gomock.Controller) *MockStorageProvider {
mock := &MockStorageProvider{ctrl: ctrl}
mock.recorder = &MockStorageProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStorageProvider) EXPECT() *MockStorageProviderMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockStorageProvider) Connect(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Connect", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Connect indicates an expected call of Connect.
func (mr *MockStorageProviderMockRecorder) Connect(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockStorageProvider)(nil).Connect), ctx)
}
// CopyObject mocks base method.
func (m *MockStorageProvider) CopyObject(ctx context.Context, src, dst string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CopyObject", ctx, src, dst)
ret0, _ := ret[0].(error)
return ret0
}
// CopyObject indicates an expected call of CopyObject.
func (mr *MockStorageProviderMockRecorder) CopyObject(ctx, src, dst any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*MockStorageProvider)(nil).CopyObject), ctx, src, dst)
}
// DeleteObject mocks base method.
func (m *MockStorageProvider) DeleteObject(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteObject", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteObject indicates an expected call of DeleteObject.
func (mr *MockStorageProviderMockRecorder) DeleteObject(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockStorageProvider)(nil).DeleteObject), ctx, name)
}
// ListDir mocks base method.
func (m *MockStorageProvider) ListDir(ctx context.Context, prefix string) ([]ObjectInfo, []string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListDir", ctx, prefix)
ret0, _ := ret[0].([]ObjectInfo)
ret1, _ := ret[1].([]string)
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// ListDir indicates an expected call of ListDir.
func (mr *MockStorageProviderMockRecorder) ListDir(ctx, prefix any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDir", reflect.TypeOf((*MockStorageProvider)(nil).ListDir), ctx, prefix)
}
// ListObjects mocks base method.
func (m *MockStorageProvider) ListObjects(ctx context.Context, prefix string) ([]string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListObjects", ctx, prefix)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListObjects indicates an expected call of ListObjects.
func (mr *MockStorageProviderMockRecorder) ListObjects(ctx, prefix any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjects", reflect.TypeOf((*MockStorageProvider)(nil).ListObjects), ctx, prefix)
}
// NewRangeReader mocks base method.
func (m *MockStorageProvider) NewRangeReader(ctx context.Context, name string, offset, length int64) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewRangeReader", ctx, name, offset, length)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewRangeReader indicates an expected call of NewRangeReader.
func (mr *MockStorageProviderMockRecorder) NewRangeReader(ctx, name, offset, length any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewRangeReader", reflect.TypeOf((*MockStorageProvider)(nil).NewRangeReader), ctx, name, offset, length)
}
// NewReader mocks base method.
func (m *MockStorageProvider) NewReader(ctx context.Context, name string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewReader", ctx, name)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewReader indicates an expected call of NewReader.
func (mr *MockStorageProviderMockRecorder) NewReader(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewReader", reflect.TypeOf((*MockStorageProvider)(nil).NewReader), ctx, name)
}
// NewWriter mocks base method.
func (m *MockStorageProvider) NewWriter(ctx context.Context, name string) io.WriteCloser {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewWriter", ctx, name)
ret0, _ := ret[0].(io.WriteCloser)
return ret0
}
// NewWriter indicates an expected call of NewWriter.
func (mr *MockStorageProviderMockRecorder) NewWriter(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewWriter", reflect.TypeOf((*MockStorageProvider)(nil).NewWriter), ctx, name)
}
// StatObject mocks base method.
func (m *MockStorageProvider) StatObject(ctx context.Context, name string) (*ObjectInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StatObject", ctx, name)
ret0, _ := ret[0].(*ObjectInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StatObject indicates an expected call of StatObject.
func (mr *MockStorageProviderMockRecorder) StatObject(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StatObject", reflect.TypeOf((*MockStorageProvider)(nil).StatObject), ctx, name)
}
// MockFileSystem is a mock of FileSystem interface.
type MockFileSystem struct {
ctrl *gomock.Controller
recorder *MockFileSystemMockRecorder
isgomock struct{}
}
// MockFileSystemMockRecorder is the mock recorder for MockFileSystem.
type MockFileSystemMockRecorder struct {
mock *MockFileSystem
}
// NewMockFileSystem creates a new mock instance.
func NewMockFileSystem(ctrl *gomock.Controller) *MockFileSystem {
mock := &MockFileSystem{ctrl: ctrl}
mock.recorder = &MockFileSystemMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFileSystem) EXPECT() *MockFileSystemMockRecorder {
return m.recorder
}
// ChDir mocks base method.
func (m *MockFileSystem) ChDir(dirname string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ChDir", dirname)
ret0, _ := ret[0].(error)
return ret0
}
// ChDir indicates an expected call of ChDir.
func (mr *MockFileSystemMockRecorder) ChDir(dirname any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChDir", reflect.TypeOf((*MockFileSystem)(nil).ChDir), dirname)
}
// Create mocks base method.
func (m *MockFileSystem) Create(name string) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", name)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockFileSystemMockRecorder) Create(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFileSystem)(nil).Create), name)
}
// Getwd mocks base method.
func (m *MockFileSystem) Getwd() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Getwd")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Getwd indicates an expected call of Getwd.
func (mr *MockFileSystemMockRecorder) Getwd() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Getwd", reflect.TypeOf((*MockFileSystem)(nil).Getwd))
}
// Mkdir mocks base method.
func (m *MockFileSystem) Mkdir(name string, perm os.FileMode) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mkdir", name, perm)
ret0, _ := ret[0].(error)
return ret0
}
// Mkdir indicates an expected call of Mkdir.
func (mr *MockFileSystemMockRecorder) Mkdir(name, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdir", reflect.TypeOf((*MockFileSystem)(nil).Mkdir), name, perm)
}
// MkdirAll mocks base method.
func (m *MockFileSystem) MkdirAll(path string, perm os.FileMode) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MkdirAll", path, perm)
ret0, _ := ret[0].(error)
return ret0
}
// MkdirAll indicates an expected call of MkdirAll.
func (mr *MockFileSystemMockRecorder) MkdirAll(path, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MockFileSystem)(nil).MkdirAll), path, perm)
}
// Open mocks base method.
func (m *MockFileSystem) Open(name string) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Open", name)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Open indicates an expected call of Open.
func (mr *MockFileSystemMockRecorder) Open(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockFileSystem)(nil).Open), name)
}
// OpenFile mocks base method.
func (m *MockFileSystem) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenFile", name, flag, perm)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OpenFile indicates an expected call of OpenFile.
func (mr *MockFileSystemMockRecorder) OpenFile(name, flag, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenFile", reflect.TypeOf((*MockFileSystem)(nil).OpenFile), name, flag, perm)
}
// ReadDir mocks base method.
func (m *MockFileSystem) ReadDir(dir string) ([]FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDir", dir)
ret0, _ := ret[0].([]FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDir indicates an expected call of ReadDir.
func (mr *MockFileSystemMockRecorder) ReadDir(dir any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDir", reflect.TypeOf((*MockFileSystem)(nil).ReadDir), dir)
}
// Remove mocks base method.
func (m *MockFileSystem) Remove(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", name)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockFileSystemMockRecorder) Remove(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockFileSystem)(nil).Remove), name)
}
// RemoveAll mocks base method.
func (m *MockFileSystem) RemoveAll(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveAll", path)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveAll indicates an expected call of RemoveAll.
func (mr *MockFileSystemMockRecorder) RemoveAll(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockFileSystem)(nil).RemoveAll), path)
}
// Rename mocks base method.
func (m *MockFileSystem) Rename(oldname, newname string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", oldname, newname)
ret0, _ := ret[0].(error)
return ret0
}
// Rename indicates an expected call of Rename.
func (mr *MockFileSystemMockRecorder) Rename(oldname, newname any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockFileSystem)(nil).Rename), oldname, newname)
}
// Stat mocks base method.
func (m *MockFileSystem) Stat(name string) (FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stat", name)
ret0, _ := ret[0].(FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stat indicates an expected call of Stat.
func (mr *MockFileSystemMockRecorder) Stat(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockFileSystem)(nil).Stat), name)
}
// MockFileSystemProvider is a mock of FileSystemProvider interface.
type MockFileSystemProvider struct {
ctrl *gomock.Controller
recorder *MockFileSystemProviderMockRecorder
isgomock struct{}
}
// MockFileSystemProviderMockRecorder is the mock recorder for MockFileSystemProvider.
type MockFileSystemProviderMockRecorder struct {
mock *MockFileSystemProvider
}
// NewMockFileSystemProvider creates a new mock instance.
func NewMockFileSystemProvider(ctrl *gomock.Controller) *MockFileSystemProvider {
mock := &MockFileSystemProvider{ctrl: ctrl}
mock.recorder = &MockFileSystemProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFileSystemProvider) EXPECT() *MockFileSystemProviderMockRecorder {
return m.recorder
}
// ChDir mocks base method.
func (m *MockFileSystemProvider) ChDir(dirname string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ChDir", dirname)
ret0, _ := ret[0].(error)
return ret0
}
// ChDir indicates an expected call of ChDir.
func (mr *MockFileSystemProviderMockRecorder) ChDir(dirname any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ChDir", reflect.TypeOf((*MockFileSystemProvider)(nil).ChDir), dirname)
}
// Connect mocks base method.
func (m *MockFileSystemProvider) Connect() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Connect")
}
// Connect indicates an expected call of Connect.
func (mr *MockFileSystemProviderMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockFileSystemProvider)(nil).Connect))
}
// Create mocks base method.
func (m *MockFileSystemProvider) Create(name string) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", name)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockFileSystemProviderMockRecorder) Create(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockFileSystemProvider)(nil).Create), name)
}
// Getwd mocks base method.
func (m *MockFileSystemProvider) Getwd() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Getwd")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Getwd indicates an expected call of Getwd.
func (mr *MockFileSystemProviderMockRecorder) Getwd() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Getwd", reflect.TypeOf((*MockFileSystemProvider)(nil).Getwd))
}
// Mkdir mocks base method.
func (m *MockFileSystemProvider) Mkdir(name string, perm os.FileMode) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mkdir", name, perm)
ret0, _ := ret[0].(error)
return ret0
}
// Mkdir indicates an expected call of Mkdir.
func (mr *MockFileSystemProviderMockRecorder) Mkdir(name, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdir", reflect.TypeOf((*MockFileSystemProvider)(nil).Mkdir), name, perm)
}
// MkdirAll mocks base method.
func (m *MockFileSystemProvider) MkdirAll(path string, perm os.FileMode) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MkdirAll", path, perm)
ret0, _ := ret[0].(error)
return ret0
}
// MkdirAll indicates an expected call of MkdirAll.
func (mr *MockFileSystemProviderMockRecorder) MkdirAll(path, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MockFileSystemProvider)(nil).MkdirAll), path, perm)
}
// Open mocks base method.
func (m *MockFileSystemProvider) Open(name string) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Open", name)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Open indicates an expected call of Open.
func (mr *MockFileSystemProviderMockRecorder) Open(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MockFileSystemProvider)(nil).Open), name)
}
// OpenFile mocks base method.
func (m *MockFileSystemProvider) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenFile", name, flag, perm)
ret0, _ := ret[0].(File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OpenFile indicates an expected call of OpenFile.
func (mr *MockFileSystemProviderMockRecorder) OpenFile(name, flag, perm any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenFile", reflect.TypeOf((*MockFileSystemProvider)(nil).OpenFile), name, flag, perm)
}
// ReadDir mocks base method.
func (m *MockFileSystemProvider) ReadDir(dir string) ([]FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDir", dir)
ret0, _ := ret[0].([]FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDir indicates an expected call of ReadDir.
func (mr *MockFileSystemProviderMockRecorder) ReadDir(dir any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDir", reflect.TypeOf((*MockFileSystemProvider)(nil).ReadDir), dir)
}
// Remove mocks base method.
func (m *MockFileSystemProvider) Remove(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", name)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MockFileSystemProviderMockRecorder) Remove(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockFileSystemProvider)(nil).Remove), name)
}
// RemoveAll mocks base method.
func (m *MockFileSystemProvider) RemoveAll(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveAll", path)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveAll indicates an expected call of RemoveAll.
func (mr *MockFileSystemProviderMockRecorder) RemoveAll(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MockFileSystemProvider)(nil).RemoveAll), path)
}
// Rename mocks base method.
func (m *MockFileSystemProvider) Rename(oldname, newname string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", oldname, newname)
ret0, _ := ret[0].(error)
return ret0
}
// Rename indicates an expected call of Rename.
func (mr *MockFileSystemProviderMockRecorder) Rename(oldname, newname any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockFileSystemProvider)(nil).Rename), oldname, newname)
}
// Stat mocks base method.
func (m *MockFileSystemProvider) Stat(name string) (FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stat", name)
ret0, _ := ret[0].(FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stat indicates an expected call of Stat.
func (mr *MockFileSystemProviderMockRecorder) Stat(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MockFileSystemProvider)(nil).Stat), name)
}
// UseLogger mocks base method.
func (m *MockFileSystemProvider) UseLogger(logger any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseLogger", logger)
}
// UseLogger indicates an expected call of UseLogger.
func (mr *MockFileSystemProviderMockRecorder) UseLogger(logger any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseLogger", reflect.TypeOf((*MockFileSystemProvider)(nil).UseLogger), logger)
}
// UseMetrics mocks base method.
func (m *MockFileSystemProvider) UseMetrics(metrics any) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "UseMetrics", metrics)
}
// UseMetrics indicates an expected call of UseMetrics.
func (mr *MockFileSystemProviderMockRecorder) UseMetrics(metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UseMetrics", reflect.TypeOf((*MockFileSystemProvider)(nil).UseMetrics), metrics)
}
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Warn mocks base method.
func (m *MockLogger) Warn(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warn", varargs...)
}
// Warn indicates an expected call of Warn.
func (mr *MockLoggerMockRecorder) Warn(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warn", reflect.TypeOf((*MockLogger)(nil).Warn), args...)
}
// Warnf mocks base method.
func (m *MockLogger) Warnf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warnf", varargs...)
}
// Warnf indicates an expected call of Warnf.
func (mr *MockLoggerMockRecorder) Warnf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/file/observability.go
================================================
package file
import (
"context"
"time"
"gofr.dev/pkg/gofr/datasource"
)
const (
// AppFileStats is the single metric name for all file operations across providers.
AppFileStats = "app_file_stats"
)
// Operation constants for standardization across all file providers.
const (
OpConnect = "CONNECT"
OpCreate = "CREATE"
OpCreateWithOptions = "CREATE_WITH_OPTIONS"
OpOpen = "OPEN"
OpOpenFile = "OPEN_FILE"
OpRemove = "REMOVE"
OpRename = "RENAME"
OpMkdir = "MKDIR"
OpMkdirAll = "MKDIR_ALL"
OpRemoveAll = "REMOVE_ALL"
OpReadDir = "READ_DIR"
OpStat = "STAT"
OpChDir = "CHDIR"
OpGetwd = "GETWD"
OpSignedURL = "SIGNED_URL"
OpRead = "READ"
OpReadAt = "READ_AT"
OpWrite = "WRITE"
OpWriteAt = "WRITE_AT"
OpSeek = "SEEK"
OpClose = "CLOSE"
)
// StorageMetrics interface that all storage providers should use.
type StorageMetrics interface {
// NewHistogram creates a new histogram with the given name, description, and buckets.
NewHistogram(name, desc string, buckets ...float64)
// RecordHistogram records a value in the histogram with the given name and labels.
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
// DefaultHistogramBuckets returns the standard bucket sizes for file operations.
func DefaultHistogramBuckets() []float64 {
return []float64{0.1, 1, 10, 100, 1000}
}
// OperationObservability contains all parameters needed for logging and metrics collection.
type OperationObservability struct {
Context context.Context
Logger datasource.Logger
Metrics StorageMetrics
Operation string
Location string
Provider string
StartTime time.Time
Status *string
Message *string
}
// ObserveOperation is a helper function that handles both logging and metrics recording.
func ObserveOperation(params *OperationObservability) {
duration := time.Since(params.StartTime).Microseconds()
log := &OperationLog{
Operation: params.Operation,
Duration: duration,
Status: params.Status,
Location: params.Location,
Message: params.Message,
Provider: params.Provider,
}
if params.Logger != nil {
params.Logger.Debug(log)
}
if params.Metrics != nil {
params.Metrics.RecordHistogram(
params.Context,
AppFileStats,
float64(duration),
"type", params.Operation,
"status", cleanString(params.Status),
"provider", params.Provider,
)
}
}
================================================
FILE: pkg/gofr/datasource/file/row_reader.go
================================================
package file
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"io"
"gofr.dev/pkg/gofr/datasource"
)
var (
errNotPointer = errors.New("input should be a pointer to a string")
errInvalidJSON = errors.New("invalid JSON")
)
// textReader reads text/CSV files line by line.
type textReader struct {
scanner *bufio.Scanner
logger datasource.Logger
}
// jsonReader reads JSON files (array or single object).
type jsonReader struct {
decoder *json.Decoder
isArray bool
consumed bool
}
// NewTextReader creates a RowReader for text/CSV files.
// Each call to Next() reads one line from the file.
// Scan() must receive a *string pointer to store the line content.
//
// Example:
//
// reader := file.NewTextReader(f, logger)
// for reader.Next() {
// var line string
// reader.Scan(&line)
// fmt.Println(line)
// }
func NewTextReader(r io.Reader, logger datasource.Logger) RowReader {
return &textReader{
scanner: bufio.NewScanner(r),
logger: logger,
}
}
// Next checks if there is data available in the next line.
func (t *textReader) Next() bool {
return t.scanner.Scan()
}
// Scan binds the line to the provided pointer to string.
func (t *textReader) Scan(i any) error {
target, ok := i.(*string)
if !ok {
return errNotPointer
}
*target = t.scanner.Text()
return nil
}
// NewJSONReader creates a RowReader for JSON files (auto-detects array vs object).
func NewJSONReader(r io.Reader, logger datasource.Logger) (RowReader, error) {
// Read entire payload so we can validate and then create a fresh decoder.
b, err := io.ReadAll(r)
if err != nil {
if logger != nil {
logger.Errorf("failed to read JSON input: %v", err)
}
return nil, err
}
trimmed := bytes.TrimLeft(b, " \t\r\n")
if len(trimmed) == 0 {
return nil, io.EOF
}
// Validate JSON to detect invalid input early.
if !json.Valid(trimmed) {
if logger != nil {
logger.Errorf("invalid JSON input")
}
return nil, errInvalidJSON
}
isArray := trimmed[0] == '['
decoder := json.NewDecoder(bytes.NewReader(trimmed))
// If array, consume the opening '[' so decoder.More() works properly.
if isArray {
_, err := decoder.Token()
if err != nil {
if logger != nil {
logger.Errorf("failed to read JSON array token: %v", err)
}
return nil, err
}
}
return &jsonReader{
decoder: decoder,
isArray: isArray,
consumed: false,
}, nil
}
// Next checks if there is a next JSON object available.
func (j *jsonReader) Next() bool {
// If it's an array, check if more elements exist
if j.isArray {
return j.decoder.More()
}
// For single object, return true only once
if !j.consumed {
return true
}
return false
}
// Scan decodes the next JSON object into the provided struct.
func (j *jsonReader) Scan(i any) error {
// For single objects, mark as consumed after first scan
if !j.isArray {
if j.consumed {
return io.EOF
}
j.consumed = true
}
// Decode the next object
return j.decoder.Decode(&i)
}
================================================
FILE: pkg/gofr/datasource/file/row_reader_test.go
================================================
package file
import (
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
type errReader struct {
err error
}
func (e errReader) Read([]byte) (int, error) { return 0, e.err }
func TestNewJSONReader_ReadErrorReturnsError(t *testing.T) {
readErr := io.ErrUnexpectedEOF
_, err := NewJSONReader(errReader{err: readErr}, nil)
require.Error(t, err)
assert.ErrorIs(t, err, readErr)
}
func TestNewJSONReader_EmptyInputReturnsEOF(t *testing.T) {
_, err := NewJSONReader(strings.NewReader(" \n\t "), nil)
require.Error(t, err)
assert.Equal(t, io.EOF, err)
}
func TestNewJSONReader_InvalidJSONReturnsError(t *testing.T) {
_, err := NewJSONReader(strings.NewReader("{"), nil)
require.Error(t, err)
// errInvalidJSON is defined in row_reader.go
assert.ErrorIs(t, err, errInvalidJSON)
}
func TestJSONReader_ArrayAndSingleObject_Behavior(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
arrayJSON := `[{"a":1},{"a":2}]`
jr, err := NewJSONReader(strings.NewReader(arrayJSON), NewMockLogger(ctrl))
require.NoError(t, err)
require.True(t, jr.Next())
var obj map[string]any
require.NoError(t, jr.Scan(&obj))
assert.InEpsilon(t, float64(1), obj["a"].(float64), 1e-9)
require.True(t, jr.Next())
var obj2 map[string]any
require.NoError(t, jr.Scan(&obj2))
assert.InEpsilon(t, float64(2), obj2["a"].(float64), 1e-9)
assert.False(t, jr.Next())
// Single object: Next true once, Scan once, then subsequent Scan returns io.EOF
single := `{"b":3}`
jr2, err := NewJSONReader(strings.NewReader(single), nil)
require.NoError(t, err)
require.True(t, jr2.Next())
var so map[string]any
require.NoError(t, jr2.Scan(&so))
assert.InEpsilon(t, float64(3), so["b"].(float64), 1e-9)
errEOF := jr2.Scan(&so)
assert.Equal(t, io.EOF, errEOF)
}
func TestNewJSONReader_ErrorOnInvalidJSON(t *testing.T) {
invalid := `{`
_, err := NewJSONReader(strings.NewReader(invalid), nil)
require.Error(t, err)
}
func TestTextReader_NextAndScan_SuccessAndTypeError(t *testing.T) {
r := strings.NewReader("line1\nline2\n")
tr := NewTextReader(r, nil)
require.True(t, tr.Next())
var s string
require.NoError(t, tr.Scan(&s))
assert.Equal(t, "line1", s)
require.True(t, tr.Next())
require.NoError(t, tr.Scan(&s))
assert.Equal(t, "line2", s)
assert.False(t, tr.Next())
// Scan with wrong type should return errNotPointer
err := tr.Scan(new(int))
assert.Equal(t, errNotPointer, err)
}
// Ensure ListDir returns object infos (else branch) and directory names.
func TestLocalProvider_ListDir_ReturnsObjectsAndDirs(t *testing.T) {
provider := &localProvider{}
tmp := t.TempDir()
// create a top-level file and a sub-directory with a nested file
err := os.MkdirAll(filepath.Join(tmp, "subdir"), 0o755)
require.NoError(t, err)
filePath := filepath.Join(tmp, "file.txt")
err = os.WriteFile(filePath, []byte("hello"), 0o600)
require.NoError(t, err)
nestedFile := filepath.Join(tmp, "subdir", "nested.txt")
err = os.WriteFile(nestedFile, []byte("x"), 0o600)
require.NoError(t, err)
objects, dirs, err := provider.ListDir(t.Context(), tmp)
require.NoError(t, err)
// verify directory list contains trailing slash
assert.Contains(t, dirs, "subdir/")
// verify objects contains the top-level file info
foundFile := false
for _, oi := range objects {
if oi.Name == "file.txt" && !oi.IsDir {
foundFile = true
break
}
}
assert.True(t, foundFile, "expected to find top-level file in objects")
}
================================================
FILE: pkg/gofr/datasource/file/s3/file.go
================================================
package s3
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"mime"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// S3File represents a file in an S3 bucket.
//
//nolint:revive // s3.S3File is repetitive. A better name could have been chosen, but it's too late as it's already exported.
type S3File struct {
conn s3Client
name string
offset int64
logger Logger
metrics Metrics
size int64
contentType string
body io.ReadCloser
lastModified time.Time
}
func (*S3File) Sys() any {
return nil
}
var (
ErrNilResponse = errors.New("response retrieved is nil ")
)
// Close closes the response body returned in Open/Create methods if the response body is not nil.
func (f *S3File) Close() error {
bucketName := getBucketName(f.name)
defer f.sendOperationStats(&FileLog{
Operation: "CLOSE",
Location: getLocation(bucketName)}, time.Now())
if f.body != nil {
return f.body.Close()
}
return nil
}
// Read reads data into the provided byte slice.
//
// It attempts to fill the entire slice with data from the file. If the number of bytes read is less than the length of the slice,
// it returns an io.EOF error to indicate that the end of the file has been reached before the slice was completely filled.
//
// Additionally, this method updates the file offset to reflect the next position that will be used for subsequent read or write operations.
func (f *S3File) Read(p []byte) (n int, err error) {
var fileName, msg string
bucketName := getBucketName(f.name)
// get path relative to current bucketName
index := strings.Index(f.name, string(filepath.Separator))
if index != -1 {
fileName = f.name[index+1:]
}
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "READ",
Location: getLocation(bucketName),
Status: &st,
Message: &msg,
}, time.Now())
res, err := f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})
if err != nil {
msg = fmt.Sprintf("Failed to retrieve %q: %v", fileName, err)
return 0, err
}
f.body = res.Body
if f.body == nil {
msg = fmt.Sprintf("File %q is nil", fileName)
return 0, fmt.Errorf("%w: S3 file is empty", ErrNilResponse)
}
buffer := make([]byte, len(p)+int(f.offset))
n, err = f.body.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
f.logger.Errorf("Error reading file %q: %v", fileName, err)
return n, err
}
buffer = buffer[f.offset:]
copy(p, buffer)
f.offset += int64(len(buffer))
st = statusSuccess
msg = fmt.Sprintf("Read %v bytes from file at path %q in bucket %q", n, fileName, bucketName)
f.logger.Logf("%v bytes read successfully)", len(p))
return len(p), err
}
// ReadAt reads data from the file at a specified offset into the provided byte slice.
//
// This method reads up to len(p) bytes from the file, starting at the given offset. It does not alter the current file offset
// used for other read or write operations. The number of bytes read is returned, along with any error encountered.
func (f *S3File) ReadAt(p []byte, offset int64) (n int, err error) {
bucketName := getBucketName(f.name)
var fileName, msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "READAT",
Location: getLocation(bucketName),
Status: &st,
Message: &msg,
}, time.Now())
// get path relative to current bucketName
index := strings.Index(f.name, string(filepath.Separator))
if index != -1 {
fileName = f.name[index+1:]
}
var res *s3.GetObjectOutput
res, err = f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})
if err != nil {
msg = fmt.Sprintf("Failed to retrieve file %q: %v", fileName, err)
return 0, err
}
f.body = res.Body
if f.body == nil {
msg = fmt.Sprintf("File %q is nil", fileName)
return 0, io.EOF
}
if int64(len(p))+offset+1 > f.size {
msg = fmt.Sprintf("Offset %v out of range", f.offset)
return 0, fmt.Errorf("%w: reading out of range, fetching from the offset. Use Seek to reset offset", ErrOutOfRange)
}
buffer := make([]byte, len(p)+int(offset)+1)
n, err = f.body.Read(buffer)
if err != nil && !errors.Is(err, io.EOF) {
return n, err
}
buffer = buffer[offset:]
copy(p, buffer)
st = statusSuccess
msg = fmt.Sprintf("Read %v bytes at an offset of %v from file at path %q in bucket %q", n, offset, fileName, bucketName)
f.logger.Logf("%v bytes read successfully.")
return len(p), nil
}
// Write writes data to the file at the current offset and updates the file offset.
//
// This method writes up to len(p) bytes from the provided byte slice to the file, starting at the current offset.
// It updates the file offset after the write operation to reflect the new position for subsequent read or write operations.
func (f *S3File) Write(p []byte) (n int, err error) {
bucketName := getBucketName(f.name)
var fileName, msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "WRITE",
Location: getLocation(bucketName),
Status: &st,
Message: &msg,
}, time.Now())
// extracting file name
index := strings.Index(f.name, string(filepath.Separator))
if index != -1 {
fileName = f.name[index+1:]
}
buffer := p
var res *s3.GetObjectOutput
// if f.offset is not 0, we need to fetch the contents of the file till the offset and then write into the file
if f.offset != 0 {
res, err = f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})
if err != nil {
msg = fmt.Sprintf("Failed to retrieve file %q: %v", fileName, err)
return 0, err
}
f.body = res.Body
buffer, err = io.ReadAll(f.body)
if err != nil && !errors.Is(err, io.EOF) {
msg = fmt.Sprintf("Failed to read file %q to perform write at offset of %v: %v", fileName, f.offset, err)
return 0, err
}
var contentBeforeOffset, contentAfterBufferBytes []byte
contentBeforeOffset = buffer[:f.offset]
if f.offset+int64(len(p)) < f.size {
contentAfterBufferBytes = buffer[f.offset+int64(len(p)):]
}
contentBeforeOffset = append(contentBeforeOffset, p...)
buffer = contentBeforeOffset
buffer = append(buffer, contentAfterBufferBytes...)
}
_, err = f.conn.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
Body: bytes.NewReader(buffer),
ContentType: aws.String(mime.TypeByExtension(path.Ext(f.name))),
// this specifies the file must be downloaded before being opened
ContentDisposition: aws.String("attachment"),
})
if err != nil {
msg = fmt.Sprintf("Failed to put file %q: %v", fileName, err)
return 0, err
}
f.offset += int64(len(p))
f.size = int64(len(buffer))
st = statusSuccess
msg = fmt.Sprintf("Wrote %v bytes to file at path %q in bucket %q", n, fileName, bucketName)
f.logger.Logf("%v bytes written successfully", len(p))
return len(p), nil
}
// WriteAt writes data to the file at a specified offset without altering the current file offset.
//
// This method writes up to len(p) bytes from the provided byte slice to the file, starting at the given offset.
// It does not modify the file's current offset used for other read or write operations.
// The number of bytes written and any error encountered during the operation are returned.
func (f *S3File) WriteAt(p []byte, offset int64) (n int, err error) {
bucketName := getBucketName(f.name)
var fileName, msg string
st := statusErr
defer f.sendOperationStats(&FileLog{Operation: "WRITEAT", Location: getLocation(bucketName), Status: &st, Message: &msg}, time.Now())
index := strings.Index(f.name, string(filepath.Separator))
if index != -1 {
fileName = f.name[index+1:]
}
res, err := f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})
if err != nil {
msg = fmt.Sprintf("Failed to retrieve file %q: %v", fileName, err)
return 0, err
}
f.body = res.Body
var contentAfterBufferBytes []byte
buffer, err := io.ReadAll(f.body)
contentBeforeOffset := buffer[:offset]
if offset+int64(len(p)) < f.size {
contentAfterBufferBytes = buffer[offset+int64(len(p)):]
}
contentBeforeOffset = append(contentBeforeOffset, p...)
buffer = contentBeforeOffset
buffer = append(buffer, contentAfterBufferBytes...)
_, err = f.conn.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
Body: bytes.NewReader(buffer),
ContentType: aws.String(mime.TypeByExtension(path.Ext(f.name))),
// this specifies the file must be downloaded before being opened
ContentDisposition: aws.String("attachment"),
})
if err != nil {
msg = fmt.Sprintf("Failed to put file %q: %v", fileName, err)
return 0, err
}
f.size = int64(len(buffer))
st = statusSuccess
msg = fmt.Sprintf("Wrote %v bytes at an offset of %v to file at path %q in bucket %q", n, offset, fileName, bucketName)
return len(p), nil
}
// check validates the provided arguments for the Seek method and updates the file offset.
//
// This method performs validation on the arguments provided to the Seek operation. If the arguments are valid, it sets
// the file offset to the specified position. If there are any validation errors, it returns an appropriate error.
func (f *S3File) check(whence int, offset, length int64, msg *string) (int64, error) {
switch whence {
case io.SeekStart:
case io.SeekEnd:
offset += length
case io.SeekCurrent:
offset += f.offset
default:
return 0, os.ErrInvalid
}
if offset < 0 || offset > length {
*msg = fmt.Sprintf("Offset %v out of bounds %v", offset, length)
return 0, ErrOutOfRange
}
f.offset = offset
return f.offset, nil
}
// Seek sets the file offset to a specified position and returns the new offset.
//
// This method changes the file's current offset to the given position based on the reference point specified by the `whence` parameter.
// It uses the provided offset and whence values to determine the new file position. The method returns the new offset
// and any error encountered during the operation.
//
// Parameters:
//
// offset int64: The desired position to seek to in the file.
// whence int: The reference point for the new offset. It can be one of the following:
// - `io.SeekStart` (0): Offset is relative to the start of the file.
// - `io.SeekCurrent` (1): Offset is relative to the current position in the file.
// - `io.SeekEnd` (2): Offset is relative to the end of the file.
func (f *S3File) Seek(offset int64, whence int) (int64, error) {
var msg string
status := statusErr
defer f.sendOperationStats(&FileLog{Operation: "SEEK", Location: f.name, Status: &status, Message: &msg}, time.Now())
n := f.Size()
res, err := f.check(whence, offset, n, &msg)
if err != nil {
f.logger.Errorf("Seek failed. Error: %v", err)
return 0, err
}
status = statusSuccess
msg = fmt.Sprintf("Offset set to %v for file with path %q", res, f.name)
f.logger.Logf("Set file offset at %v", f.offset)
return res, nil
}
// sendOperationStats logs the FileLog of any file operations performed in S3.
func (f *S3File) sendOperationStats(fl *FileLog, startTime time.Time) {
duration := time.Since(startTime).Microseconds()
fl.Duration = duration
f.logger.Debug(fl)
}
================================================
FILE: pkg/gofr/datasource/file/s3/file_parse.go
================================================
package s3
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
file "gofr.dev/pkg/gofr/datasource/file"
)
var (
// errNotPointer is returned when Read method is called with a non-pointer argument.
errStringNotPointer = errors.New("input should be a pointer to a string")
ErrOutOfRange = errors.New("out of range")
)
const (
statusErr = "ERROR"
statusSuccess = "SUCCESS"
)
// textReader implements RowReader for reading text files.
type textReader struct {
scanner *bufio.Scanner
logger Logger
}
// jsonReader implements RowReader for reading JSON files.
type jsonReader struct {
decoder *json.Decoder
token json.Token
}
// ReadAll reads either JSON or text files based on file extension and returns a corresponding RowReader.
func (f *S3File) ReadAll() (file.RowReader, error) {
bucketName := strings.Split(f.name, string(filepath.Separator))[0]
var fileName string
index := strings.Index(f.name, string(filepath.Separator))
if index != -1 {
fileName = f.name[index+1:]
}
location := path.Join(bucketName, fileName)
defer f.sendOperationStats(&FileLog{Operation: "READALL", Location: location}, time.Now())
if strings.HasSuffix(f.Name(), ".json") {
return f.createJSONReader(location)
}
return f.createTextCSVReader(location)
}
// createJSONReader creates a JSON reader for JSON files.
func (f *S3File) createJSONReader(location string) (file.RowReader, error) {
status := statusErr
defer f.sendOperationStats(&FileLog{Operation: "JSON READER", Location: location, Status: &status}, time.Now())
buffer, err := io.ReadAll(f.body)
if err != nil {
f.logger.Errorf("ReadAll Failed: Unable to read json file: %v", err)
return nil, err
}
reader := bytes.NewReader(buffer)
decoder := json.NewDecoder(reader)
// Peek the first JSON token to determine the type
// Note: This results in offset to move ahead, making it necessary to
// decode again if we are decoding a json object instead of array
token, err := decoder.Token()
if err != nil {
f.logger.Errorf("Error decoding token: %v", err)
return nil, err
}
if d, ok := token.(json.Delim); ok && d == '[' {
status = statusSuccess
return &jsonReader{decoder: decoder, token: token}, err
}
// Reading JSON object
decoder = json.NewDecoder(reader)
status = statusSuccess
return &jsonReader{decoder: decoder}, nil
}
// createTextCSVReader creates a text reader for reading text files.
func (f *S3File) createTextCSVReader(location string) (file.RowReader, error) {
status := statusErr
defer f.sendOperationStats(&FileLog{Operation: "TEXT/CSV READER", Location: location, Status: &status}, time.Now())
buffer, err := io.ReadAll(f.body)
if err != nil {
f.logger.Errorf("ReadAll failed: Unable to read text file: %v", err)
return nil, err
}
reader := bytes.NewReader(buffer)
status = statusSuccess
return &textReader{
scanner: bufio.NewScanner(reader),
logger: f.logger,
}, err
}
// Next checks if there is another JSON object available.
func (j *jsonReader) Next() bool {
return j.decoder.More()
}
// Scan decodes the next JSON object into the provided structure.
func (j *jsonReader) Scan(i any) error {
return j.decoder.Decode(&i)
}
// Next checks if there is another line available in the text file.
func (f *textReader) Next() bool {
return f.scanner.Scan()
}
// Scan scans the next line from the text file into the provided pointer to string.
func (f *textReader) Scan(i any) error {
if val, ok := i.(*string); ok {
*val = f.scanner.Text()
return nil
}
return errStringNotPointer
}
// Name returns the base name of the file.
//
// For a file, this method returns the name of the file without any directory components.
// For directories, it returns the name of the directory.
func (f *S3File) Name() string {
bucketName := getBucketName(f.name)
f.sendOperationStats(&FileLog{
Operation: "GET NAME",
Location: getLocation(bucketName),
}, time.Now())
return path.Base(f.name)
}
// Mode is not supported for the current implementation of S3 buckets.
// This method is included to adhere to the FileSystem interface in GoFr.
//
// Note: The Mode method does not provide meaningful information for S3 objects
// and should be considered a placeholder in this context.
func (f *S3File) Mode() os.FileMode {
bucketName := getBucketName(f.name)
f.sendOperationStats(&FileLog{
Operation: "FILE MODE",
Location: getLocation(bucketName),
Message: aws.String("Not supported for S3"),
}, time.Now())
return 0
}
// Size returns the size of the retrieved object.
//
// For files, it returns the size of the file in bytes.
// For directories, it returns the sum of sizes of all files contained within the directory.
//
// Note:
// - This method should be called on a FileInfo instance obtained from a Stat or ReadDir operation.
func (f *S3File) Size() int64 {
bucketName := getBucketName(f.name)
f.sendOperationStats(&FileLog{
Operation: "FILE/DIR SIZE",
Location: getLocation(bucketName),
}, time.Now())
return f.size
}
// ModTime returns the last modification time of the file or directory.
//
// For files, it returns the timestamp of the last modification to the file's contents.
// For directories, it returns the timestamp of the most recent change to the directory's contents, including updates
// to files within the directory.
func (f *S3File) ModTime() time.Time {
bucketName := getBucketName(f.name)
f.sendOperationStats(&FileLog{
Operation: "LAST MODIFIED",
Location: getLocation(bucketName),
}, time.Now())
return f.lastModified
}
// IsDir checks if the FileInfo describes a directory.
//
// This method returns true if the FileInfo object represents a directory; otherwise, it returns false.
// It is specifically used to determine the type of the file system object represented by the FileInfo.
//
// Note:
// - This method should be called on a FileInfo instance obtained from a Stat or ReadDir operation.
// - The [FileInfo] interface is used to describe file system objects, and IsDir is one of its methods
// to query whether the object is a directory.
func (f *S3File) IsDir() bool {
bucketName := getBucketName(f.name)
f.sendOperationStats(&FileLog{
Operation: "IS DIR",
Location: getLocation(bucketName),
}, time.Now())
return strings.HasSuffix(f.name, "/")
}
================================================
FILE: pkg/gofr/datasource/file/s3/file_test.go
================================================
package s3
import (
"bytes"
"context"
"encoding/json"
"errors"
"io"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
var (
errGetObject = errors.New("failed to get object from S3")
errPutObject = errors.New("failed to put object to S3")
errCloseFailed = errors.New("close failed")
errReadAllFailed = errors.New("simulated io.ReadAll error")
errS3Test = errors.New("s3 error")
)
// Helper function to create a new S3File instance for testing.
func newTestS3File(t *testing.T, ctrl *gomock.Controller, name string, size, offset int64) *S3File {
t.Helper()
return newTestS3FileWithTime(t, ctrl, name, size, offset, time.Now())
}
// Helper function to create a new S3File instance for testing with custom time.
func newTestS3FileWithTime(_ *testing.T, ctrl *gomock.Controller, name string, size, offset int64,
lastModified time.Time) *S3File {
mockClient := NewMocks3Client(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
return &S3File{
conn: mockClient,
name: name,
offset: offset,
logger: mockLogger,
metrics: mockMetrics,
size: size,
lastModified: lastModified,
}
}
// Helper to create a successful GetObjectOutput.
func getObjectOutput(content string) *s3.GetObjectOutput {
return &s3.GetObjectOutput{
Body: io.NopCloser(bytes.NewReader([]byte(content))),
ContentLength: aws.Int64(int64(len(content))),
}
}
// Helper to create a successful PutObjectOutput.
func putObjectOutput() *s3.PutObjectOutput {
return &s3.PutObjectOutput{}
}
// Define mock for io.ReadCloser to test Close.
type mockReadCloser struct {
io.Reader
closeErr error
}
func (m *mockReadCloser) Close() error {
return m.closeErr
}
// TestS3File_Close_Success tests the successful Close operations of S3File.
func TestS3File_Close_Success(t *testing.T) {
testCases := []struct {
name string
body io.ReadCloser
}{
{
name: "Success_BodyNil",
body: nil,
},
{
name: "Success_BodyNotNil",
body: &mockReadCloser{Reader: bytes.NewReader([]byte("test")), closeErr: nil},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, "test-bucket/test-file.txt", 10, 0)
f.body = tc.body
err := f.Close()
assert.NoError(t, err, "Expected no error")
})
}
}
// TestS3File_Close_Failure tests the failure cases of S3File Close operations.
func TestS3File_Close_Failure(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, "test-bucket/test-file.txt", 10, 0)
f.body = &mockReadCloser{Reader: bytes.NewReader([]byte("test")), closeErr: errCloseFailed}
err := f.Close()
require.Error(t, err, "Expected an error")
assert.True(t, errors.Is(err, errCloseFailed) || strings.Contains(err.Error(), errCloseFailed.Error()),
"Expected error to be %v or contain %q, got %v", errCloseFailed, errCloseFailed.Error(), err)
}
// TestS3File_Read_Success tests the successful Read operations of S3File.
func TestS3File_Read_Success(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
content := "This is a test file content."
testCases := []struct {
name string
offset int64
bufferLen int
mockGetObject func(m *Mocks3ClientMockRecorder)
expectedN int
expectedP string
expectedErr error
}{
{
name: "Success_ReadFromStart",
offset: 0,
bufferLen: 5,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Eq(&s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})).Return(getObjectOutput(content), nil)
},
expectedN: 5,
expectedP: "This ",
expectedErr: nil,
},
{
name: "Success_ReadFromOffset",
offset: 5,
bufferLen: 4,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Eq(&s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(fileName),
})).Return(getObjectOutput(content), nil)
},
expectedN: 4,
expectedP: "is a",
expectedErr: nil,
},
{
name: "Success_ReadToEOF",
offset: 0,
bufferLen: len(content),
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil)
},
expectedN: len(content),
expectedP: content,
expectedErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, int64(len(content)), tc.offset)
m := f.conn.(*Mocks3Client)
tc.mockGetObject(m.EXPECT())
p := make([]byte, tc.bufferLen)
for i := range p {
p[i] = 0
}
n, err := f.Read(p)
require.NoError(t, err, "Expected no error")
assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n)
assert.Equal(t, tc.expectedP[:n], string(p[:n]), "Expected content %q, got %q", tc.expectedP[:n], string(p[:n]))
})
}
}
// TestS3File_Read_Failure tests the failure cases of S3File Read operations.
func TestS3File_Read_Failure(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
content := "This is a test file content."
testCases := []struct {
name string
offset int64
bufferLen int
mockGetObject func(m *Mocks3ClientMockRecorder)
expectedN int
expectedP string
expectedErr error
}{
{
name: "Failure_GetObjectError",
offset: 0,
bufferLen: 10,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test)
},
expectedN: 0,
expectedP: "",
expectedErr: errS3Test,
},
{
name: "Failure_NilResponse",
offset: 0,
bufferLen: 10,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: nil}, nil)
},
expectedN: 0,
expectedP: "",
expectedErr: ErrNilResponse,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, int64(len(content)), tc.offset)
m := f.conn.(*Mocks3Client)
tc.mockGetObject(m.EXPECT())
p := make([]byte, tc.bufferLen)
for i := range p {
p[i] = 0
}
n, err := f.Read(p)
require.Error(t, err, "Expected an error")
require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err)
assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n)
})
}
}
// TestS3File_ReadAt_Success tests the successful ReadAt operations of S3File.
func TestS3File_ReadAt_Success(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
content := "This is a test file content."
fileSize := int64(len(content))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, fileSize, 10)
m := f.conn.(*Mocks3Client)
m.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil)
p := make([]byte, 4)
n, err := f.ReadAt(p, 5)
require.NoError(t, err, "Expected no error")
assert.Equal(t, 4, n, "Expected bytes read 4, got %d", n)
assert.Equal(t, "is a", string(p[:n]), "Expected content %q, got %q", "is a", string(p[:n]))
assert.Equal(t, int64(10), f.offset, "ReadAt modified offset. Expected 10, got %d", f.offset)
}
// TestS3File_ReadAt_Failure tests the failure cases of S3File ReadAt operations.
func TestS3File_ReadAt_Failure(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
content := "This is a test file content."
fileSize := int64(len(content))
testCases := []struct {
name string
readAtOffset int64
bufferLen int
mockGetObject func(m *Mocks3ClientMockRecorder)
expectedN int
expectedErr error
}{
{
name: "Failure_GetObjectError",
readAtOffset: 0,
bufferLen: 10,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test)
},
expectedN: 0,
expectedErr: errS3Test,
},
{
name: "Failure_NilBody",
readAtOffset: 0,
bufferLen: 10,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{Body: nil}, nil)
},
expectedN: 0,
expectedErr: io.EOF,
},
{
name: "Failure_OutOfRange",
readAtOffset: 25,
bufferLen: 4,
mockGetObject: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(content), nil)
},
expectedN: 0,
expectedErr: ErrOutOfRange,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, fileSize, 10)
m := f.conn.(*Mocks3Client)
tc.mockGetObject(m.EXPECT())
p := make([]byte, tc.bufferLen)
n, err := f.ReadAt(p, tc.readAtOffset)
require.Error(t, err, "Expected an error")
require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err)
assert.Equal(t, tc.expectedN, n, "Expected bytes read %d, got %d", tc.expectedN, n)
})
}
}
// TestS3File_Write_Success tests the successful Write operations of S3File.
func TestS3File_Write_Success(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
initialContent := "Hello, World!"
initialSize := int64(len(initialContent))
dataToWrite := []byte("GoFr")
testCases := []struct {
name string
initialOffset int64
initialSize int64
dataToWrite []byte
mockExpectations func(m *Mocks3ClientMockRecorder)
expectedN int
expectedOffset int64
expectedSize int64
expectedErr error
}{
{
name: "Success_WriteFromStart_NewFile",
initialOffset: 0,
initialSize: 0,
dataToWrite: dataToWrite,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil)
},
expectedN: len(dataToWrite),
expectedOffset: int64(len(dataToWrite)),
expectedSize: int64(len(dataToWrite)),
expectedErr: nil,
},
{
name: "Success_WriteFromStart_Overwrite",
initialOffset: 0,
initialSize: initialSize,
dataToWrite: dataToWrite,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil)
},
expectedN: len(dataToWrite),
expectedOffset: int64(len(dataToWrite)),
expectedSize: int64(len(dataToWrite)),
expectedErr: nil,
},
{
name: "Success_WriteFromMiddle",
initialOffset: 7,
initialSize: initialSize,
dataToWrite: dataToWrite,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil)
m.PutObject(gomock.Any(), gomock.Any()).Return(putObjectOutput(), nil)
},
expectedN: len(dataToWrite),
expectedOffset: 7 + int64(len(dataToWrite)),
expectedSize: initialSize,
expectedErr: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, tc.initialSize, tc.initialOffset)
m := f.conn.(*Mocks3Client)
tc.mockExpectations(m.EXPECT())
n, err := f.Write(tc.dataToWrite)
require.NoError(t, err, "Expected no error")
assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n)
assert.Equal(t, tc.expectedOffset, f.offset, "Expected offset %d, got %d", tc.expectedOffset, f.offset)
assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size)
})
}
}
// TestS3File_Write_Failure tests the failure cases of S3File Write operations.
func TestS3File_Write_Failure(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
initialContent := "Hello, World!"
initialSize := int64(len(initialContent))
dataToWrite := []byte("GoFr")
testCases := []struct {
name string
initialOffset int64
initialSize int64
dataToWrite []byte
mockExpectations func(m *Mocks3ClientMockRecorder)
expectedN int
expectedOffset int64
expectedSize int64
expectedErr error
}{
{
name: "Failure_GetObjectError",
initialOffset: 5,
initialSize: initialSize,
dataToWrite: dataToWrite,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test)
},
expectedN: 0,
expectedOffset: 5,
expectedSize: initialSize,
expectedErr: errS3Test,
},
{
name: "Failure_PutObjectError",
initialOffset: 0,
initialSize: initialSize,
dataToWrite: dataToWrite,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.PutObject(gomock.Any(), gomock.Any()).Return(nil, errS3Test)
},
expectedN: 0,
expectedOffset: 0,
expectedSize: initialSize,
expectedErr: errS3Test,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, tc.initialSize, tc.initialOffset)
m := f.conn.(*Mocks3Client)
tc.mockExpectations(m.EXPECT())
n, err := f.Write(tc.dataToWrite)
require.Error(t, err, "Expected an error")
require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err)
assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n)
assert.Equal(t, tc.expectedOffset, f.offset, "Expected offset %d, got %d", tc.expectedOffset, f.offset)
assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size)
})
}
}
// TestS3File_WriteAt_Success tests the successful WriteAt operations of S3File.
func TestS3File_WriteAt_Success(t *testing.T) {
bucketName, fileName := "test-bucket", "test-file.txt"
fullPath := bucketName + "/" + fileName
initialContent := "Hello, World!"
initialSize, dataToWrite := int64(len(initialContent)), []byte("GoFr")
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, initialSize, 10)
m := f.conn.(*Mocks3Client)
m.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil)
expectedPutBody := []byte("Hello, GoFrd!")
m.EXPECT().PutObject(gomock.Any(), gomock.Any()).Do(func(_ context.Context, params *s3.PutObjectInput, _ ...func(*s3.Options)) {
actualPutBody := getBodyContent(t, params.Body)
require.True(t, bytes.Equal(expectedPutBody, actualPutBody),
"PutObject Body mismatch. Expected: %q, Got: %q", string(expectedPutBody), string(actualPutBody))
}).Return(putObjectOutput(), nil)
n, err := f.WriteAt(dataToWrite, 7)
require.NoError(t, err, "Expected no error")
assert.Equal(t, len(dataToWrite), n, "Expected bytes written %d, got %d", len(dataToWrite), n)
assert.Equal(t, int64(10), f.offset, "WriteAt modified offset. Expected 10, got %d", f.offset)
assert.Equal(t, initialSize, f.size, "Expected size %d, got %d", initialSize, f.size)
}
// TestS3File_WriteAt_Failure tests the failure cases of S3File WriteAt operations.
func TestS3File_WriteAt_Failure(t *testing.T) {
bucketName, fileName := "test-bucket", "test-file.txt"
fullPath := bucketName + "/" + fileName
initialContent := "Hello, World!"
initialSize, dataToWrite := int64(len(initialContent)), []byte("GoFr")
testCases := []struct {
name string
writeAtOffset int64
initialOffset int64
mockExpectations func(m *Mocks3ClientMockRecorder)
expectedN int
expectedOffset int64
expectedSize int64
expectedErr error
}{
{
name: "Failure_GetObjectError",
initialOffset: 10,
writeAtOffset: 5,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(nil, errGetObject)
},
expectedN: 0,
expectedOffset: 10,
expectedSize: initialSize,
expectedErr: errGetObject,
},
{
name: "Failure_PutObjectError",
initialOffset: 10,
writeAtOffset: 0,
mockExpectations: func(m *Mocks3ClientMockRecorder) {
m.GetObject(gomock.Any(), gomock.Any()).Return(getObjectOutput(initialContent), nil)
m.PutObject(gomock.Any(), gomock.Any()).Return(nil, errPutObject)
},
expectedN: 0,
expectedOffset: 10,
expectedSize: initialSize,
expectedErr: errPutObject,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, initialSize, tc.initialOffset)
m := f.conn.(*Mocks3Client)
tc.mockExpectations(m.EXPECT())
n, err := f.WriteAt(dataToWrite, tc.writeAtOffset)
require.Error(t, err, "Expected an error")
require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err)
assert.Equal(t, tc.expectedN, n, "Expected bytes written %d, got %d", tc.expectedN, n)
assert.Equal(t, tc.expectedOffset, f.offset, "WriteAt modified offset. Expected %d, got %d", tc.expectedOffset, f.offset)
assert.Equal(t, tc.expectedSize, f.size, "Expected size %d, got %d", tc.expectedSize, f.size)
})
}
}
// Helper to read the content of the PutObjectInput Body.
func getBodyContent(t *testing.T, body io.Reader) []byte {
t.Helper()
b, err := io.ReadAll(body)
require.NoError(t, err, "Failed to read PutObject body")
return b
}
// TestS3File_Seek_Success tests the successful Seek operations of S3File.
func TestS3File_Seek_Success(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
fileSize := int64(20)
testCases := []struct {
name string
initialOffset int64
offset int64
whence int
expectedNewOffset int64
}{
{
name: "SeekStart_Success",
initialOffset: 5,
offset: 10,
whence: io.SeekStart,
expectedNewOffset: 10,
},
{
name: "SeekCurrent_Success",
initialOffset: 5,
offset: 10,
whence: io.SeekCurrent,
expectedNewOffset: 15,
},
{
name: "SeekEnd_Success",
initialOffset: 5,
offset: -5,
whence: io.SeekEnd,
expectedNewOffset: 15,
},
{
name: "SeekEnd_Success_ToStart",
initialOffset: 5,
offset: -20,
whence: io.SeekEnd,
expectedNewOffset: 0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, fileSize, tc.initialOffset)
newOffset, err := f.Seek(tc.offset, tc.whence)
require.NoError(t, err, "Expected no error")
assert.Equal(t, tc.expectedNewOffset, newOffset, "Expected new offset %d, got %d", tc.expectedNewOffset, newOffset)
assert.Equal(t, tc.expectedNewOffset, f.offset, "File struct offset was not updated. "+
"Expected %d, got %d", tc.expectedNewOffset, f.offset)
})
}
}
// TestS3File_Seek_Failure tests the failure cases of S3File Seek operations.
func TestS3File_Seek_Failure(t *testing.T) {
bucketName := "test-bucket"
fileName := "test-file.txt"
fullPath := bucketName + "/" + fileName
fileSize := int64(20)
testCases := []struct {
name string
initialOffset int64
offset int64
whence int
expectedErr error
}{
{
name: "SeekStart_Failure_Negative",
initialOffset: 5,
offset: -1,
whence: io.SeekStart,
expectedErr: ErrOutOfRange,
},
{
name: "SeekStart_Failure_TooLarge",
initialOffset: 5,
offset: 21,
whence: io.SeekStart,
expectedErr: ErrOutOfRange,
},
{
name: "SeekCurrent_Failure_NegativeResult",
initialOffset: 5,
offset: -6,
whence: io.SeekCurrent,
expectedErr: ErrOutOfRange,
},
{
name: "SeekEnd_Failure_TooLarge",
initialOffset: 5,
offset: 1,
whence: io.SeekEnd,
expectedErr: ErrOutOfRange,
},
{
name: "Seek_InvalidWhence",
initialOffset: 5,
offset: 0,
whence: 3, // Invalid whence
expectedErr: os.ErrInvalid,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3File(t, ctrl, fullPath, fileSize, tc.initialOffset)
_, err := f.Seek(tc.offset, tc.whence)
require.Error(t, err, "Expected an error")
require.ErrorIs(t, err, tc.expectedErr, "Expected error %v, got %v", tc.expectedErr, err)
})
}
}
// TestJsonReader_ValidObjects tests reading valid JSON objects from a jsonReader.
func TestJsonReader_ValidObjects(t *testing.T) {
jsonContent := `[
{"name": "Alice", "age": 30},
{"name": "Bob", "age": 25}
]`
reader := bytes.NewReader([]byte(jsonContent))
decoder := json.NewDecoder(reader)
_, _ = decoder.Token()
jReader := jsonReader{decoder: decoder}
require.True(t, jReader.Next(), "Expected Next to be true for the first object")
var data1 struct {
Name string
Age int
}
require.NoError(t, jReader.Scan(&data1), "Scan failed for first object")
assert.Equal(t, "Alice", data1.Name)
assert.Equal(t, 30, data1.Age)
require.True(t, jReader.Next(), "Expected Next to be true for the second object")
var data2 struct {
Name string
Age int
}
require.NoError(t, jReader.Scan(&data2), "Scan failed for second object")
assert.Equal(t, "Bob", data2.Name)
assert.Equal(t, 25, data2.Age)
}
// TestJsonReader_NullAndEnd tests reading null values and end of array from a jsonReader.
func TestJsonReader_NullAndEnd(t *testing.T) {
jsonContent := `[
{"name": "Alice", "age": 30},
null
]`
reader := bytes.NewReader([]byte(jsonContent))
decoder := json.NewDecoder(reader)
_, _ = decoder.Token()
jReader := jsonReader{decoder: decoder}
jReader.Next()
err := jReader.Scan(&struct{}{})
require.NoError(t, err, "Scan failed for null object")
require.True(t, jReader.Next(), "Expected Next to be true for the null object")
var data3 any
require.NoError(t, jReader.Scan(&data3), "Scan failed for null object")
assert.Nil(t, data3)
assert.False(t, jReader.Next(), "Expected Next to be false at the end of the array")
var invalidScanTarget struct{}
require.Error(t, jReader.Scan(&invalidScanTarget), "Expected Scan to fail after array end")
}
// TestS3File_Metadata_Methods tests simple metadata methods.
func TestS3File_Metadata_Methods(t *testing.T) {
bucketName := "test-bucket"
fileName := "path/to/my-file.txt"
fullPath := bucketName + "/" + fileName
testSize := int64(4096)
testTime := time.Date(2023, 10, 10, 12, 0, 0, 0, time.UTC)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
f := newTestS3FileWithTime(t, ctrl, fullPath, testSize, 0, testTime)
expectedName := "my-file.txt"
assert.Equal(t, expectedName, f.Name())
assert.Equal(t, testSize, f.Size())
assert.Equal(t, testTime, f.ModTime())
assert.False(t, f.IsDir())
f.name = bucketName + "/path/to/my-dir/"
assert.True(t, f.IsDir())
assert.Equal(t, os.FileMode(0), f.Mode())
}
// createMockBodyWithError creates a MockReadCloser that returns an error when reading.
func createMockBodyWithError(bodyReadError error) *MockReadCloser {
return &MockReadCloser{
Reader: io.NopCloser(errorReader{err: bodyReadError}),
CloseFunc: func() error {
return nil
},
}
}
// createMockBodyWithContent creates a MockReadCloser with the provided content.
func createMockBodyWithContent(fileBody []byte) *MockReadCloser {
return &MockReadCloser{
Reader: bytes.NewReader(fileBody),
CloseFunc: func() error {
return nil
},
}
}
// Helper function for creating a new S3File instance for a test.
func newS3FileForReadAll(t *testing.T, ctrl *gomock.Controller, name string, body io.ReadCloser) *S3File {
t.Helper()
f := newTestS3File(t, ctrl, name, 0, 0)
f.body = body
return f
}
// TestS3File_ReadAll_JSONArray_Success tests reading JSON array from S3File.
func TestS3File_ReadAll_JSONArray_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithContent([]byte(`[{"id": 1}, {"id": 2}]`))
f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/data.json", mockBody)
reader, err := f.ReadAll()
require.NoError(t, err, "ReadAll() unexpected error")
require.NotNil(t, reader, "ReadAll() returned nil reader on success")
assert.IsType(t, &jsonReader{}, reader, "ReadAll() for JSON array expected *jsonReader")
}
// TestS3File_ReadAll_JSONObject_Success tests reading JSON object from S3File.
func TestS3File_ReadAll_JSONObject_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithContent([]byte(`{"key": "value"}`))
f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/config.json", mockBody)
reader, err := f.ReadAll()
require.NoError(t, err, "ReadAll() unexpected error")
require.NotNil(t, reader, "ReadAll() returned nil reader on success")
assert.IsType(t, &jsonReader{}, reader, "ReadAll() for JSON object expected *jsonReader")
}
// TestS3File_ReadAll_Text_Success tests reading text/CSV from S3File.
func TestS3File_ReadAll_Text_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithContent([]byte("col1,col2\n1,2"))
f := newS3FileForReadAll(t, ctrl, "my-bucket/path/to/data.csv", mockBody)
reader, err := f.ReadAll()
require.NoError(t, err, "ReadAll() unexpected error")
require.NotNil(t, reader, "ReadAll() returned nil reader on success")
assert.IsType(t, &textReader{}, reader, "ReadAll() for text file expected *textReader")
}
// TestS3File_ReadAll_JSON_Error tests ReadAll error for JSON file.
func TestS3File_ReadAll_JSON_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithError(errReadAllFailed)
f := newS3FileForReadAll(t, ctrl, "my-bucket/fail.json", mockBody)
reader, err := f.ReadAll()
require.Error(t, err, "ReadAll() expected an error, but got nil")
assert.Nil(t, reader, "ReadAll() expected nil reader on error")
}
// TestS3File_ReadAll_Text_Error tests ReadAll error for text/CSV file.
func TestS3File_ReadAll_Text_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithError(errReadAllFailed)
f := newS3FileForReadAll(t, ctrl, "my-bucket/fail.txt", mockBody)
reader, err := f.ReadAll()
require.Error(t, err, "ReadAll() expected an error, but got nil")
assert.Nil(t, reader, "ReadAll() expected nil reader on error")
}
// TestS3File_ReadAll_JSONInvalidToken_Error tests ReadAll error for invalid JSON.
func TestS3File_ReadAll_JSONInvalidToken_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockBody := createMockBodyWithContent([]byte(`not a json`))
f := newS3FileForReadAll(t, ctrl, "my-bucket/invalid.json", mockBody)
reader, err := f.ReadAll()
require.Error(t, err, "ReadAll() expected an error, but got nil")
assert.Nil(t, reader, "ReadAll() expected nil reader on error")
}
// errorReader is a helper to simulate an io.ReadAll failure for testing.
type errorReader struct {
err error
}
func (er errorReader) Read(_ []byte) (n int, err error) {
return 0, er.err
}
// MockReadCloser is a minimal mock for the io.ReadCloser field 'f.body'.
type MockReadCloser struct {
io.Reader
CloseFunc func() error
}
func (m *MockReadCloser) Close() error {
return m.CloseFunc()
}
func TestFileSystem_Connect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
successConfig := &Config{
BucketName: "test-bucket",
Region: "us-east-1",
AccessKeyID: "AKIA_SUCCESS",
SecretAccessKey: "SECRET_SUCCESS",
EndPoint: "http://localhost:9000",
}
t.Run("SuccessCase", func(t *testing.T) {
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
fs := &FileSystem{
config: successConfig,
logger: mockLogger,
}
fs.Connect()
assert.NotNil(t, fs.conn, "Connect() failed to initialize S3 client")
})
}
================================================
FILE: pkg/gofr/datasource/file/s3/fs.go
================================================
package s3
import (
"bytes"
"context"
"errors"
"fmt"
"mime"
"os"
"path"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
file "gofr.dev/pkg/gofr/datasource/file"
)
const (
typeFile = "file"
typeDirectory = "directory"
)
var (
errIncorrectFileType = errors.New("incorrect file type")
)
// client struct embeds the *s3.Client.
type client struct {
*s3.Client
}
type FileSystem struct {
s3File S3File
conn s3Client
config *Config
logger Logger
metrics Metrics
}
// Config represents the s3 configuration.
type Config struct {
EndPoint string // AWS S3 base endpoint
BucketName string // AWS Bucket name
Region string // AWS Region
AccessKeyID string // Aws configs
SecretAccessKey string // Aws configs
}
// New initializes a new instance of FTP fileSystem with provided configuration.
func New(config *Config) file.FileSystemProvider {
return &FileSystem{config: config}
}
// UseLogger sets the Logger interface for the FTP file system.
func (f *FileSystem) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
f.logger = l
}
}
// UseMetrics sets the Metrics interface.
func (f *FileSystem) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
f.metrics = m
}
}
// Connect initializes and validates the connection to the S3 service.
//
// This method sets up the S3 client using the provided configuration, including access key, secret key, region, and base endpoint.
// It loads the AWS configuration and creates an S3 client, which is then assigns it to the `fileSystem` struct.
// This method also logs the outcome of the connection attempt.
func (f *FileSystem) Connect() {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "CONNECT",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
f.logger.Debugf("connecting to S3 bucket: %s", f.config.BucketName)
// Load the AWS configuration
cfg, err := awsConfig.LoadDefaultConfig(context.TODO(),
awsConfig.WithRegion(f.config.Region),
awsConfig.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
f.config.AccessKeyID,
f.config.SecretAccessKey,
"")), // "" is the session token. Currently, we do not handle connections through session token.
)
if err != nil {
f.logger.Errorf("failed to load configuration: %v", err)
return
}
// Create the S3 client from config
s3Client := s3.NewFromConfig(cfg,
func(o *s3.Options) {
o.UsePathStyle = true
o.BaseEndpoint = &f.config.EndPoint
},
)
f.conn = client{s3Client}
st = statusSuccess
msg = "S3 Client connected."
f.logger.Logf("connected to S3 bucket %s", f.config.BucketName)
}
// Create creates a new file in the S3 bucket.
//
// This method creates an empty file at the specified path in the S3 bucket. It first checks if the parent directory exists;
// if the parent directory does not exist, it returns an error. After creating the file, it retrieves the file metadata
// and returns a `file` object representing the newly created file.
func (f *FileSystem) Create(name string) (file.File, error) {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "CREATE FILE",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
parentPath := path.Dir(name)
// if parentPath is not empty, we check if it exists or not.
if parentPath != "." {
res2, err := f.conn.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String(f.config.BucketName),
Prefix: aws.String(parentPath + "/"),
})
if err != nil {
return nil, err
}
if len(res2.Contents) == 0 {
f.logger.Errorf("Parentpath %q does not exist", parentPath)
return nil, fmt.Errorf("%w: create parent path before creating a file", ErrOperationNotPermitted)
}
}
_, err := f.conn.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(f.config.BucketName),
Key: aws.String(name),
Body: bytes.NewReader(make([]byte, 0)),
ContentType: aws.String(mime.TypeByExtension(path.Ext(name))),
// this specifies the file must be downloaded before being opened
ContentDisposition: aws.String("attachment"),
})
if err != nil {
f.logger.Errorf("Failed to create the file: %v", err)
return nil, err
}
res, err := f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(f.config.BucketName),
Key: aws.String(name),
})
if err != nil {
f.logger.Errorf("Failed to retrieve %q: %v", name, err)
return nil, err
}
st = statusSuccess
msg = "File creation on S3 successful."
f.logger.Logf("File with name %s created.", name)
return &S3File{
conn: f.conn,
name: path.Join(f.config.BucketName, name),
logger: f.logger,
metrics: f.metrics,
body: res.Body,
contentType: *res.ContentType,
lastModified: *res.LastModified,
size: *res.ContentLength,
}, nil
}
// Remove deletes a file from the S3 bucket.
//
// This method deletes the specified file from the S3 bucket. Currently, it supports the deletion of unversioned files
// from general-purpose buckets only. Directory buckets and versioned files are not supported for deletion by this method.
func (f *FileSystem) Remove(name string) error {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "REMOVE FILE",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
_, err := f.conn.DeleteObject(context.TODO(), &s3.DeleteObjectInput{
Bucket: aws.String(f.config.BucketName),
Key: aws.String(name),
})
if err != nil {
f.logger.Errorf("Error while deleting file: %v", err)
return err
}
st = statusSuccess
msg = "File deletion on S3 successful"
f.logger.Logf("File with path %q deleted", name)
return nil
}
// Open retrieves a file from the S3 bucket and returns a `file` object representing it.
//
// This method fetches the specified file from the S3 bucket and returns a `file` object with its content and metadata.
// If the file cannot be retrieved, it returns an error.
func (f *FileSystem) Open(name string) (file.File, error) {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "OPEN FILE",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
res, err := f.conn.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(f.config.BucketName),
Key: aws.String(name),
})
if err != nil {
f.logger.Errorf("failed to retrieve %q: %v", name, err)
return nil, err
}
st = statusSuccess
msg = fmt.Sprintf("File with path %q retrieved successfully", name)
return &S3File{
conn: f.conn,
name: path.Join(f.config.BucketName, name),
logger: f.logger,
metrics: f.metrics,
body: res.Body,
contentType: *res.ContentType,
lastModified: *res.LastModified,
size: *res.ContentLength,
}, nil
}
// OpenFile is a wrapper for the Open method to comply with the generic FileSystem interface.
//
// This method calls the `Open` method of the `fileSystem` struct to retrieve a file. It is provided to align with the
// FileSystem interface requirements in the GoFr framework.
func (f *FileSystem) OpenFile(name string, _ int, _ os.FileMode) (file.File, error) {
return f.Open(name)
}
// Rename changes the name of a file or directory within the S3 bucket.
//
// This method handles both files and directories. It ensures that:
// - The new name does not move the file to a different directory.
// - The file types of the old and new names match.
//
// If the old and new names are the same, no operation is performed.
func (f *FileSystem) Rename(oldname, newname string) error {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "RENAME",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
// check if they have the same name or not
if oldname == newname {
f.logger.Logf("%q & %q are same", oldname, newname)
return nil
}
// check if both exist at same location or not
if path.Dir(oldname) != path.Dir(newname) {
f.logger.Errorf("%q & %q are not in same location", oldname, newname)
return fmt.Errorf("%w: renaming as well as moving file to different location is not allowed", ErrOperationNotPermitted)
}
// check if it is a directory
if path.Ext(oldname) == "" {
return f.renameDirectory(&st, &msg, oldname, newname)
}
// check if they are of the same type or not
if path.Ext(oldname) != path.Ext(newname) {
f.logger.Errorf("new file must be same as the old file type")
return fmt.Errorf("%w: new filename must match the old file's type", errIncorrectFileType)
}
_, err := f.conn.CopyObject(context.TODO(), &s3.CopyObjectInput{
Bucket: aws.String(f.config.BucketName),
// The source object can be up to 5 GB.
// If the source object is an object that was uploaded by using a multipart upload, the object copy
// will be a single part object after the source object is copied to the destination bucket.
CopySource: aws.String(f.config.BucketName + "/" + oldname),
Key: aws.String(newname),
ContentType: aws.String(mime.TypeByExtension(path.Ext(newname))),
ContentDisposition: aws.String("attachment"),
})
if err != nil {
msg = fmt.Sprintf("Error while copying file: %v", err)
return err
}
err = f.Remove(oldname)
if err != nil {
msg = fmt.Sprintf("failed to remove old file %s", oldname)
return err
}
st = statusSuccess
msg = "File renamed successfully"
f.logger.Logf("File with path %q renamed to %q", oldname, newname)
return nil
}
================================================
FILE: pkg/gofr/datasource/file/s3/fs_dir.go
================================================
package s3
import (
"context"
"errors"
"fmt"
"mime"
"os"
"path"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
file "gofr.dev/pkg/gofr/datasource/file"
)
var (
ErrOperationNotPermitted = errors.New("operation not permitted")
)
// getBucketName returns the currentS3Bucket.
func getBucketName(filePath string) string {
return strings.Split(filePath, string(filepath.Separator))[0]
}
// getLocation returns the absolute path of the S3 bucket.
func getLocation(bucket string) string {
return path.Join(string(filepath.Separator), bucket)
}
// Mkdir creates a directory and any necessary parent directories in the S3 bucket.
//
// This method creates a pseudo-directory in the S3 bucket by putting objects with the specified path prefixes.
// Since S3 uses a flat storage structure, directories are represented by object keys with trailing slashes.
// The method processes the path segments and ensures that each segment (directory) exists in S3.
func (f *FileSystem) Mkdir(name string, _ os.FileMode) error {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "MKDIR",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
directories := strings.Split(name, string(filepath.Separator))
var currentdir string
for _, dir := range directories {
currentdir = path.Join(currentdir, dir)
_, err := f.conn.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(f.config.BucketName),
Key: aws.String(currentdir + "/"),
})
if err != nil {
msg = fmt.Sprintf("failed to create directory %q on s3: %v", currentdir, err)
return err
}
}
st = statusSuccess
msg = fmt.Sprintf("Directories on path %q created successfully", name)
f.logger.Logf("Created directories on path %q", name)
return nil
}
// MkdirAll creates directories in the S3 bucket.
//
// This method calls `MkDir` because AWS S3 buckets do not support traditional directory or file structures.
// Instead, they use a flat structure.
// S3 treats paths as part of object keys, so creating a directory is functionally equivalent to creating an
// object with a specific prefix.
func (f *FileSystem) MkdirAll(name string, perm os.FileMode) error {
return f.Mkdir(name, perm)
}
// RemoveAll deletes a directory and all its contents from the S3 bucket.
//
// This method removes a directory and all objects within it from the S3 bucket. It only supports deleting directories
// and will return an error if a file path (as indicated by a file extension) is provided. The method lists all objects
// under the specified directory prefix and deletes them in a single batch operation.
func (f *FileSystem) RemoveAll(name string) error {
if path.Ext(name) != "" {
return f.Remove(name)
}
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "REMOVEALL",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
res, err := f.conn.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String(f.config.BucketName),
Prefix: aws.String(name + "/"),
})
if err != nil {
msg = fmt.Sprintf("Error retrieving objects: %v", err)
return err
}
objects := make([]types.ObjectIdentifier, len(res.Contents))
for i, obj := range res.Contents {
objects[i] = types.ObjectIdentifier{
Key: obj.Key,
}
}
_, err = f.conn.DeleteObjects(context.TODO(), &s3.DeleteObjectsInput{
Bucket: aws.String(f.config.BucketName),
Delete: &types.Delete{
Objects: objects,
},
})
if err != nil {
f.logger.Errorf("Error while deleting directory: %v", err)
return err
}
st = statusSuccess
msg = fmt.Sprintf("Directory with path %q, deleted successfully", name)
f.logger.Logf("Directory %s deleted.", name)
return nil
}
func getRelativepath(key, filePath string) string {
relativepath := strings.TrimPrefix(key, filePath)
oneLevelDeepPathIndex := strings.Index(relativepath, string(filepath.Separator))
if oneLevelDeepPathIndex != -1 {
relativepath = relativepath[:oneLevelDeepPathIndex+1]
}
return relativepath
}
// ReadDir lists the files and directories within the specified directory in the S3 bucket.
//
// This method retrieves and returns information about the files and directories located under the specified path
// within the S3 bucket. It uses the provided directory name to construct the S3 prefix for listing objects.
// It returns a slice of `file_interface.FileInfo` representing the files and directories found. If the directory name is
// ".", it lists the contents at the root of the bucket.
// Note:
// - Directories are represented by the prefixes of the file keys in S3, and this method retrieves file entries
// only one level deep from the specified directory.
func (f *FileSystem) ReadDir(name string) ([]file.FileInfo, error) {
var filePath, msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "READDIR",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
filePath = name + string(filepath.Separator)
if name == "." {
filePath = ""
}
// TODO: Enhance the implementation to fetch only data that is one level deep.
// Currently, the system retrieves metadata of all files matching the prefix,
// which may include files in nested directories. This takes more memory.
entries, err := f.conn.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String(f.config.BucketName),
Prefix: aws.String(filePath),
})
if err != nil {
msg = fmt.Sprintf("Error retrieving objects: %v", err)
return nil, err
}
fileInfo := make([]file.FileInfo, 0)
for i := range entries.Contents {
if i == 0 && filePath != "" {
continue
}
relativepath := getRelativepath(*entries.Contents[i].Key, filePath)
if len(fileInfo) > 0 {
temp, ok := fileInfo[len(fileInfo)-1].(*S3File)
if ok && relativepath == path.Base(temp.name)+string(filepath.Separator) {
continue
}
}
fileInfo = append(fileInfo, &S3File{
conn: f.conn,
logger: f.logger,
metrics: f.metrics,
size: *entries.Contents[i].Size,
name: f.config.BucketName + string(filepath.Separator) + *entries.Contents[i].Key,
lastModified: *entries.Contents[i].LastModified,
})
}
st = statusSuccess
msg = fmt.Sprintf("Directory/Files in directory with path %q retrieved successfully", name)
f.logger.Logf("Reading directory/files from S3 at path %q successful.", name)
return fileInfo, nil
}
// ChDir is not supported in S3 as the bucket is constant and the filesystem requires a full path relative to the selected bucket.
//
// This method attempts to change the current directory, but S3 does not support directory changes due to its flat file structure.
// The bucket is constant and fixed, so directory operations are not applicable.
func (f *FileSystem) ChDir(string) error {
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "CHDIR",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: aws.String("Changing directory not supported"),
}, time.Now())
return fmt.Errorf("%w: s3 has a flat file structure", ErrOperationNotPermitted)
}
// Getwd returns the currently set bucket on S3.
//
// This method retrieves the name of the bucket that is currently set for S3 operations.
func (f *FileSystem) Getwd() (string, error) {
status := statusSuccess
f.sendOperationStats(&FileLog{Operation: "GETWD", Location: getLocation(f.config.BucketName), Status: &status}, time.Now())
return getLocation(f.config.BucketName), nil
}
// renameDirectory renames a directory by copying all its contents to a new path and then deleting the old path.
//
// This method handles the process of renaming a directory in an S3 bucket. It first lists all objects under the old
// directory path, copies each object to the new directory path, and then deletes the old directory and its contents.
func (f *FileSystem) renameDirectory(st, msg *string, oldPath, newPath string) error {
entries, err := f.conn.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String(f.config.BucketName),
Prefix: aws.String(oldPath + "/"),
})
if err != nil {
f.logger.Errorf("Error while listing objects: %v", err)
return err
}
// copying objects to new path
for _, obj := range entries.Contents {
newFilePath := strings.Replace(*obj.Key, oldPath, newPath, 1)
_, err = f.conn.CopyObject(context.TODO(), &s3.CopyObjectInput{
Bucket: aws.String(f.config.BucketName),
CopySource: aws.String(f.config.BucketName + "/" + *obj.Key),
Key: aws.String(newFilePath),
ContentType: aws.String(mime.TypeByExtension(path.Ext(newPath))),
ContentDisposition: aws.String("attachment"),
})
if err != nil {
*msg = fmt.Sprintf("Failed to copy objects to directory %q", newPath)
return err
}
}
// deleting objects
err = f.RemoveAll(oldPath)
if err != nil {
*msg = fmt.Sprintf("Failed to remove old objects from the directories %q", oldPath)
return err
}
*st = statusSuccess
*msg = fmt.Sprintf("Directory with path %q successfully renamed to %q", oldPath, newPath)
return nil
}
// Stat retrieves the FileInfo for the specified file or directory in the S3 bucket.
//
// If the provided name has no file extension, it is treated as a directory by default. If the name starts with "0",
// it is interpreted as a binary file rather than a directory, with the "0" prefix removed.
//
// For directories, the method aggregates the sizes of all objects within the directory and returns the latest modified
// time among them. For files, it returns the file's size and last modified time.
func (f *FileSystem) Stat(name string) (file.FileInfo, error) {
var msg string
st := statusErr
defer f.sendOperationStats(&FileLog{
Operation: "STAT",
Location: getLocation(f.config.BucketName),
Status: &st,
Message: &msg,
}, time.Now())
filetype := typeFile
// Here we assume the user passes "0filePath" in case it wants to get fileinfo about a binary file instead of a directory
if path.Ext(name) == "" {
filetype = typeDirectory
var isBinary bool
name, isBinary = strings.CutPrefix(name, "0")
if isBinary {
filetype = typeFile
}
}
res, err := f.conn.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String(f.config.BucketName),
Prefix: aws.String(name),
})
if err != nil {
f.logger.Errorf("Error returning file info: %v", err)
return nil, err
}
if len(res.Contents) == 0 {
return nil, nil
}
if filetype == typeDirectory {
var size int64
var lastModified time.Time
for i := range res.Contents {
size += *res.Contents[i].Size
if res.Contents[i].LastModified.After(lastModified) {
lastModified = *res.Contents[i].LastModified
}
}
// directory exist and first value gives information about the directory
st = statusSuccess
msg = fmt.Sprintf("Directory with path %q info retrieved successfully", name)
return &S3File{
conn: f.conn,
logger: f.logger,
metrics: f.metrics,
size: size,
contentType: filetype,
name: f.config.BucketName + string(filepath.Separator) + *res.Contents[0].Key,
lastModified: lastModified,
}, nil
}
return &S3File{
conn: f.conn,
logger: f.logger,
metrics: f.metrics,
size: *res.Contents[0].Size,
name: f.config.BucketName + string(filepath.Separator) + *res.Contents[0].Key,
contentType: filetype,
lastModified: *res.Contents[0].LastModified,
}, nil
}
// sendOperationStats logs the FileLog of any file operations performed in S3.
func (f *FileSystem) sendOperationStats(fl *FileLog, startTime time.Time) {
duration := time.Since(startTime).Microseconds()
fl.Duration = duration
f.logger.Debug(fl)
}
================================================
FILE: pkg/gofr/datasource/file/s3/fs_test.go
================================================
package s3
import (
"context"
"errors"
"io"
"os"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
var errMock = errors.New("mocked error")
// testMocks contains all the mock objects needed for tests.
type testMocks struct {
mockS3 *Mocks3Client
mockLogger *MockLogger
mockMetrics *MockMetrics
}
// setupTestMocks creates and returns all mock objects needed for testing.
func setupTestMocks(ctrl *gomock.Controller) *testMocks {
return &testMocks{
mockS3: NewMocks3Client(ctrl),
mockLogger: NewMockLogger(ctrl),
mockMetrics: NewMockMetrics(ctrl),
}
}
// defaultTestConfig returns a default Config for testing.
func defaultTestConfig() *Config {
return &Config{
EndPoint: "https://example.com",
BucketName: "test-bucket",
Region: "us-east-1",
AccessKeyID: "dummy-access-key",
SecretAccessKey: "dummy-secret-key",
}
}
// setupTestFileSystem creates and returns a FileSystem with all required dependencies.
func setupTestFileSystem(mocks *testMocks, config *Config) *FileSystem {
if config == nil {
config = defaultTestConfig()
}
f := S3File{
logger: mocks.mockLogger,
metrics: mocks.mockMetrics,
conn: mocks.mockS3,
}
return &FileSystem{
s3File: f,
conn: mocks.mockS3,
logger: mocks.mockLogger,
config: config,
metrics: mocks.mockMetrics,
}
}
func Test_CreateFile_TxtFile_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil)
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader("test file content")),
ContentLength: aws.Int64(int64(len("test file content"))),
ContentType: aws.String("text/plain"),
LastModified: aws.Time(time.Now()),
}, nil)
_, err := fs.Create("abc.txt")
require.NoError(t, err, "Failed to create file")
}
func Test_CreateFile_WithExistingDirectory_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().
ListObjectsV2(gomock.Any(), gomock.Any()).
Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("abc.txt"),
Size: aws.Int64(1),
},
},
}, nil)
mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil)
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader("test file content")),
ContentLength: aws.Int64(int64(len("test file content"))),
ContentType: aws.String("text/plain"),
LastModified: aws.Time(time.Now()),
}, nil)
_, err := fs.Create("abc/efg.txt")
require.NoError(t, err, "Failed to create file with existing directory")
}
func Test_CreateFile_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().
ListObjectsV2(gomock.Any(), gomock.Any()).
Return(nil, errMock)
_, err := fs.Create("abc/abc.txt")
require.Error(t, err, "Expected error during file creation with invalid path")
}
func Test_OpenFile(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader("mock file content")),
ContentType: aws.String("text/plain"),
LastModified: aws.Time(time.Now()),
ContentLength: aws.Int64(123),
}, nil).AnyTimes()
_, err := fs.OpenFile("abc.json", 0, os.ModePerm)
require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Failed to open file")
}
func Test_MakingDirectories(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().
PutObject(gomock.Any(), gomock.Any()).
Return(&s3.PutObjectOutput{}, nil).Times(3)
err := fs.MkdirAll("abc/bcd/cfg", os.ModePerm)
require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Error creating directory")
}
func Test_RenameDirectory(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
config := defaultTestConfig()
config.BucketName = "mock-bucket"
fs := setupTestFileSystem(mocks, config)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().
ListObjectsV2(gomock.Any(), gomock.Any()).
Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("old-dir/file1.txt"),
},
{
Key: aws.String("old-dir/file2.txt"),
},
},
}, nil).Times(1)
mocks.mockS3.EXPECT().
CopyObject(gomock.Any(), gomock.Any()).
Return(&s3.CopyObjectOutput{}, nil).Times(1)
mocks.mockS3.EXPECT().
CopyObject(gomock.Any(), gomock.Any()).
Return(&s3.CopyObjectOutput{}, nil).Times(1)
mocks.mockS3.EXPECT().
ListObjectsV2(gomock.Any(), gomock.Any()).
Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("old-dir/file1.txt"),
},
{
Key: aws.String("old-dir/file2.txt"),
},
},
}, nil).Times(1)
mocks.mockS3.EXPECT().
DeleteObjects(gomock.Any(), gomock.Any()).
Return(&s3.DeleteObjectsOutput{}, nil).Times(1)
err := fs.Rename("old-dir", "new-dir")
require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Failed to rename directory")
}
type result struct {
Name string
Size int64
IsDir bool
}
func Test_ReadDir(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
tests := []struct {
name string
dirPath string
expectedResults []result
setupMock func()
}{
{
name: "Valid directory path with files and subdirectory",
dirPath: "abc/efg",
expectedResults: []result{
{"file.txt", 1, false},
{"hij", 0, true},
},
setupMock: func() {
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{Key: aws.String("abc/efg/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())},
{Key: aws.String("abc/efg/file.txt"), Size: aws.Int64(1), LastModified: aws.Time(time.Now())},
{Key: aws.String("abc/efg/hij/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())},
},
}, nil)
},
},
{
name: "Valid directory path with only subdirectory",
dirPath: "abc",
expectedResults: []result{
{"efg", 0, true},
},
setupMock: func() {
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{Key: aws.String("abc/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())},
{Key: aws.String("abc/efg/"), Size: aws.Int64(0), LastModified: aws.Time(time.Now())},
},
}, nil)
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
runReadDirTest(t, fs, tt.dirPath, tt.expectedResults, tt.setupMock)
})
}
}
func runReadDirTest(t *testing.T, fs *FileSystem, dirPath string, expectedResults []result, setupMock func()) {
t.Helper()
if setupMock != nil {
setupMock()
}
res, err := fs.ReadDir(dirPath)
require.NoError(t, err, "Error reading directory")
results := make([]result, 0)
for _, entry := range res {
results = append(results, result{entry.Name(), entry.Size(), entry.IsDir()})
}
assert.Equal(t, expectedResults, results, "Mismatch in results for path: %v", dirPath)
}
func TestRemove(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any())
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
name := "testfile.txt"
mocks.mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1)
err := fs.Remove(name)
require.NoError(t, err, "Remove() failed")
}
func Test_RenameFile_ToNewName_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().CopyObject(gomock.Any(), gomock.Any()).Return(&s3.CopyObjectOutput{}, nil).Times(1)
mocks.mockS3.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).Return(&s3.DeleteObjectOutput{}, nil).Times(1)
err := fs.Rename("abcd.json", "abc.json")
require.NoError(t, err, "Unexpected error when renaming file to new name")
}
func Test_RenameFile_ToSameName_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
err := fs.Rename("abcd.json", "abcd.json")
require.NoError(t, err, "Unexpected error when renaming file to same name")
}
func Test_RenameFile_WithDifferentExtension_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
err := fs.Rename("abcd.json", "abcd.txt")
require.Error(t, err, "Expected error when renaming file with different extension")
}
func Test_RenameFile_ToDirectoryPath_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
err := fs.Rename("abcd.json", "abc/abcd.json")
require.Error(t, err, "Expected error when renaming file to directory path")
}
func Test_StatFile(t *testing.T) {
tm := time.Now()
type result struct {
name string
size int64
isDir bool
}
expectedResponse := result{
name: "file.txt",
size: 1,
isDir: false,
}
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("file.txt"),
Size: aws.Int64(1),
LastModified: aws.Time(tm),
},
},
}, nil).AnyTimes()
res, err := fs.Stat("dir1/dir2/file.txt")
response := result{res.Name(), res.Size(), res.IsDir()}
require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Error getting file info")
assert.Equal(t, expectedResponse, response, "Mismatch in results for path: %v", expectedResponse.name)
}
func Test_StatDirectory(t *testing.T) {
type result struct {
name string
size int64
isDir bool
}
expectedResponse := result{
name: "dir2",
size: 1,
isDir: true,
}
ctrl := gomock.NewController(t)
mocks := setupTestMocks(ctrl)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any(), gomock.Any()).AnyTimes()
cfg := &Config{
EndPoint: "http://localhost:4566",
BucketName: "gofr-bucket-2",
Region: "us-east-1",
AccessKeyID: "test",
SecretAccessKey: "test",
}
fs := setupTestFileSystem(mocks, cfg)
mocks.mockS3.EXPECT().ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{
Bucket: aws.String("gofr-bucket-2"),
Prefix: aws.String("dir1/dir2"),
}).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("dir1/dir2/"),
Size: aws.Int64(0),
LastModified: aws.Time(time.Now()),
},
{
Key: aws.String("dir1/dir2/file.txt"),
Size: aws.Int64(1),
LastModified: aws.Time(time.Now()),
},
},
}, nil)
res, err := fs.Stat("dir1/dir2")
response := result{res.Name(), res.Size(), res.IsDir()}
require.NoError(t, err, "TEST[%d] Failed. Desc: %v", 0, "Error getting directory stats")
assert.Equal(t, expectedResponse, response, "Mismatch in results for path: %v", expectedResponse.name)
}
func Test_CreateFile_PutObjectFails_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("folder/"),
Size: aws.Int64(0),
},
},
}, nil)
mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(nil, errMock)
_, err := fs.Create("folder/test.txt")
require.Error(t, err, "Expected error when PutObject fails")
}
func Test_CreateFile_GetObjectFails_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(&s3.ListObjectsV2Output{
Contents: []types.Object{
{
Key: aws.String("folder/"),
Size: aws.Int64(0),
},
},
}, nil)
mocks.mockS3.EXPECT().PutObject(gomock.Any(), gomock.Any()).Return(&s3.PutObjectOutput{}, nil)
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(nil, errMock)
_, err := fs.Create("folder/test.txt")
require.Error(t, err, "Expected error when GetObject fails after PutObject success")
}
func Test_Open_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(&s3.GetObjectOutput{
Body: io.NopCloser(strings.NewReader("test content")),
ContentType: aws.String("application/json"),
LastModified: aws.Time(time.Now()),
ContentLength: aws.Int64(12),
}, nil).Times(1)
_, err := fs.Open("test.json")
require.NoError(t, err, "Unexpected error when opening file")
}
func Test_Open_GetObjectFails_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Errorf("failed to retrieve %q: %v", "missing.json", errMock).Times(1)
mocks.mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
mocks.mockS3.EXPECT().GetObject(gomock.Any(), gomock.Any()).Return(nil, errMock).Times(1)
_, err := fs.Open("missing.json")
require.Error(t, err, "Expected error when GetObject fails")
require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error")
}
func Test_RenameDirectory_ListObjectsV2Fails_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(nil, errMock)
err := fs.Rename("old-dir", "new-dir")
require.Error(t, err, "Expected error when ListObjectsV2 fails in renameDirectory")
require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error")
}
func Test_Stat_ListObjectsV2Fails_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mocks := setupTestMocks(ctrl)
fs := setupTestFileSystem(mocks, nil)
mocks.mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocks.mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocks.mockS3.EXPECT().ListObjectsV2(gomock.Any(), gomock.Any()).Return(nil, errMock)
_, err := fs.Stat("test-file.txt")
require.Error(t, err, "Expected error when ListObjectsV2 fails")
require.Contains(t, err.Error(), "mocked error", "Expected error to contain mocked error")
}
================================================
FILE: pkg/gofr/datasource/file/s3/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/file/s3
go 1.25.0
require (
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/credentials v1.19.6
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
google.golang.org/api v0.270.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/file/s3/go.sum
================================================
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/file/s3/interface.go
================================================
package s3
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// Logger interface is used by s3 package to log information about query execution.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
// s3Client defines the interface that is used for gofr-s3 datasource as well as mocks to
// streamline the testing as well as the implementation process.
type s3Client interface {
ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error)
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error)
CopyObject(ctx context.Context, params *s3.CopyObjectInput, optFns ...func(*s3.Options)) (*s3.CopyObjectOutput, error)
}
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/file/s3/logger.go
================================================
package s3
import (
"fmt"
"io"
"regexp"
"strings"
)
// FileLog handles logging with different levels.
// In DEBUG MODE, this FileLog can be exported into a file while
// running the application or can be logged in the terminal.
type FileLog struct {
Operation string `json:"operation"`
Duration int64 `json:"duration"`
Status *string `json:"status"`
Location string `json:"location,omitempty"`
Message *string `json:"message,omitempty"`
}
var regexpSpaces = regexp.MustCompile(`\s+`)
func clean(query *string) string {
if query == nil {
return ""
}
return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " "))
}
func (fl *FileLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;148m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-10s \u001B[0m %-48s \n",
clean(&fl.Operation), "AWS_S3", fl.Duration, clean(fl.Status), clean(fl.Message))
}
================================================
FILE: pkg/gofr/datasource/file/s3/logger_test.go
================================================
package s3
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileLogPrettyPrint(t *testing.T) {
msg := "File Created successfully"
fileLog := FileLog{
Operation: "Create file",
Duration: 1234,
Location: "/ftp/one",
Message: &msg,
}
expected := "Create file"
expectedMsg := "File Created successfully"
var buf bytes.Buffer
fileLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
assert.Contains(t, buf.String(), expectedMsg)
}
func TestFileLogPrettyPrintWhitespaceHandling(t *testing.T) {
msg := " File creation complete "
fileLog := FileLog{
Operation: " Create file ",
Duration: 5678,
Message: &msg,
}
expected := "Create file"
expectedMsg := "File creation complete"
var buf bytes.Buffer
fileLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
assert.Contains(t, buf.String(), expectedMsg)
}
================================================
FILE: pkg/gofr/datasource/file/s3/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=s3
//
// Package s3 is a generated GoMock package.
package s3
import (
context "context"
reflect "reflect"
s3 "github.com/aws/aws-sdk-go-v2/service/s3"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// Mocks3Client is a mock of s3Client interface.
type Mocks3Client struct {
ctrl *gomock.Controller
recorder *Mocks3ClientMockRecorder
}
// Mocks3ClientMockRecorder is the mock recorder for Mocks3Client.
type Mocks3ClientMockRecorder struct {
mock *Mocks3Client
}
// NewMocks3Client creates a new mock instance.
func NewMocks3Client(ctrl *gomock.Controller) *Mocks3Client {
mock := &Mocks3Client{ctrl: ctrl}
mock.recorder = &Mocks3ClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mocks3Client) EXPECT() *Mocks3ClientMockRecorder {
return m.recorder
}
// CopyObject mocks base method.
func (m *Mocks3Client) CopyObject(ctx context.Context, params *s3.CopyObjectInput, optFns ...func(*s3.Options)) (*s3.CopyObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CopyObject", varargs...)
ret0, _ := ret[0].(*s3.CopyObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CopyObject indicates an expected call of CopyObject.
func (mr *Mocks3ClientMockRecorder) CopyObject(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*Mocks3Client)(nil).CopyObject), varargs...)
}
// DeleteObject mocks base method.
func (m *Mocks3Client) DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteObject", varargs...)
ret0, _ := ret[0].(*s3.DeleteObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteObject indicates an expected call of DeleteObject.
func (mr *Mocks3ClientMockRecorder) DeleteObject(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*Mocks3Client)(nil).DeleteObject), varargs...)
}
// DeleteObjects mocks base method.
func (m *Mocks3Client) DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteObjects", varargs...)
ret0, _ := ret[0].(*s3.DeleteObjectsOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteObjects indicates an expected call of DeleteObjects.
func (mr *Mocks3ClientMockRecorder) DeleteObjects(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjects", reflect.TypeOf((*Mocks3Client)(nil).DeleteObjects), varargs...)
}
// GetObject mocks base method.
func (m *Mocks3Client) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetObject", varargs...)
ret0, _ := ret[0].(*s3.GetObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetObject indicates an expected call of GetObject.
func (mr *Mocks3ClientMockRecorder) GetObject(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*Mocks3Client)(nil).GetObject), varargs...)
}
// ListObjectsV2 mocks base method.
func (m *Mocks3Client) ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ListObjectsV2", varargs...)
ret0, _ := ret[0].(*s3.ListObjectsV2Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListObjectsV2 indicates an expected call of ListObjectsV2.
func (mr *Mocks3ClientMockRecorder) ListObjectsV2(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2", reflect.TypeOf((*Mocks3Client)(nil).ListObjectsV2), varargs...)
}
// PutObject mocks base method.
func (m *Mocks3Client) PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PutObject", varargs...)
ret0, _ := ret[0].(*s3.PutObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutObject indicates an expected call of PutObject.
func (mr *Mocks3ClientMockRecorder) PutObject(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*Mocks3Client)(nil).PutObject), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/file/sftp/file.go
================================================
package sftp
import (
"bufio"
"encoding/json"
"errors"
"os"
"strings"
"time"
"github.com/pkg/sftp"
"gofr.dev/pkg/gofr/datasource/file"
)
var errNotStringPointer = errors.New("input should be a pointer to a string")
type sftpFile struct {
*sftp.File
logger Logger
}
func (f sftpFile) Size() int64 {
stat, err := f.File.Stat()
if err != nil {
f.logger.Errorf("failed to get file size: %v", err)
return 0
}
return stat.Size()
}
func (f sftpFile) ModTime() time.Time {
stat, err := f.File.Stat()
if err != nil {
f.logger.Errorf("failed to get file modification time: %v", err)
return time.Unix(0, 0)
}
return stat.ModTime()
}
func (f sftpFile) IsDir() bool {
stat, err := f.File.Stat()
if err != nil {
f.logger.Errorf("failed to check if file is directory: %v", err)
return false
}
return stat.IsDir()
}
func (f sftpFile) Mode() os.FileMode {
stat, err := f.File.Stat()
if err != nil {
f.logger.Errorf("failed to get file mode: %v", err)
return 0
}
return stat.Mode()
}
func (f sftpFile) Sys() any {
stat, err := f.File.Stat()
if err != nil {
f.logger.Errorf("failed to get file system info: %v", err)
return nil
}
return stat.Sys()
}
type textReader struct {
scanner *bufio.Scanner
logger Logger
}
type jsonReader struct {
decoder *json.Decoder
token json.Token
}
// ReadAll reads either json, csv or text fileSystem, file with multiple rows, objects or single object can be read
// in the same way.
// File format is decided based on the extension
// JSON fileSystem are read in struct, while CSV fileSystem are read in pointer to string.
//
// newCsvFile, _ = fileStore.Open("file.csv")
// reader := newCsvFile.ReadAll()
//
// Reading JSON fileSystem
//
// for reader.Next() {
// var u User
// reader.Scan(&u)
// }
//
// Reading CSV fileSystem
//
// for reader.Next() {
// var content string
// reader.Scan(&u)
// }
func (f sftpFile) ReadAll() (file.RowReader, error) {
if strings.HasSuffix(f.File.Name(), ".json") {
return f.createJSONReader()
}
return f.createTextCSVReader(), nil
}
// Factory method to create the appropriate JSON reader.
func (f sftpFile) createJSONReader() (file.RowReader, error) {
decoder := json.NewDecoder(f.File)
token, err := f.peekJSONToken(decoder)
if err != nil {
f.logger.Errorf("failed to decode JSON token %v", err)
return nil, err
}
if d, ok := token.(json.Delim); ok && d == '[' {
// JSON array
return &jsonReader{decoder: decoder, token: token}, nil
}
// JSON object
return f.createJSONObjectReader()
}
// Peek the first JSON token to determine its type.
func (sftpFile) peekJSONToken(decoder *json.Decoder) (json.Token, error) {
newDecoder := *decoder
token, err := newDecoder.Token()
if err != nil {
return nil, err
}
return token, nil
}
// Create a JSON reader for a JSON object.
func (f sftpFile) createJSONObjectReader() (file.RowReader, error) {
name := f.File.Name()
if err := f.File.Close(); err != nil {
f.logger.Errorf("failed to close JSON file for reading as object %v", err)
return nil, err
}
newFile, err := os.Open(name)
if err != nil {
f.logger.Errorf("failed to open JSON file for reading as object %v", err)
return nil, err
}
decoder := json.NewDecoder(newFile)
return &jsonReader{decoder: decoder}, nil
}
func (f sftpFile) createTextCSVReader() file.RowReader {
return &textReader{
scanner: bufio.NewScanner(f.File),
logger: f.logger,
}
}
// Next checks if there is next json object available otherwise returns false.
func (j jsonReader) Next() bool {
return j.decoder.More()
}
// Scan binds the data to provided struct.
func (j jsonReader) Scan(i any) error {
return j.decoder.Decode(&i)
}
// Next checks if there is data available in next line otherwise returns false.
func (f textReader) Next() bool {
return f.scanner.Scan()
}
// Scan binds the line to provided pointer to string.
func (f textReader) Scan(i any) error {
// Use a type switch to check if the provided interface is a pointer to a string.
switch target := i.(type) {
case *string:
// If the interface is indeed a pointer to a string, assign the text from the scanner to it.
*target = f.scanner.Text()
return nil
default:
// If the interface is not a pointer to a string, return an error.
return errNotStringPointer
}
}
================================================
FILE: pkg/gofr/datasource/file/sftp/fs.go
================================================
package sftp
import (
"fmt"
"os"
"time"
"github.com/pkg/sftp"
"gofr.dev/pkg/gofr/datasource/file"
"golang.org/x/crypto/ssh"
)
const (
statusSuccess = "SUCCESS"
statusError = "ERROR"
)
type FileSystem struct {
logger Logger
metrics Metrics
config Config
client sftpClient
}
type Config struct {
User string
Password string
Host string
Port int
HostKeyCallBack ssh.HostKeyCallback
}
func New(cfg Config) *FileSystem {
return &FileSystem{config: cfg}
}
// UseLogger sets the logger for the FileSystem client.
func (f *FileSystem) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
f.logger = l
}
}
// UseMetrics sets the metrics for the FileSystem client.
func (f *FileSystem) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
f.metrics = m
}
}
// Connect establishes a connection to FileSystem and registers metrics using the provided configuration when the client was Created.
func (f *FileSystem) Connect() {
f.logger.Debugf("connecting to SFTP server with host `%v` and port `%v`", f.config.Host, f.config.Port)
addr := fmt.Sprintf("%s:%d", f.config.Host, f.config.Port)
config := &ssh.ClientConfig{
User: f.config.User,
Auth: []ssh.AuthMethod{ssh.Password(f.config.Password)},
HostKeyCallback: f.config.HostKeyCallBack,
}
conn, err := ssh.Dial("tcp", addr, config)
if err != nil {
f.logger.Errorf("failed to connect with sftp with err %v", err)
return
}
client, err := sftp.NewClient(conn)
if err != nil {
f.logger.Errorf("failed to create sftp client with err %v", err)
return
}
f.client = client
f.logger.Logf("connected to SFTP client successfully")
}
func (f *FileSystem) Create(name string) (file.File, error) {
status := statusError
defer f.sendOperationStats(&FileLog{
Operation: "CREATE",
Location: name,
Status: &status,
}, time.Now())
newFile, err := f.client.Create(name)
if err != nil {
return nil, err
}
status = statusSuccess
return sftpFile{
File: newFile,
logger: f.logger,
}, nil
}
func (f *FileSystem) Mkdir(name string, _ os.FileMode) error {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "MKDIR", Location: name, Status: &status}, time.Now())
err := f.client.Mkdir(name)
if err != nil {
status = statusError
return err
}
return nil
}
func (f *FileSystem) MkdirAll(path string, _ os.FileMode) error {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "MKDIR", Location: path, Status: &status}, time.Now())
err := f.client.MkdirAll(path)
if err != nil {
status = statusError
return err
}
return nil
}
func (f *FileSystem) Open(name string) (file.File, error) {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "OPEN", Location: name, Status: &status}, time.Now())
openedFile, err := f.client.Open(name)
if err != nil {
status = statusError
return nil, err
}
return sftpFile{
File: openedFile,
logger: f.logger,
}, nil
}
func (f *FileSystem) OpenFile(name string, flag int, _ os.FileMode) (file.File, error) {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "OPENFILE", Location: name, Status: &status}, time.Now())
openedFile, err := f.client.OpenFile(name, flag)
if err != nil {
status = statusSuccess
return nil, err
}
return sftpFile{
File: openedFile,
logger: f.logger,
}, nil
}
func (f *FileSystem) Remove(name string) error {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "REMOVE", Location: name, Status: &status}, time.Now())
err := f.client.Remove(name)
if err != nil {
status = statusError
return err
}
return nil
}
func (f *FileSystem) RemoveAll(path string) error {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "REMOVEALL", Location: path, Status: &status}, time.Now())
err := f.client.RemoveAll(path)
if err != nil {
status = statusError
return err
}
return nil
}
func (f *FileSystem) Rename(oldname, newname string) error {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "RENAME", Location: fmt.Sprintf("%v to %v", oldname, newname),
Status: &status}, time.Now())
err := f.client.Rename(oldname, newname)
if err != nil {
status = statusError
return err
}
return nil
}
func (f *FileSystem) ReadDir(dir string) ([]file.FileInfo, error) {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "READDIR", Location: dir, Status: &status}, time.Now())
dirs, err := f.client.ReadDir(dir)
if err != nil {
status = statusError
return nil, err
}
newDirs := make([]file.FileInfo, 0, len(dirs))
for _, v := range dirs {
newDirs = append(newDirs, v)
}
return newDirs, nil
}
func (f *FileSystem) Stat(name string) (file.FileInfo, error) {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "STAT", Location: name, Status: &status}, time.Now())
fileInfo, err := f.client.Stat(name)
if err != nil {
status = statusError
return nil, err
}
return fileInfo, nil
}
func (f *FileSystem) ChDir(_ string) error {
f.logger.Errorf("Chdir is not implemented for SFTP")
return nil
}
func (f *FileSystem) Getwd() (string, error) {
status := statusSuccess
defer f.sendOperationStats(&FileLog{Operation: "STAT", Location: "", Status: &status}, time.Now())
name, err := f.client.Getwd()
if err != nil {
status = statusError
return "", err
}
return name, err
}
// TODO : Implement metrics.
func (f *FileSystem) sendOperationStats(fl *FileLog, startTime time.Time) {
duration := time.Since(startTime).Microseconds()
fl.Duration = duration
f.logger.Debug(fl)
}
================================================
FILE: pkg/gofr/datasource/file/sftp/fs_test.go
================================================
package sftp
import (
"errors"
"os"
"testing"
"github.com/pkg/sftp"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
File "gofr.dev/pkg/gofr/datasource/file"
)
type mocks struct {
client *MocksftpClient
logger *MockLogger
metrics *MockMetrics
file *File.MockFile
}
var (
errCreateFile = errors.New("failed to create file")
errOpenFile = errors.New("failed to open file")
)
func getMocks(t *testing.T) (FileSystem, mocks) {
t.Helper()
ctrl := gomock.NewController(t)
mockClient := NewMocksftpClient(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockFile := File.NewMockFile(ctrl)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
client := FileSystem{logger: mockLogger, metrics: mockMetrics, client: mockClient}
return client, mocks{
client: mockClient,
logger: mockLogger,
metrics: mockMetrics,
file: mockFile,
}
}
func TestFiles_Mkdir(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
err error
}{
{"directory created successfully", nil},
{"directory creation failed", errCreateFile},
}
for _, tc := range testCases {
mocks.client.EXPECT().Mkdir("/test").Return(tc.err)
err := files.Mkdir("/test", 1)
require.Equal(t, tc.err, err, "TEST[%d] Failed. Desc %v")
}
}
func TestFiles_MkdirAll(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
err error
}{
{"directory created successfully", nil},
{"directory creation failed", errCreateFile},
}
for _, tc := range testCases {
mocks.client.EXPECT().MkdirAll("/test").Return(tc.err)
err := files.MkdirAll("/test", 1)
require.Equal(t, tc.err, err, "TEST[%d] Failed. Desc %v")
}
}
func TestFiles_Remove(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
err error
}{
{"directory removed successfully", nil},
{"directory removal failed", errCreateFile},
}
for _, tc := range testCases {
mocks.client.EXPECT().Remove("/test").Return(tc.err)
err := files.Remove("/test")
require.Equal(t, tc.err, err, "TEST[%d] Failed. Desc %v")
}
}
func TestFiles_RemoveAll(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
err error
}{
{"directory removed successfully", nil},
{"directory removal failed", errCreateFile},
}
for _, tc := range testCases {
mocks.client.EXPECT().RemoveAll("/test/upload").Return(tc.err)
err := files.RemoveAll("/test/upload")
require.Equal(t, tc.err, err, "TEST[%d] Failed. Desc %v")
}
}
func TestFiles_Rename(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
err error
}{
{"directory renamed successfully", nil},
{"directory rename failed", errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().Rename("test.csv", "new_test.csv").Return(tc.err)
err := files.Rename("test.csv", "new_test.csv")
require.Equal(t, tc.err, err, "TEST[%d] Failed. Desc %v", i, tc.desc)
}
}
func TestFiles_ChDir(t *testing.T) {
files, mocks := getMocks(t)
mocks.logger.EXPECT().Errorf("Chdir is not implemented for SFTP")
err := files.ChDir("test.csv")
require.NoError(t, err, "TEST[%d] Failed. Desc %v")
}
func TestFiles_GetWd(t *testing.T) {
files, mocks := getMocks(t)
testCases := []struct {
desc string
name string
err error
}{
{"directory renamed successfully", "file", nil},
{"directory rename failed", "", errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().Getwd().Return(tc.name, tc.err)
name, err := files.Getwd()
require.Equal(t, tc.name, name, "TEST[%d] Failed. Desc %v", i, tc.desc)
require.Equal(t, tc.err, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
func TestFiles_Create(t *testing.T) {
client, mocks := getMocks(t)
mockSftpFile := sftp.File{}
testCases := []struct {
desc string
name string
expFile File.File
expError error
}{
{"File Created Successfully", "text.csv", sftpFile{File: &mockSftpFile, logger: mocks.logger}, nil},
{"File Creation Failed", "text.csv", nil, errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().Create(tc.name).Return(&mockSftpFile, tc.expError)
createdFile, err := client.Create(tc.name)
require.Equal(t, tc.expFile, createdFile, "Test[%d] Failed.\n DESC %v", i, tc.desc)
require.Equal(t, tc.expError, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
func TestFiles_Open(t *testing.T) {
client, mocks := getMocks(t)
mockSftpFile := sftp.File{}
testCases := []struct {
desc string
name string
expFile File.File
expError error
}{
{"File Opened Successfully", "text.csv", sftpFile{File: &mockSftpFile, logger: mocks.logger}, nil},
{"File Open Failed", "text.csv", nil, errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().Open(tc.name).Return(&mockSftpFile, tc.expError)
openedFile, err := client.Open(tc.name)
require.Equal(t, tc.expFile, openedFile, "Test[%d] Failed.\n DESC %v", i, tc.desc)
require.Equal(t, tc.expError, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
func TestFiles_OpenFile(t *testing.T) {
client, mocks := getMocks(t)
mockSftpFile := sftp.File{}
testCases := []struct {
desc string
name string
expFile File.File
expError error
}{
{"File Opened Successfully", "text.csv", sftpFile{File: &mockSftpFile, logger: mocks.logger}, nil},
{"File Open Failed", "text.csv", nil, errOpenFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().OpenFile(tc.name, 0).Return(&mockSftpFile, tc.expError)
openedFile, err := client.OpenFile(tc.name, 0, 0)
require.Equal(t, tc.expFile, openedFile, "Test[%d] Failed.\n DESC %v", i, tc.desc)
require.Equal(t, tc.expError, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
func TestFiles_ReadDir(t *testing.T) {
client, mocks := getMocks(t)
file, _ := os.CreateTemp("temp", "t")
file.Close()
info, _ := file.Stat()
osFile := []os.FileInfo{info}
testCases := []struct {
desc string
name string
expFile []File.FileInfo
expError error
}{
{"Dir Read Successfully", "text.csv", []File.FileInfo{info}, nil},
{"Dir Read Failed", "text.csv", nil, errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().ReadDir(tc.name).Return(osFile, tc.expError)
createdFile, err := client.ReadDir(tc.name)
require.Equal(t, tc.expFile, createdFile, "Test[%d] Failed.\n DESC %v", i, tc.desc)
require.Equal(t, tc.expError, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
func TestFiles_Stat(t *testing.T) {
client, mocks := getMocks(t)
file, _ := os.CreateTemp("temp", "t")
file.Close()
info, _ := file.Stat()
testCases := []struct {
desc string
name string
expFile File.FileInfo
expError error
}{
{"File Stat Successfully Returned", "text.csv", info, nil},
{"File Stat Fetch Failed", "text.csv", nil, errCreateFile},
}
for i, tc := range testCases {
mocks.client.EXPECT().Stat(tc.name).Return(info, tc.expError)
createdFile, err := client.Stat(tc.name)
require.Equal(t, tc.expFile, createdFile, "Test[%d] Failed.\n DESC %v", i, tc.desc)
require.Equal(t, tc.expError, err, "Test[%d] Failed.\n DESC %v", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/datasource/file/sftp/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/file/sftp
go 1.25.0
require (
github.com/pkg/sftp v1.13.10
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
golang.org/x/crypto v0.48.0
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
golang.org/x/sys v0.41.0 // indirect
google.golang.org/api v0.270.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/file/sftp/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/file/sftp/interface.go
================================================
package sftp
import (
"context"
"os"
"github.com/pkg/sftp"
)
// Logger interface is used by ftp package to log information about query execution.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
type sftpClient interface {
Create(path string) (*sftp.File, error)
Mkdir(path string) error
MkdirAll(path string) error
Open(path string) (*sftp.File, error)
OpenFile(path string, f int) (*sftp.File, error)
Remove(path string) error
RemoveAll(path string) error
Rename(oldname, newname string) error
ReadDir(p string) ([]os.FileInfo, error)
Stat(p string) (os.FileInfo, error)
Getwd() (string, error)
}
================================================
FILE: pkg/gofr/datasource/file/sftp/logger.go
================================================
package sftp
import (
"fmt"
"io"
"regexp"
"strings"
)
// FileLog handles logging with different levels.
type FileLog struct {
Operation string `json:"operation"`
Duration int64 `json:"duration"`
Status *string `json:"status"`
Location string `json:"location,omitempty"`
}
var regexpSpaces = regexp.MustCompile(`\s+`)
func clean(query *string) string {
if query == nil {
return ""
}
return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " "))
}
func (fl *FileLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;148m%-6s\u001B[0m %8d\u001B[38;5;8mµs \u001B[0m %-48s \n",
clean(&fl.Operation)+" "+clean(fl.Status), "SFTP", fl.Duration, fl.Location)
}
================================================
FILE: pkg/gofr/datasource/file/sftp/logger_test.go
================================================
package sftp
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFileLogPrettyPrint(t *testing.T) {
fileLog := FileLog{
Operation: "Create file",
Duration: 1234,
Location: "/ftp/one",
}
expected := "Create file"
var buf bytes.Buffer
fileLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
func TestFileLogPrettyPrintWhitespaceHandling(t *testing.T) {
fileLog := FileLog{
Operation: " Create file ",
Duration: 5678,
}
expectedMsg := "Create file"
var buf bytes.Buffer
fileLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expectedMsg)
}
================================================
FILE: pkg/gofr/datasource/file/sftp/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=sftp
//
// Package sftp is a generated GoMock package.
package sftp
import (
context "context"
os "os"
reflect "reflect"
sftp "github.com/pkg/sftp"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// MocksftpClient is a mock of sftpClient interface.
type MocksftpClient struct {
ctrl *gomock.Controller
recorder *MocksftpClientMockRecorder
}
// MocksftpClientMockRecorder is the mock recorder for MocksftpClient.
type MocksftpClientMockRecorder struct {
mock *MocksftpClient
}
// NewMocksftpClient creates a new mock instance.
func NewMocksftpClient(ctrl *gomock.Controller) *MocksftpClient {
mock := &MocksftpClient{ctrl: ctrl}
mock.recorder = &MocksftpClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MocksftpClient) EXPECT() *MocksftpClientMockRecorder {
return m.recorder
}
// Create mocks base method.
func (m *MocksftpClient) Create(path string) (*sftp.File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", path)
ret0, _ := ret[0].(*sftp.File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MocksftpClientMockRecorder) Create(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MocksftpClient)(nil).Create), path)
}
// Getwd mocks base method.
func (m *MocksftpClient) Getwd() (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Getwd")
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Getwd indicates an expected call of Getwd.
func (mr *MocksftpClientMockRecorder) Getwd() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Getwd", reflect.TypeOf((*MocksftpClient)(nil).Getwd))
}
// Mkdir mocks base method.
func (m *MocksftpClient) Mkdir(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Mkdir", path)
ret0, _ := ret[0].(error)
return ret0
}
// Mkdir indicates an expected call of Mkdir.
func (mr *MocksftpClientMockRecorder) Mkdir(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Mkdir", reflect.TypeOf((*MocksftpClient)(nil).Mkdir), path)
}
// MkdirAll mocks base method.
func (m *MocksftpClient) MkdirAll(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MkdirAll", path)
ret0, _ := ret[0].(error)
return ret0
}
// MkdirAll indicates an expected call of MkdirAll.
func (mr *MocksftpClientMockRecorder) MkdirAll(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MkdirAll", reflect.TypeOf((*MocksftpClient)(nil).MkdirAll), path)
}
// Open mocks base method.
func (m *MocksftpClient) Open(path string) (*sftp.File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Open", path)
ret0, _ := ret[0].(*sftp.File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Open indicates an expected call of Open.
func (mr *MocksftpClientMockRecorder) Open(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Open", reflect.TypeOf((*MocksftpClient)(nil).Open), path)
}
// OpenFile mocks base method.
func (m *MocksftpClient) OpenFile(path string, f int) (*sftp.File, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OpenFile", path, f)
ret0, _ := ret[0].(*sftp.File)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OpenFile indicates an expected call of OpenFile.
func (mr *MocksftpClientMockRecorder) OpenFile(path, f any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenFile", reflect.TypeOf((*MocksftpClient)(nil).OpenFile), path, f)
}
// ReadDir mocks base method.
func (m *MocksftpClient) ReadDir(p string) ([]os.FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadDir", p)
ret0, _ := ret[0].([]os.FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadDir indicates an expected call of ReadDir.
func (mr *MocksftpClientMockRecorder) ReadDir(p any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadDir", reflect.TypeOf((*MocksftpClient)(nil).ReadDir), p)
}
// Remove mocks base method.
func (m *MocksftpClient) Remove(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", path)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove.
func (mr *MocksftpClientMockRecorder) Remove(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MocksftpClient)(nil).Remove), path)
}
// RemoveAll mocks base method.
func (m *MocksftpClient) RemoveAll(path string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveAll", path)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveAll indicates an expected call of RemoveAll.
func (mr *MocksftpClientMockRecorder) RemoveAll(path any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveAll", reflect.TypeOf((*MocksftpClient)(nil).RemoveAll), path)
}
// Rename mocks base method.
func (m *MocksftpClient) Rename(oldname, newname string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", oldname, newname)
ret0, _ := ret[0].(error)
return ret0
}
// Rename indicates an expected call of Rename.
func (mr *MocksftpClientMockRecorder) Rename(oldname, newname any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MocksftpClient)(nil).Rename), oldname, newname)
}
// Stat mocks base method.
func (m *MocksftpClient) Stat(p string) (os.FileInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stat", p)
ret0, _ := ret[0].(os.FileInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stat indicates an expected call of Stat.
func (mr *MocksftpClientMockRecorder) Stat(p any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stat", reflect.TypeOf((*MocksftpClient)(nil).Stat), p)
}
================================================
FILE: pkg/gofr/datasource/health.go
================================================
package datasource
const (
StatusUp = "UP"
StatusDown = "DOWN"
)
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
================================================
FILE: pkg/gofr/datasource/influxdb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/influxdb
go 1.24.2
require (
github.com/golang/mock v1.6.0
github.com/influxdata/influxdb-client-go/v2 v2.14.0
github.com/stretchr/testify v1.11.1
go.opencensus.io v0.24.0
go.opentelemetry.io/otel v1.41.0
)
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 // indirect
github.com/oapi-codegen/runtime v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/net v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/influxdb/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4=
github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo=
github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A=
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
================================================
FILE: pkg/gofr/datasource/influxdb/influxdb.go
================================================
package influxdb
import (
"context"
"errors"
"strings"
"time"
influxdb "github.com/influxdata/influxdb-client-go/v2"
"go.opencensus.io/trace"
)
// Config holds the configuration for connecting to InfluxDB.
type Config struct {
URL string
Token string
Username string
Password string
}
type influx struct {
client client
organization organization
bucket bucket
query query
}
// Client represents the InfluxDB client.
type Client struct {
influx influx
config Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
type HealthInflux struct {
URL string
Token string
Username string
Password string
}
const (
statusDown = "DOWN"
statusUp = "UP"
)
var (
errEmptyOrganizationName = errors.New("organization name must not be empty")
errEmptyOrganizationID = errors.New("organization id must not be empty")
errEmptyBucketID = errors.New("bucket id must not be empty")
errEmptyBucketName = errors.New("bucket name must not be empty")
errFetchOrganization = errors.New("failed to fetch all organizations")
errHealthCheckFailed = errors.New("influxdb health check failed")
)
// CreateOrganization creates a new organization in InfluxDB with the specified name.
// It implements the container.InfluxDBProvider interface.
//
// Parameters:
// - ctx: Context for request cancellation and timeouts.
// - orgName: The name of the organization to be created. Must not be empty.
//
// Returns:
// - string: The ID of the newly created organization.
// - error: Error if organization creation fails or if orgName is empty.
func (c *Client) CreateOrganization(ctx context.Context, orgName string) (string, error) {
if orgName == "" {
return "", errEmptyOrganizationName
}
tracedCtx, span := c.addTrace(ctx, "create-organization", "")
start := time.Now()
defer c.sendOperationStats(start, "CreateOrganization", "create-organization", span, orgName)
newOrg, err := c.influx.organization.CreateOrganizationWithName(tracedCtx, orgName)
if err != nil {
c.logger.Errorf("failed to create new organization with name '%v' %v", orgName, err)
return "", err
}
return *newOrg.Id, nil
}
// DeleteOrganization deletes an organization in InfluxDB using its ID.
// It implements the container.InfluxDBProvider interface.
//
// Parameters:
// - ctx: Context for request cancellation and timeouts.
// - orgID: The ID of the organization to be deleted. Must not be empty.
//
// Returns:
// - err: Error if the organization deletion fails or if orgID is empty.
func (c *Client) DeleteOrganization(ctx context.Context, orgID string) error {
if orgID == "" {
return errEmptyOrganizationID
}
tracedCtx, span := c.addTrace(ctx, "delete-organization", "")
start := time.Now()
defer c.sendOperationStats(start, "DeleteOrganization", "delete-organization", span, orgID)
err := c.influx.organization.DeleteOrganizationWithID(tracedCtx, orgID)
if err != nil {
return err
}
return nil
}
// ListOrganization retrieves all organizations from InfluxDB and returns their IDs and names.
// It implements the container.InfluxDBProvider interface.
//
// Parameters:
// - ctx: Context for request cancellation and timeouts.
//
// Returns:
// - orgs: A map of organization IDs to their corresponding names.
// - err: Error if the API call fails or the organizations cannot be retrieved.
func (c *Client) ListOrganization(ctx context.Context) (map[string]string, error) {
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "list-organizations", "")
defer c.sendOperationStats(start, "ListOrganization", "list-organizations", span)
allOrg, err := c.influx.organization.GetOrganizations(tracedCtx)
if err != nil {
return nil, errFetchOrganization
}
if allOrg == nil || len(*allOrg) == 0 {
return map[string]string{}, nil
}
orgs := make(map[string]string, len(*allOrg))
for _, org := range *allOrg {
if org.Id != nil {
orgs[*org.Id] = org.Name
}
}
return orgs, nil
}
// CreateBucket creates a new bucket in InfluxDB for the specified organization.
// Parameters:
// - ctx: Context for request cancellation and timeouts.
// - orgID: The ID of the organization in which the bucket will be created.
// - bucketName: The name of the bucket to be created.
//
// Returns:
// - bucketID: The ID of the newly created bucket.
// - err: Error if bucket creation fails.
func (c *Client) CreateBucket(ctx context.Context, orgID, bucketName string) (bucketID string, err error) {
if orgID == "" {
return "", errEmptyOrganizationID
}
if bucketName == "" {
return "", errEmptyBucketName
}
tracedCtx, span := c.addTrace(ctx, "create-bucket", "")
start := time.Now()
defer c.sendOperationStats(start, "CreateBucket", "create-bucket", span, orgID, bucketName)
newBucket, err := c.influx.bucket.CreateBucketWithNameWithID(tracedCtx, orgID, bucketName)
if err != nil {
return "", err
}
return *newBucket.Id, nil
}
// DeleteBucket deletes a bucket from InfluxDB by its ID.
// Parameters:
// - ctx: Context for request cancellation and timeouts.
// - org: The ID or name of the organization (not used directly in this implementation).
// - bucketID: The ID of the bucket to be deleted. Must not be empty.
//
// Returns:
// - err: Error if the bucket deletion fails or if bucketID is empty.
func (c *Client) DeleteBucket(ctx context.Context, bucketID string) error {
if bucketID == "" {
return errEmptyBucketID
}
tracedCtx, span := c.addTrace(ctx, "delete-bucket", "")
start := time.Now()
defer c.sendOperationStats(start, "DeleteBucket", "delete-bucket", span, bucketID)
if err := c.influx.bucket.DeleteBucketWithID(tracedCtx, bucketID); err != nil {
return err
}
return nil
}
type Health struct {
Status string `json:"status"` // "UP" or "DOWN"
Details map[string]any `json:"details,omitempty"` // extra metadata
}
// HealthCheck retrieves the health status of the InfluxDB instance.
// It implements the container.InfluxDBProvider interface.
//
// Parameters:
// - ctx: Context for request cancellation and timeouts.
//
// Returns:
// - any: A datasource.Health object containing the status and details of the InfluxDB service.
// - err: Error if the health check request fails or the InfluxDB client returns an error.
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "health-check", "")
defer c.sendOperationStats(start, "HealthCheck", "health-check", span)
h := Health{Details: make(map[string]any)}
h.Details["Username"] = c.config.Username
h.Details["Url"] = c.config.URL
health, err := c.influx.client.Health(tracedCtx)
if err != nil {
h.Status = statusDown
h.Details["error"] = err.Error()
return &h, errHealthCheckFailed
}
h.Status = statusUp
h.Details["Name"] = health.Name
h.Details["Commit"] = health.Commit
h.Details["Version"] = health.Version
h.Details["Message"] = health.Message
h.Details["Checks"] = health.Checks
h.Details["Status"] = health.Status
return h, nil
}
/*
ListBuckets retrieves the names of all buckets for a given organization from InfluxDB.
It implements the container.InfluxDBProvider interface.
Parameters:
- ctx: Context for cancellation and deadlines.
- org: Organization name (must not be empty).
Returns:
- A slice of bucket names.
- An error, if any occurred during retrieval.
*/
func (c *Client) ListBuckets(ctx context.Context, org string) (buckets map[string]string, err error) {
// Validate input
if org == "" {
return nil, errEmptyOrganizationName
}
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "list-buckets", "")
defer c.sendOperationStats(start, "ListBuckets", "list-buckets", span, org)
bucketsDomain, err := c.influx.bucket.FindBucketsByOrgName(tracedCtx, org)
if err != nil {
return nil, err
}
buckets = make(map[string]string) // Initialize the map
for _, bucket := range *bucketsDomain {
if bucket.Name != "" {
buckets[*bucket.Id] = bucket.Name
}
}
return buckets, nil
}
// Ping pings the InfluxDB server to check its availability.
// It implements the container.InfluxDBProvider interface.
//
// Parameters:
// - ctx: Context for request cancellation and timeouts.
//
// Returns:
// - bool: True if the InfluxDB server is reachable; false otherwise.
// - err: Error if the ping request fails.
func (c *Client) Ping(ctx context.Context) (bool, error) {
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "ping", "")
defer c.sendOperationStats(start, "Ping", "ping", span)
ping, err := c.influx.client.Ping(tracedCtx)
if err != nil {
c.logger.Errorf("%v", err)
return false, err
}
return ping, nil
}
func (c *Client) Query(ctx context.Context, org, fluxQuery string) ([]map[string]any, error) {
tracedCtx, span := c.addTrace(ctx, "query", fluxQuery)
start := time.Now()
defer c.sendOperationStats(start, "Query", fluxQuery, span, org)
queryAPI := c.influx.client.QueryAPI(org)
result, err := queryAPI.Query(tracedCtx, fluxQuery)
if err != nil {
c.logger.Errorf("InfluxDB Flux Query '%v' failed: %v", fluxQuery, err.Error())
return nil, err
}
var records []map[string]any
for result.Next() {
if result.Err() != nil {
c.logger.Errorf("Error processing InfluxDB Flux Query result: %v", result.Err().Error())
return nil, result.Err()
}
record := make(map[string]any)
for k, v := range result.Record().Values() {
record[k] = v
}
records = append(records, record)
}
if result.Err() != nil {
c.logger.Errorf("Final error in InfluxDB Flux Query result: %v", result.Err().Error())
return nil, result.Err()
}
return records, nil
}
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for InfluxDB client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
func (c *Client) WritePoint(ctx context.Context,
org, bucket, measurement string,
tags map[string]string, fields map[string]any, timestamp time.Time,
) error {
start := time.Now()
tracedCtx, span := c.addTrace(ctx, "write-point", "")
defer c.sendOperationStats(start, "WritePoint", "write-point", span, org, bucket, measurement)
p := influxdb.NewPoint(measurement, tags, fields, timestamp)
writeAPI := c.influx.client.WriteAPIBlocking(org, bucket)
if err := writeAPI.WritePoint(tracedCtx, p); err != nil {
c.logger.Errorf("Failed to write point to influxdb: %v", err.Error())
return err
}
return nil
}
// New creates a new InfluxDB client with the provided configuration.
func New(config Config) *Client {
return &Client{
config: config,
}
}
// Connect initializes a new InfluxDB client using the configured URL and authentication token.
// It logs the connection status and performs a health check to verify connectivity.
//
// If the health check fails, it logs an error and exits early without returning an error.
// No parameters or return values.
func (c *Client) Connect() {
c.logger.Logf("connecting to influxdb at %v", c.config.URL)
// Create a new client using an InfluxDB server base URL and an authentication token
c.influx.client = influxdb.NewClient(
c.config.URL,
c.config.Token,
)
c.influx.organization = NewInfluxdbOrganizationAPI(c.influx.client.OrganizationsAPI())
c.influx.bucket = NewInfluxdbBucketAPI(c.influx.client.BucketsAPI())
c.influx.query = c.influx.client.QueryAPI("")
if c.metrics != nil {
influxBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram(
"app_influxdb_stats",
"Response time of InfluxDB operations in milliseconds.",
influxBuckets...,
)
}
if _, err := c.HealthCheck(context.Background()); err != nil {
c.logger.Errorf("InfluxDB health check failed: %v", err.Error())
return
}
c.logger.Logf("connected to influxdb at : %v", c.config.URL)
}
func (c *Client) addTrace(ctx context.Context, method, query string) (context.Context, *trace.Span) {
if c.tracer != nil {
ctxWithSpan, span := trace.StartSpan(ctx, "influxdb-"+method)
if query != "" {
span.AddAttributes(trace.StringAttribute("influxdb.query", query))
}
return ctxWithSpan, span
}
return ctx, nil
}
// sendOperationStats logs duration, ends span, records histogram.
func (c *Client) sendOperationStats(start time.Time, methodType, query string, span *trace.Span, args ...any) {
durationMS := time.Since(start).Milliseconds()
c.logger.Debug(&QueryLog{
Operation: methodType,
Query: query,
Duration: durationMS,
Args: args,
})
if span != nil {
span.End()
}
if c.metrics != nil {
opType := getOperationType(query)
if opType == "" {
opType = methodType // fallback for non-query operations
}
c.metrics.RecordHistogram(
context.Background(),
"app_influxdb_stats",
float64(durationMS),
"url", c.config.URL,
"type", opType,
)
}
}
// getOperationType extracts the first token (SELECT-like for Flux or blank).
func getOperationType(q string) string {
q = strings.TrimSpace(q)
if q == "" {
return ""
}
parts := strings.Fields(q)
return strings.ToUpper(parts[0])
}
================================================
FILE: pkg/gofr/datasource/influxdb/influxdb_test.go
================================================
package influxdb
import (
"errors"
"testing"
gomock "github.com/golang/mock/gomock"
"github.com/influxdata/influxdb-client-go/v2/domain"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
)
var (
errInvalidOrgID = errors.New("invalid organization id")
errFailedCreatingOrg = errors.New("failed to create new organization")
errPingFailed = errors.New("failed to ping")
errFailedQuery = errors.New("error failed query")
)
func setupDB(t *testing.T, ctrl *gomock.Controller) *Client {
t.Helper()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
config := Config{
URL: "http://localhost:8086",
Username: "username",
Password: "password",
Token: "token",
}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-influxdb"))
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Replace the client with our mocked version
client.influx.client = NewMockclient(ctrl)
return client
}
func Test_HealthCheckSuccess(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockInflux := client.influx.client.(*Mockclient)
expectedHealth := &domain.HealthCheck{Status: "pass"}
mockInflux.EXPECT().
Health(gomock.Any()).
Return(expectedHealth, nil).
Times(1)
_, err := client.HealthCheck(t.Context())
require.NoError(t, err)
}
func Test_HealthCheckFail(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockInflux := client.influx.client.(*Mockclient)
expectedHealth := &domain.HealthCheck{Status: "fail"}
mockInflux.EXPECT().
Health(gomock.Any()).
Return(expectedHealth, errEmptyBucketID).
Times(1)
_, err := client.HealthCheck(t.Context())
require.Error(t, err)
}
func Test_PingSuccess(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockInflux := client.influx.client.(*Mockclient)
mockInflux.EXPECT().
Ping(gomock.Any()).
Return(true, nil).
Times(1)
health, err := client.Ping(t.Context())
require.NoError(t, err) // empty organization name
require.True(t, health)
}
func Test_PingFailed(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockInflux := client.influx.client.(*Mockclient)
mockInflux.EXPECT().
Ping(gomock.Any()).
Return(false, errPingFailed).
Times(1)
health, err := client.Ping(t.Context())
require.Error(t, err) // empty organization name
require.False(t, health)
}
func Test_CreateOrganization(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dummyID := "dummyID"
testCases := []struct {
name string
orgName string
resp *domain.Organization
expectErr bool
err error
}{
{
name: "empty organizations name",
orgName: "",
resp: &domain.Organization{},
expectErr: true,
err: errEmptyOrganizationName,
},
{
name: "create new organization",
orgName: "testOrg",
resp: &domain.Organization{
Id: &dummyID,
},
expectErr: false,
err: nil,
},
{
name: "create duplicate organization",
orgName: "testOrg",
resp: &domain.Organization{},
expectErr: true,
err: errFailedCreatingOrg,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
client := *setupDB(t, ctrl)
mockOrganization := NewMockorganization(ctrl)
client.influx.organization = mockOrganization
mockOrganization.EXPECT().
CreateOrganizationWithName(gomock.Any(), tt.orgName).
Return(tt.resp, tt.err).
AnyTimes()
newOrgID, err := client.CreateOrganization(t.Context(), tt.orgName)
if tt.expectErr {
require.Error(t, err)
require.Equal(t, err, tt.err)
require.Empty(t, newOrgID)
} else {
require.NoError(t, err)
}
})
}
}
func Test_DeleteOrganization(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dummyID := "dummyID"
testCases := []struct {
name string
orgID string
expectErr bool
err error
}{
{
name: "delete empty organizations with id",
orgID: "",
expectErr: true,
err: errEmptyOrganizationID,
},
{
name: "delete organization with id",
orgID: dummyID,
expectErr: false,
err: nil,
},
{
name: "delete invalid organization with id",
orgID: dummyID,
expectErr: true,
err: errInvalidOrgID,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
client := *setupDB(t, ctrl)
mockOrganization := NewMockorganization(ctrl)
client.influx.organization = mockOrganization
mockOrganization.EXPECT().
DeleteOrganizationWithID(gomock.Any(), tt.orgID).
Return(tt.err).
AnyTimes()
err := client.DeleteOrganization(t.Context(), tt.orgID)
if tt.expectErr {
require.Error(t, err)
require.Equal(t, err, tt.err)
} else {
require.NoError(t, err)
}
})
}
}
func Test_ListOrganization(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockOrganization := NewMockorganization(ctrl)
client.influx.organization = mockOrganization
t.Run("test zero organization", func(t *testing.T) {
allOrgs := []domain.Organization{}
mockOrganization.EXPECT().
GetOrganizations(gomock.Any()).
Return(&allOrgs, nil).
Times(1)
orgs, err := client.ListOrganization(t.Context())
require.NoError(t, err)
require.Empty(t, orgs)
})
t.Run("testing error in fetching organization", func(t *testing.T) {
allOrgs := &[]domain.Organization{}
mockOrganization.EXPECT().
GetOrganizations(gomock.Any()).
Return(allOrgs, errFetchOrganization).
Times(1)
orgs, err := client.ListOrganization(t.Context())
require.Empty(t, orgs)
require.Error(t, err)
require.Equal(t, err, errFetchOrganization)
})
t.Run("testing fetching list of organization", func(t *testing.T) {
id1, name1 := "id1", "name1"
id2, name2 := "id1", "name1"
allOrg := &[]domain.Organization{
{Id: &id1, Name: name1},
{Id: &id2, Name: name2},
}
wantOrg := map[string]string{id1: name1, id2: name2}
mockOrganization.EXPECT().
GetOrganizations(gomock.Any()).
Return(allOrg, nil).
Times(1)
resultOrg, err := client.ListOrganization(t.Context())
require.NoError(t, err)
require.NotEmpty(t, resultOrg)
orgs := make(map[string]string, len(*allOrg))
for _, org := range *allOrg {
orgs[*org.Id] = org.Name
}
require.Equal(t, wantOrg, orgs)
})
}
func Test_CreateBucket(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dummyID := "id1"
dummyOrgID := "org123"
dummyBucketName := "bucket123"
testCases := []struct {
name string
orgID string
bucketName string
respBucket *domain.Bucket
wantBucketID string
expectErr bool
err error
}{
{
name: "try creating bucket with empty organization id",
orgID: "",
bucketName: dummyBucketName,
expectErr: true,
respBucket: nil,
wantBucketID: "",
err: errEmptyOrganizationID,
},
{
name: "try creating bucket with empty bucket name",
orgID: dummyOrgID,
bucketName: "",
expectErr: true,
respBucket: nil,
wantBucketID: "",
err: errEmptyBucketName,
},
{
name: "successfully creating a new bucket",
orgID: dummyOrgID,
bucketName: dummyBucketName,
expectErr: false,
respBucket: &domain.Bucket{Id: &dummyID},
wantBucketID: dummyID,
err: nil,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
client := *setupDB(t, ctrl)
mockBucket := NewMockbucket(ctrl)
client.influx.bucket = mockBucket
mockBucket.EXPECT().
CreateBucketWithNameWithID(gomock.Any(), tt.orgID, tt.bucketName).
Return(tt.respBucket, tt.err).
AnyTimes()
bucketID, err := client.CreateBucket(t.Context(), tt.orgID, tt.bucketName)
if tt.expectErr {
require.Error(t, err)
require.Equal(t, err, tt.err)
} else {
require.Equal(t, tt.wantBucketID, bucketID)
require.NoError(t, err)
}
})
}
}
func Test_DeleteBucket(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dummyID := "id1"
testCases := []struct {
name string
orgID string
bucketID string
expectErr bool
err error
}{
{
name: "try deleting bucket with empty bucket id",
orgID: "",
bucketID: "",
expectErr: true,
err: errEmptyBucketID,
},
{
name: "successfully deleting a new bucket",
orgID: "",
bucketID: dummyID,
expectErr: false,
err: nil,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
client := *setupDB(t, ctrl)
mockBucket := NewMockbucket(ctrl)
client.influx.bucket = mockBucket
mockBucket.EXPECT().
DeleteBucketWithID(gomock.Any(), tt.bucketID).
Return(tt.err).
AnyTimes()
err := client.DeleteBucket(t.Context(), tt.bucketID)
if tt.expectErr {
require.Error(t, err)
require.Equal(t, err, tt.err)
} else {
require.NoError(t, err)
}
})
}
}
func Test_ListBucket(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
dummyOrgName := "orgName"
id1, name1 := "id1", "name1"
id2, name2 := "id1", "name1"
testCases := []struct {
name string
orgName string
resp *[]domain.Bucket
wantBuckets map[string]string
expectErr bool
err error
}{
{
name: "try list bucket with empty organization name",
orgName: "",
expectErr: true,
wantBuckets: nil,
resp: &[]domain.Bucket{},
err: errEmptyOrganizationName,
},
{
name: "success list organizations",
orgName: dummyOrgName,
resp: &[]domain.Bucket{
{Id: &id1, Name: name1},
{Id: &id2, Name: name2},
},
wantBuckets: map[string]string{
id1: name1,
id2: name2,
},
expectErr: false,
err: nil,
},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
client := *setupDB(t, ctrl)
mockBucket := NewMockbucket(ctrl)
client.influx.bucket = mockBucket
mockBucket.EXPECT().
FindBucketsByOrgName(gomock.Any(), tt.orgName).
Return(tt.resp, tt.err).
AnyTimes()
buckets, err := client.ListBuckets(t.Context(), tt.orgName)
if tt.expectErr {
require.Error(t, err)
require.Equal(t, err, tt.err)
} else {
require.NoError(t, err)
require.NotEmpty(t, buckets)
require.Equal(t, tt.wantBuckets, buckets)
}
})
}
}
func Test_Query(t *testing.T) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := *setupDB(t, ctrl)
mockClient := client.influx.client.(*Mockclient)
mockQueryAPI := NewMockquery(ctrl)
// Mock QueryAPI call
mockClient.EXPECT().
QueryAPI("org1").
Return(mockQueryAPI).
Times(1)
// Mock Query call
mockQueryAPI.EXPECT().
Query(gomock.Any(), "dummyQuery1").
Return(nil, errFailedQuery).
Times(1)
result, err := client.Query(t.Context(), "org1", "dummyQuery1")
require.Error(t, err)
require.Equal(t, errFailedQuery, err)
require.Empty(t, result)
}
================================================
FILE: pkg/gofr/datasource/influxdb/interface.go
================================================
package influxdb
import (
"context"
influxdb "github.com/influxdata/influxdb-client-go/v2"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
// client defines the operations required to interact with an InfluxDB instance.
type client interface {
Setup(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int) (*domain.OnboardingResponse, error)
Health(ctx context.Context) (*domain.HealthCheck, error)
Ping(ctx context.Context) (bool, error)
Options() *influxdb.Options
WriteAPIBlocking(org, bucket string) api.WriteAPIBlocking
QueryAPI(org string) api.QueryAPI
AuthorizationsAPI() api.AuthorizationsAPI
OrganizationsAPI() api.OrganizationsAPI
DeleteAPI() api.DeleteAPI
BucketsAPI() api.BucketsAPI
}
// organization provides methods for managing Organizations in a InfluxDB server.
type organization interface {
GetOrganizations(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Organization, error)
FindOrganizationByName(ctx context.Context, orgName string) (*domain.Organization, error)
CreateOrganizationWithName(ctx context.Context, orgName string) (*domain.Organization, error)
DeleteOrganizationWithID(ctx context.Context, orgID string) error
}
// bucket provides methods for managing Buckets in a InfluxDB server.
type bucket interface {
GetBuckets(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Bucket, error)
FindBucketByName(ctx context.Context, bucketName string) (*domain.Bucket, error)
FindBucketByID(ctx context.Context, bucketID string) (*domain.Bucket, error)
FindBucketsByOrgName(ctx context.Context, orgName string, pagingOptions ...api.PagingOption) (*[]domain.Bucket, error)
CreateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error)
CreateBucketWithName(
ctx context.Context, org *domain.Organization, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error)
CreateBucketWithNameWithID(ctx context.Context, orgID, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error)
UpdateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error)
DeleteBucketWithID(ctx context.Context, bucketID string) error
}
type query interface {
QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error)
QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params any) (string, error)
Query(ctx context.Context, query string) (*api.QueryTableResult, error)
QueryWithParams(ctx context.Context, query string, params any) (*api.QueryTableResult, error)
}
================================================
FILE: pkg/gofr/datasource/influxdb/internal.go
================================================
package influxdb
import (
"context"
"github.com/influxdata/influxdb-client-go/v2/api"
"github.com/influxdata/influxdb-client-go/v2/domain"
)
type influxdbOrganizationAPI struct {
api api.OrganizationsAPI
}
// NewInfluxdbOrganizationAPI creates a new bucket API wrapper.
func NewInfluxdbOrganizationAPI(a api.OrganizationsAPI) organization {
return influxdbOrganizationAPI{api: a}
}
func (a influxdbOrganizationAPI) GetOrganizations(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Organization, error) {
return a.api.GetOrganizations(ctx, pagingOptions...)
}
func (a influxdbOrganizationAPI) FindOrganizationByName(ctx context.Context, orgName string) (*domain.Organization, error) {
return a.api.FindOrganizationByName(ctx, orgName)
}
func (a influxdbOrganizationAPI) CreateOrganizationWithName(ctx context.Context, orgName string) (*domain.Organization, error) {
return a.api.CreateOrganizationWithName(ctx, orgName)
}
func (a influxdbOrganizationAPI) DeleteOrganizationWithID(ctx context.Context, orgID string) error {
return a.api.DeleteOrganizationWithID(ctx, orgID)
}
// influxdbBucketAPI.
type influxdbBucketAPI struct {
api api.BucketsAPI
}
// NewInfluxdbBucketAPI creates a new bucket API wrapper.
func NewInfluxdbBucketAPI(a api.BucketsAPI) bucket {
return influxdbBucketAPI{api: a}
}
func (b influxdbBucketAPI) GetBuckets(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Bucket, error) {
return b.api.GetBuckets(ctx, pagingOptions...)
}
func (b influxdbBucketAPI) FindBucketsByOrgName(ctx context.Context, orgName string, pagingOptions ...api.PagingOption) (
*[]domain.Bucket, error,
) {
return b.api.FindBucketsByOrgName(ctx, orgName, pagingOptions...)
}
func (b influxdbBucketAPI) FindBucketByName(ctx context.Context, bucketName string) (*domain.Bucket, error) {
return b.api.FindBucketByName(ctx, bucketName)
}
func (b influxdbBucketAPI) FindBucketByID(ctx context.Context, bucketID string) (*domain.Bucket, error) {
return b.api.FindBucketByID(ctx, bucketID)
}
func (b influxdbBucketAPI) CreateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
return b.api.CreateBucket(ctx, bucket)
}
func (b influxdbBucketAPI) CreateBucketWithName(
ctx context.Context, org *domain.Organization, bucketName string, rules ...domain.RetentionRule,
) (*domain.Bucket, error) {
return b.api.CreateBucketWithName(ctx, org, bucketName, rules...)
}
func (b influxdbBucketAPI) CreateBucketWithNameWithID(
ctx context.Context, orgID, bucketName string, rules ...domain.RetentionRule,
) (*domain.Bucket, error) {
return b.api.CreateBucketWithNameWithID(ctx, orgID, bucketName, rules...)
}
func (b influxdbBucketAPI) UpdateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
return b.api.UpdateBucket(ctx, bucket)
}
func (b influxdbBucketAPI) DeleteBucketWithID(ctx context.Context, bucketID string) error {
return b.api.DeleteBucketWithID(ctx, bucketID)
}
================================================
FILE: pkg/gofr/datasource/influxdb/logger.go
================================================
package influxdb
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type QueryLog struct {
Operation string `json:"operation"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Keyspace string `json:"keyspace,omitempty"`
Args []any `json:"args,omitempty"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
var argsStr string
if len(ql.Args) > 0 {
var parts []string
for _, a := range ql.Args {
parts = append(parts, clean(fmt.Sprintf("%v", a)))
}
argsStr = " [" + strings.Join(parts, ", ") + "]"
}
fmt.Fprintf(
writer,
"\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mms\u001B[0m %s \u001B[38;5;8m%-32s\u001B[0m%s\n",
clean(ql.Operation),
"INFL",
ql.Duration,
clean(ql.Keyspace),
clean(ql.Query),
argsStr,
)
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/influxdb/metrics.go
================================================
package influxdb
import "context"
// Metrics defines the interface for capturing metrics.
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/influxdb/metrics_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ./metrics.go
//
// Generated by this command:
//
// mockgen -source=./metrics.go -destination=./metrics_logger.go -package=influxdb
//
// Package influxdb is a generated GoMock package.
package influxdb
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/influxdb/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
// Package mock_influxdb is a generated GoMock package.
package influxdb
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
influxdb "github.com/influxdata/influxdb-client-go/v2"
api "github.com/influxdata/influxdb-client-go/v2/api"
domain "github.com/influxdata/influxdb-client-go/v2/domain"
)
// Mockclient is a mock of client interface.
type Mockclient struct {
ctrl *gomock.Controller
recorder *MockclientMockRecorder
}
// MockclientMockRecorder is the mock recorder for Mockclient.
type MockclientMockRecorder struct {
mock *Mockclient
}
// NewMockclient creates a new mock instance.
func NewMockclient(ctrl *gomock.Controller) *Mockclient {
mock := &Mockclient{ctrl: ctrl}
mock.recorder = &MockclientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockclient) EXPECT() *MockclientMockRecorder {
return m.recorder
}
// AuthorizationsAPI mocks base method.
func (m *Mockclient) AuthorizationsAPI() api.AuthorizationsAPI {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AuthorizationsAPI")
ret0, _ := ret[0].(api.AuthorizationsAPI)
return ret0
}
// AuthorizationsAPI indicates an expected call of AuthorizationsAPI.
func (mr *MockclientMockRecorder) AuthorizationsAPI() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthorizationsAPI", reflect.TypeOf((*Mockclient)(nil).AuthorizationsAPI))
}
// BucketsAPI mocks base method.
func (m *Mockclient) BucketsAPI() api.BucketsAPI {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BucketsAPI")
ret0, _ := ret[0].(api.BucketsAPI)
return ret0
}
// BucketsAPI indicates an expected call of BucketsAPI.
func (mr *MockclientMockRecorder) BucketsAPI() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BucketsAPI", reflect.TypeOf((*Mockclient)(nil).BucketsAPI))
}
// DeleteAPI mocks base method.
func (m *Mockclient) DeleteAPI() api.DeleteAPI {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAPI")
ret0, _ := ret[0].(api.DeleteAPI)
return ret0
}
// DeleteAPI indicates an expected call of DeleteAPI.
func (mr *MockclientMockRecorder) DeleteAPI() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAPI", reflect.TypeOf((*Mockclient)(nil).DeleteAPI))
}
// Health mocks base method.
func (m *Mockclient) Health(ctx context.Context) (*domain.HealthCheck, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health", ctx)
ret0, _ := ret[0].(*domain.HealthCheck)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Health indicates an expected call of Health.
func (mr *MockclientMockRecorder) Health(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*Mockclient)(nil).Health), ctx)
}
// Options mocks base method.
func (m *Mockclient) Options() *influxdb.Options {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Options")
ret0, _ := ret[0].(*influxdb.Options)
return ret0
}
// Options indicates an expected call of Options.
func (mr *MockclientMockRecorder) Options() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Options", reflect.TypeOf((*Mockclient)(nil).Options))
}
// OrganizationsAPI mocks base method.
func (m *Mockclient) OrganizationsAPI() api.OrganizationsAPI {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OrganizationsAPI")
ret0, _ := ret[0].(api.OrganizationsAPI)
return ret0
}
// OrganizationsAPI indicates an expected call of OrganizationsAPI.
func (mr *MockclientMockRecorder) OrganizationsAPI() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrganizationsAPI", reflect.TypeOf((*Mockclient)(nil).OrganizationsAPI))
}
// Ping mocks base method.
func (m *Mockclient) Ping(ctx context.Context) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Ping indicates an expected call of Ping.
func (mr *MockclientMockRecorder) Ping(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*Mockclient)(nil).Ping), ctx)
}
// QueryAPI mocks base method.
func (m *Mockclient) QueryAPI(org string) api.QueryAPI {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryAPI", org)
ret0, _ := ret[0].(api.QueryAPI)
return ret0
}
// QueryAPI indicates an expected call of QueryAPI.
func (mr *MockclientMockRecorder) QueryAPI(org interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryAPI", reflect.TypeOf((*Mockclient)(nil).QueryAPI), org)
}
// Setup mocks base method.
func (m *Mockclient) Setup(ctx context.Context, username, password, org, bucket string, retentionPeriodHours int) (*domain.OnboardingResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Setup", ctx, username, password, org, bucket, retentionPeriodHours)
ret0, _ := ret[0].(*domain.OnboardingResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Setup indicates an expected call of Setup.
func (mr *MockclientMockRecorder) Setup(ctx, username, password, org, bucket, retentionPeriodHours interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Setup", reflect.TypeOf((*Mockclient)(nil).Setup), ctx, username, password, org, bucket, retentionPeriodHours)
}
// WriteAPIBlocking mocks base method.
func (m *Mockclient) WriteAPIBlocking(org, bucket string) api.WriteAPIBlocking {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WriteAPIBlocking", org, bucket)
ret0, _ := ret[0].(api.WriteAPIBlocking)
return ret0
}
// WriteAPIBlocking indicates an expected call of WriteAPIBlocking.
func (mr *MockclientMockRecorder) WriteAPIBlocking(org, bucket interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteAPIBlocking", reflect.TypeOf((*Mockclient)(nil).WriteAPIBlocking), org, bucket)
}
// Mockorganization is a mock of organization interface.
type Mockorganization struct {
ctrl *gomock.Controller
recorder *MockorganizationMockRecorder
}
// MockorganizationMockRecorder is the mock recorder for Mockorganization.
type MockorganizationMockRecorder struct {
mock *Mockorganization
}
// NewMockorganization creates a new mock instance.
func NewMockorganization(ctrl *gomock.Controller) *Mockorganization {
mock := &Mockorganization{ctrl: ctrl}
mock.recorder = &MockorganizationMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockorganization) EXPECT() *MockorganizationMockRecorder {
return m.recorder
}
// CreateOrganizationWithName mocks base method.
func (m *Mockorganization) CreateOrganizationWithName(ctx context.Context, orgName string) (*domain.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrganizationWithName", ctx, orgName)
ret0, _ := ret[0].(*domain.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrganizationWithName indicates an expected call of CreateOrganizationWithName.
func (mr *MockorganizationMockRecorder) CreateOrganizationWithName(ctx, orgName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrganizationWithName", reflect.TypeOf((*Mockorganization)(nil).CreateOrganizationWithName), ctx, orgName)
}
// DeleteOrganizationWithID mocks base method.
func (m *Mockorganization) DeleteOrganizationWithID(ctx context.Context, orgID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOrganizationWithID", ctx, orgID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteOrganizationWithID indicates an expected call of DeleteOrganizationWithID.
func (mr *MockorganizationMockRecorder) DeleteOrganizationWithID(ctx, orgID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOrganizationWithID", reflect.TypeOf((*Mockorganization)(nil).DeleteOrganizationWithID), ctx, orgID)
}
// FindOrganizationByName mocks base method.
func (m *Mockorganization) FindOrganizationByName(ctx context.Context, orgName string) (*domain.Organization, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOrganizationByName", ctx, orgName)
ret0, _ := ret[0].(*domain.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindOrganizationByName indicates an expected call of FindOrganizationByName.
func (mr *MockorganizationMockRecorder) FindOrganizationByName(ctx, orgName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOrganizationByName", reflect.TypeOf((*Mockorganization)(nil).FindOrganizationByName), ctx, orgName)
}
// GetOrganizations mocks base method.
func (m *Mockorganization) GetOrganizations(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Organization, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx}
for _, a := range pagingOptions {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetOrganizations", varargs...)
ret0, _ := ret[0].(*[]domain.Organization)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetOrganizations indicates an expected call of GetOrganizations.
func (mr *MockorganizationMockRecorder) GetOrganizations(ctx interface{}, pagingOptions ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx}, pagingOptions...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOrganizations", reflect.TypeOf((*Mockorganization)(nil).GetOrganizations), varargs...)
}
// Mockbucket is a mock of bucket interface.
type Mockbucket struct {
ctrl *gomock.Controller
recorder *MockbucketMockRecorder
}
// MockbucketMockRecorder is the mock recorder for Mockbucket.
type MockbucketMockRecorder struct {
mock *Mockbucket
}
// NewMockbucket creates a new mock instance.
func NewMockbucket(ctrl *gomock.Controller) *Mockbucket {
mock := &Mockbucket{ctrl: ctrl}
mock.recorder = &MockbucketMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockbucket) EXPECT() *MockbucketMockRecorder {
return m.recorder
}
// CreateBucket mocks base method.
func (m *Mockbucket) CreateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateBucket", ctx, bucket)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateBucket indicates an expected call of CreateBucket.
func (mr *MockbucketMockRecorder) CreateBucket(ctx, bucket interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*Mockbucket)(nil).CreateBucket), ctx, bucket)
}
// CreateBucketWithName mocks base method.
func (m *Mockbucket) CreateBucketWithName(ctx context.Context, org *domain.Organization, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, org, bucketName}
for _, a := range rules {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateBucketWithName", varargs...)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateBucketWithName indicates an expected call of CreateBucketWithName.
func (mr *MockbucketMockRecorder) CreateBucketWithName(ctx, org, bucketName interface{}, rules ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, org, bucketName}, rules...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucketWithName", reflect.TypeOf((*Mockbucket)(nil).CreateBucketWithName), varargs...)
}
// CreateBucketWithNameWithID mocks base method.
func (m *Mockbucket) CreateBucketWithNameWithID(ctx context.Context, orgID, bucketName string, rules ...domain.RetentionRule) (*domain.Bucket, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, orgID, bucketName}
for _, a := range rules {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateBucketWithNameWithID", varargs...)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateBucketWithNameWithID indicates an expected call of CreateBucketWithNameWithID.
func (mr *MockbucketMockRecorder) CreateBucketWithNameWithID(ctx, orgID, bucketName interface{}, rules ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, orgID, bucketName}, rules...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucketWithNameWithID", reflect.TypeOf((*Mockbucket)(nil).CreateBucketWithNameWithID), varargs...)
}
// DeleteBucketWithID mocks base method.
func (m *Mockbucket) DeleteBucketWithID(ctx context.Context, bucketID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteBucketWithID", ctx, bucketID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteBucketWithID indicates an expected call of DeleteBucketWithID.
func (mr *MockbucketMockRecorder) DeleteBucketWithID(ctx, bucketID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketWithID", reflect.TypeOf((*Mockbucket)(nil).DeleteBucketWithID), ctx, bucketID)
}
// FindBucketByID mocks base method.
func (m *Mockbucket) FindBucketByID(ctx context.Context, bucketID string) (*domain.Bucket, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindBucketByID", ctx, bucketID)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindBucketByID indicates an expected call of FindBucketByID.
func (mr *MockbucketMockRecorder) FindBucketByID(ctx, bucketID interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindBucketByID", reflect.TypeOf((*Mockbucket)(nil).FindBucketByID), ctx, bucketID)
}
// FindBucketByName mocks base method.
func (m *Mockbucket) FindBucketByName(ctx context.Context, bucketName string) (*domain.Bucket, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindBucketByName", ctx, bucketName)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindBucketByName indicates an expected call of FindBucketByName.
func (mr *MockbucketMockRecorder) FindBucketByName(ctx, bucketName interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindBucketByName", reflect.TypeOf((*Mockbucket)(nil).FindBucketByName), ctx, bucketName)
}
// FindBucketsByOrgName mocks base method.
func (m *Mockbucket) FindBucketsByOrgName(ctx context.Context, orgName string, pagingOptions ...api.PagingOption) (*[]domain.Bucket, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx, orgName}
for _, a := range pagingOptions {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FindBucketsByOrgName", varargs...)
ret0, _ := ret[0].(*[]domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FindBucketsByOrgName indicates an expected call of FindBucketsByOrgName.
func (mr *MockbucketMockRecorder) FindBucketsByOrgName(ctx, orgName interface{}, pagingOptions ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx, orgName}, pagingOptions...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindBucketsByOrgName", reflect.TypeOf((*Mockbucket)(nil).FindBucketsByOrgName), varargs...)
}
// GetBuckets mocks base method.
func (m *Mockbucket) GetBuckets(ctx context.Context, pagingOptions ...api.PagingOption) (*[]domain.Bucket, error) {
m.ctrl.T.Helper()
varargs := []interface{}{ctx}
for _, a := range pagingOptions {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetBuckets", varargs...)
ret0, _ := ret[0].(*[]domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetBuckets indicates an expected call of GetBuckets.
func (mr *MockbucketMockRecorder) GetBuckets(ctx interface{}, pagingOptions ...interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]interface{}{ctx}, pagingOptions...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBuckets", reflect.TypeOf((*Mockbucket)(nil).GetBuckets), varargs...)
}
// UpdateBucket mocks base method.
func (m *Mockbucket) UpdateBucket(ctx context.Context, bucket *domain.Bucket) (*domain.Bucket, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateBucket", ctx, bucket)
ret0, _ := ret[0].(*domain.Bucket)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateBucket indicates an expected call of UpdateBucket.
func (mr *MockbucketMockRecorder) UpdateBucket(ctx, bucket interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateBucket", reflect.TypeOf((*Mockbucket)(nil).UpdateBucket), ctx, bucket)
}
// Mockquery is a mock of query interface.
type Mockquery struct {
ctrl *gomock.Controller
recorder *MockqueryMockRecorder
}
// MockqueryMockRecorder is the mock recorder for Mockquery.
type MockqueryMockRecorder struct {
mock *Mockquery
}
// NewMockquery creates a new mock instance.
func NewMockquery(ctrl *gomock.Controller) *Mockquery {
mock := &Mockquery{ctrl: ctrl}
mock.recorder = &MockqueryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockquery) EXPECT() *MockqueryMockRecorder {
return m.recorder
}
// Query mocks base method.
func (m *Mockquery) Query(ctx context.Context, query string) (*api.QueryTableResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query)
ret0, _ := ret[0].(*api.QueryTableResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockqueryMockRecorder) Query(ctx, query interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*Mockquery)(nil).Query), ctx, query)
}
// QueryRaw mocks base method.
func (m *Mockquery) QueryRaw(ctx context.Context, query string, dialect *domain.Dialect) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryRaw", ctx, query, dialect)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryRaw indicates an expected call of QueryRaw.
func (mr *MockqueryMockRecorder) QueryRaw(ctx, query, dialect interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRaw", reflect.TypeOf((*Mockquery)(nil).QueryRaw), ctx, query, dialect)
}
// QueryRawWithParams mocks base method.
func (m *Mockquery) QueryRawWithParams(ctx context.Context, query string, dialect *domain.Dialect, params any) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryRawWithParams", ctx, query, dialect, params)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryRawWithParams indicates an expected call of QueryRawWithParams.
func (mr *MockqueryMockRecorder) QueryRawWithParams(ctx, query, dialect, params interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRawWithParams", reflect.TypeOf((*Mockquery)(nil).QueryRawWithParams), ctx, query, dialect, params)
}
// QueryWithParams mocks base method.
func (m *Mockquery) QueryWithParams(ctx context.Context, query string, params any) (*api.QueryTableResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "QueryWithParams", ctx, query, params)
ret0, _ := ret[0].(*api.QueryTableResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// QueryWithParams indicates an expected call of QueryWithParams.
func (mr *MockqueryMockRecorder) QueryWithParams(ctx, query, params interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithParams", reflect.TypeOf((*Mockquery)(nil).QueryWithParams), ctx, query, params)
}
================================================
FILE: pkg/gofr/datasource/influxdb/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ./logger.go
//
// Generated by this command:
//
// mockgen -source=./logger.go -destination=./mock_logger.go -package=influxdb
//
// Package influxdb is a generated GoMock package.
package influxdb
import (
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/interface.go
================================================
package datasource
import (
"errors"
"io"
"os"
)
// File represents a file in the filesystem.
type File interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
io.Writer
io.WriterAt
ReadAll() (RowReader, error)
}
type RowReader interface {
Next() bool
Scan(any) error
}
// FileSystem : Any simulated or real filesystem should implement this interface.
type FileSystem interface {
// Create creates a file in the filesystem, returning the file and an
// error, if any happens.
Create(name string) (File, error)
// Mkdir creates a directory in the filesystem, return an error if any
// happens.
Mkdir(name string, perm os.FileMode) error
// MkdirAll creates a directory path and all parents that does not exist
// yet.
MkdirAll(path string, perm os.FileMode) error
// Open opens a file, returning it or an error, if any happens.
Open(name string) (File, error)
// OpenFile opens a file using the given flags and the given mode.
OpenFile(name string, flag int, perm os.FileMode) (File, error)
// Remove removes a file identified by name, returning an error, if any
// happens.
Remove(name string) error
// RemoveAll removes a directory path and any children it contains. It
// does not fail if the path does not exist (return nil).
RemoveAll(path string) error
// Rename renames a file.
Rename(oldname, newname string) error
}
var (
ErrFileClosed = errors.New("File is closed")
ErrOutOfRange = errors.New("out of range")
ErrTooLarge = errors.New("too large")
ErrFileNotFound = os.ErrNotExist
ErrFileExists = os.ErrExist
ErrDestinationExists = os.ErrExist
)
type FileSystemProvider interface {
FileSystem
// UseLogger sets the logger for the FileSystem client.
UseLogger(logger any)
// UseMetrics sets the metrics for the FileSystem client.
UseMetrics(metrics any)
// Connect establishes a connection to FileSystem and registers metrics using the provided configuration when the client was Created.
Connect()
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/badger.go
================================================
package badger
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/dgraph-io/badger/v4"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var errStatusDown = errors.New("status down")
type Configs struct {
DirPath string
}
type Client struct {
db *badger.DB
configs *Configs
logger Logger
metrics Metrics
tracer trace.Tracer
}
func New(configs Configs) *Client {
return &Client{configs: &configs}
}
// UseLogger sets the logger for the BadgerDB client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the BadgerDB client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for BadgerDB client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// Connect establishes a connection to BadgerDB and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
c.logger.Debugf("connecting to BadgerDB at %v", c.configs.DirPath)
badgerBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_badger_stats", "Response time of Badger queries in milliseconds.", badgerBuckets...)
db, err := badger.Open(badger.DefaultOptions(c.configs.DirPath))
if err != nil {
c.logger.Errorf("error while connecting to BadgerDB: %v", err)
return
}
c.db = db
c.logger.Infof("connected to BadgerDB at %v", c.configs.DirPath)
}
func (c *Client) Get(ctx context.Context, key string) (string, error) {
span := c.addTrace(ctx, "get", key)
defer c.sendOperationStats(time.Now(), "GET", "get", span, key)
var value []byte
// transaction is set to false as we don't want to make any changes to data.
txn := c.db.NewTransaction(false)
defer txn.Discard()
item, err := txn.Get([]byte(key))
if err != nil {
c.logger.Debugf("error while fetching data for key: %v, error: %v", key, err)
return "", err
}
value, err = item.ValueCopy(nil)
if err != nil {
c.logger.Debugf("error while reading value for key: %v, error: %v", key, err)
return "", err
}
err = txn.Commit()
if err != nil {
c.logger.Debugf("error while committing transaction: %v", err)
return "", err
}
return string(value), nil
}
func (c *Client) Set(ctx context.Context, key, value string) error {
span := c.addTrace(ctx, "set", key)
defer c.sendOperationStats(time.Now(), "SET", "set", span, key, value)
return c.useTransaction(func(txn *badger.Txn) error {
return txn.Set([]byte(key), []byte(value))
})
}
func (c *Client) Delete(ctx context.Context, key string) error {
span := c.addTrace(ctx, "delete", key)
defer c.sendOperationStats(time.Now(), "DELETE", "delete", span, key, "")
return c.useTransaction(func(txn *badger.Txn) error {
return txn.Delete([]byte(key))
})
}
func (c *Client) useTransaction(f func(txn *badger.Txn) error) error {
txn := c.db.NewTransaction(true)
defer txn.Discard()
err := f(txn)
if err != nil {
c.logger.Debugf("error while executing transaction: %v", err)
return err
}
err = txn.Commit()
if err != nil {
c.logger.Debugf("error while committing transaction: %v", err)
return err
}
return nil
}
func (c *Client) sendOperationStats(start time.Time, methodType string, method string,
span trace.Span, kv ...string) {
duration := time.Since(start).Microseconds()
c.logger.Debug(&Log{
Type: methodType,
Duration: duration,
Key: strings.Join(kv, " "),
})
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("badger.%v.duration(μs)", method), time.Since(start).Microseconds()))
}
c.metrics.RecordHistogram(context.Background(), "app_badger_stats", float64(duration), "database", c.configs.DirPath,
"type", methodType)
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
func (c *Client) HealthCheck(context.Context) (any, error) {
h := Health{
Details: make(map[string]any),
}
h.Details["location"] = c.configs.DirPath
closed := c.db.IsClosed()
if closed {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
return &h, nil
}
func (c *Client) addTrace(ctx context.Context, method, key string) trace.Span {
if c.tracer != nil {
_, span := c.tracer.Start(ctx, fmt.Sprintf("badger-%v", method))
span.SetAttributes(
attribute.String("badger.key", key),
)
return span
}
return nil
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/badger_test.go
================================================
package badger
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func setupDB(t *testing.T) *Client {
t.Helper()
cl := New(Configs{DirPath: t.TempDir()})
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics.EXPECT().NewHistogram("app_badger_stats", "Response time of Badger queries in milliseconds.", gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_badger_stats", gomock.Any(), "database", cl.configs.DirPath,
"type", gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
cl.UseLogger(mockLogger)
cl.UseMetrics(mockMetrics)
cl.Connect()
return cl
}
func Test_ClientSet(t *testing.T) {
cl := setupDB(t)
err := cl.Set(context.Background(), "lkey", "lvalue")
require.NoError(t, err)
}
func Test_ClientGet(t *testing.T) {
cl := setupDB(t)
err := cl.Set(context.Background(), "lkey", "lvalue")
require.NoError(t, err)
val, err := cl.Get(context.Background(), "lkey")
require.NoError(t, err)
assert.Equal(t, "lvalue", val)
}
func Test_ClientGetError(t *testing.T) {
cl := setupDB(t)
val, err := cl.Get(context.Background(), "lkey")
require.EqualError(t, err, "Key not found")
assert.Empty(t, val)
}
func Test_ClientDeleteSuccessError(t *testing.T) {
cl := setupDB(t)
err := cl.Delete(context.Background(), "lkey")
require.NoError(t, err)
}
func Test_ClientHealthCheck(t *testing.T) {
cl := setupDB(t)
val, err := cl.HealthCheck(context.Background())
require.NoError(t, err)
assert.Contains(t, fmt.Sprint(val), "UP")
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/kv-store/badger
go 1.25.0
require (
github.com/dgraph-io/badger/v4 v4.9.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/kv-store/badger/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc=
github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/kv-store/badger/logger.go
================================================
package badger
import (
"fmt"
"io"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Info(args ...any)
Infof(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Type string `json:"type"`
Duration int64 `json:"duration"`
Key string `json:"key"`
Value string `json:"value,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;162m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s \n",
l.Type, "BADGR", l.Duration, l.Key+" "+l.Value)
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/logger_test.go
================================================
package badger
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := Log{
Type: "GET",
Duration: 12345,
}
expected := "GET"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/metrics.go
================================================
package badger
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=badger
//
// Package badger is a generated GoMock package.
package badger
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/badger/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=badger
//
// Package badger is a generated GoMock package.
package badger
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/dynamo.go
================================================
package dynamodb
import (
"context"
"encoding/json"
"errors"
"fmt"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
errClientNotConnected = errors.New("client not connected, call Connect() first")
errKeyNotFound = errors.New("key not found")
errStatusDown = errors.New("status down")
)
type Configs struct {
Table string
Region string
Endpoint string
PartitionKeyName string
}
type dynamoDBInterface interface {
PutItem(
ctx context.Context,
params *dynamodb.PutItemInput,
optFns ...func(*dynamodb.Options),
) (*dynamodb.PutItemOutput, error)
GetItem(
ctx context.Context,
params *dynamodb.GetItemInput,
optFns ...func(*dynamodb.Options),
) (*dynamodb.GetItemOutput, error)
DeleteItem(
ctx context.Context,
params *dynamodb.DeleteItemInput,
optFns ...func(*dynamodb.Options),
) (*dynamodb.DeleteItemOutput, error)
DescribeTable(
ctx context.Context,
params *dynamodb.DescribeTableInput,
optFns ...func(*dynamodb.Options),
) (*dynamodb.DescribeTableOutput, error)
}
type Client struct {
db dynamoDBInterface
configs *Configs
logger Logger
metrics Metrics
tracer trace.Tracer
connected bool
}
func New(configs Configs) *Client {
if configs.PartitionKeyName == "" {
configs.PartitionKeyName = "pk"
}
return &Client{configs: &configs, connected: false}
}
// Connect establishes a connection to DynamoDB and registers metrics using the provided configuration.
func (c *Client) Connect() {
c.logger.Debugf("connecting to DynamoDB table %v in region %v", c.configs.Table, c.configs.Region)
dynamoBuckets := []float64{1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000}
c.metrics.NewHistogram("app_dynamodb_duration_ms", "Response time of DynamoDB queries in milliseconds.", dynamoBuckets...)
awsCfg, err := config.LoadDefaultConfig(context.Background(), config.WithRegion(c.configs.Region))
if err != nil {
c.logger.Errorf("error loading AWS config: %v", err)
return
}
var opts []func(*dynamodb.Options)
if c.configs.Endpoint != "" {
opts = append(opts, func(o *dynamodb.Options) {
o.BaseEndpoint = aws.String(c.configs.Endpoint)
})
}
db := dynamodb.NewFromConfig(awsCfg, opts...)
c.db = db
c.connected = true
c.logger.Infof("connected to DynamoDB table %v in region %v", c.configs.Table, c.configs.Region)
}
// UseLogger sets the logger for the Dynamo client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the Dynamo client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for Dynamo client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// ToJSON converts a Go struct to JSON string for use with KVStore.Set.
func ToJSON(value any) (string, error) {
jsonData, err := json.Marshal(value)
if err != nil {
return "", fmt.Errorf("failed to marshal value to JSON: %w", err)
}
return string(jsonData), nil
}
// FromJSON converts a JSON string to a Go struct for use with KVStore.Get.
func FromJSON(jsonData string, dest any) error {
if err := json.Unmarshal([]byte(jsonData), dest); err != nil {
return fmt.Errorf("failed to unmarshal JSON: %w", err)
}
return nil
}
func (c *Client) Get(ctx context.Context, key string) (string, error) {
if !c.connected {
return "", errClientNotConnected
}
span := c.addTrace(ctx, "get", key)
defer c.sendOperationsStats(time.Now(), "GET", "get", span, key)
input := &dynamodb.GetItemInput{
TableName: aws.String(c.configs.Table),
Key: map[string]types.AttributeValue{
c.configs.PartitionKeyName: &types.AttributeValueMemberS{Value: key},
},
}
out, err := c.db.GetItem(ctx, input)
if err != nil {
c.logger.Errorf("error while fetching data for key: %v, error: %v", key, err)
return "", err
}
if out.Item == nil {
return "", errKeyNotFound
}
// Look for a "value" field that contains the JSON string
if valueField, exists := out.Item["value"]; exists {
if stringValue, ok := valueField.(*types.AttributeValueMemberS); ok {
return stringValue.Value, nil
}
}
// If no "value" field exists, return key not found error
return "", fmt.Errorf("%w: %s", errKeyNotFound, key)
}
func (c *Client) Set(ctx context.Context, key, value string) error {
if !c.connected {
return errClientNotConnected
}
span := c.addTrace(ctx, "set", key)
defer c.sendOperationsStats(time.Now(), "SET", "set", span, key)
// Store the value as a string in the "value" field
item := map[string]types.AttributeValue{
c.configs.PartitionKeyName: &types.AttributeValueMemberS{Value: key},
"value": &types.AttributeValueMemberS{Value: value},
}
input := &dynamodb.PutItemInput{
TableName: aws.String(c.configs.Table),
Item: item,
}
_, err := c.db.PutItem(ctx, input)
if err != nil {
c.logger.Errorf("error while setting data for key: %v, error: %v", key, err)
return err
}
return nil
}
func (c *Client) Delete(ctx context.Context, key string) error {
if !c.connected {
return errClientNotConnected
}
span := c.addTrace(ctx, "delete", key)
defer c.sendOperationsStats(time.Now(), "DELETE", "delete", span, key)
input := &dynamodb.DeleteItemInput{
TableName: aws.String(c.configs.Table),
Key: map[string]types.AttributeValue{
c.configs.PartitionKeyName: &types.AttributeValueMemberS{Value: key},
},
}
_, err := c.db.DeleteItem(ctx, input)
if err != nil {
c.logger.Errorf("error while deleting data for key: %v, error: %v", key, err)
return err
}
return nil
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
if !c.connected {
return &Health{Status: "DOWN", Details: map[string]any{"error": "client not connected"}}, errStatusDown
}
h := Health{
Details: make(map[string]any),
}
h.Details["table"] = c.configs.Table
h.Details["region"] = c.configs.Region
input := &dynamodb.DescribeTableInput{TableName: aws.String(c.configs.Table)}
_, err := c.db.DescribeTable(ctx, input)
if err != nil {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
return &h, nil
}
func (c *Client) sendOperationsStats(start time.Time, methodType, method string, span trace.Span, kv ...string) {
duration := time.Since(start)
var key string
if len(kv) > 0 {
key = kv[0]
}
c.logger.Debug(&Log{
Type: methodType,
Duration: duration.Microseconds(),
Key: key,
Value: c.configs.Table,
})
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("dynamodb.%v.duration(μs)", method), duration.Microseconds()))
}
c.metrics.RecordHistogram(context.Background(), "app_dynamodb_duration_ms", float64(duration.Milliseconds()),
"table", c.configs.Table,
"operation", methodType)
}
func (c *Client) addTrace(ctx context.Context, method, key string) trace.Span {
if c.tracer != nil {
_, span := c.tracer.Start(ctx, fmt.Sprintf("dynamodb-%v", method))
span.SetAttributes(
attribute.String("dynamodb.method", method),
attribute.String("dynamodb.key", key),
)
return span
}
return nil
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/dynamo_test.go
================================================
package dynamodb
import (
"context"
"errors"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
)
var errDynamoFailure = errors.New("dynamodb error")
type testDeps struct {
ctx context.Context
client *Client
mockDB *MockdynamoDBInterface
mockLogger *MockLogger
mockMetrics *MockMetrics
finish func()
}
func setupTest(t *testing.T) testDeps {
t.Helper()
ctrl := gomock.NewController(t)
mockDB := NewMockdynamoDBInterface(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
client := &Client{
db: mockDB,
configs: &Configs{Table: "test-table", Region: "us-east-1", PartitionKeyName: "pk"},
logger: mockLogger,
metrics: mockMetrics,
connected: true, // Set as connected for tests
}
return testDeps{
ctx: t.Context(),
client: client,
mockDB: mockDB,
mockLogger: mockLogger,
mockMetrics: mockMetrics,
finish: ctrl.Finish,
}
}
func Test_ClientSet(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
value := "test-value"
expectedInput := &dynamodb.PutItemInput{
TableName: aws.String("test-table"),
Item: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
"value": &types.AttributeValueMemberS{Value: value},
},
}
mockDB.EXPECT().PutItem(ctx, expectedInput, gomock.Any()).Return(&dynamodb.PutItemOutput{}, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", client.configs.Table,
"operation", "SET",
)
require.NoError(t, client.Set(ctx, key, value))
}
func Test_ClientSetError(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
value := "test-value"
expectedErr := errDynamoFailure
mockDB.EXPECT().PutItem(ctx, gomock.Any(), gomock.Any()).Return(nil, expectedErr)
mockLogger.EXPECT().Errorf("error while setting data for key: %v, error: %v", key, expectedErr)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "SET",
)
err := client.Set(ctx, key, value)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func Test_ClientGet(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
expectedValue := "test-value"
expectedInput := &dynamodb.GetItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
expectedOutput := &dynamodb.GetItemOutput{
Item: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
"value": &types.AttributeValueMemberS{Value: expectedValue},
},
}
mockDB.EXPECT().GetItem(ctx, expectedInput, gomock.Any()).Return(expectedOutput, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "GET",
)
result, err := client.Get(ctx, key)
require.NoError(t, err)
assert.Equal(t, expectedValue, result)
}
func Test_ClientGetError(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
expectedErr := errDynamoFailure
expectedInput := &dynamodb.GetItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
mockDB.EXPECT().GetItem(ctx, expectedInput, gomock.Any()).Return(nil, expectedErr)
mockLogger.EXPECT().Errorf("error while fetching data for key: %v, error: %v", key, expectedErr)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "GET",
)
value, err := client.Get(ctx, key)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
assert.Empty(t, value)
}
func Test_ClientDelete(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
expectedInput := &dynamodb.DeleteItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
mockDB.EXPECT().DeleteItem(ctx, expectedInput, gomock.Any()).Return(&dynamodb.DeleteItemOutput{}, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", client.configs.Table,
"operation", "DELETE",
)
require.NoError(t, client.Delete(ctx, key))
}
func Test_ClientDeleteError(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "test-key"
expectedErr := errDynamoFailure
expectedInput := &dynamodb.DeleteItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
mockDB.EXPECT().DeleteItem(ctx, expectedInput, gomock.Any()).Return(nil, expectedErr)
mockLogger.EXPECT().Errorf("error while deleting data for key: %v, error: %v", key, expectedErr)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "DELETE",
)
err := client.Delete(ctx, key)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func Test_ClientHealthCheckSuccess(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
finish := deps.finish
defer finish()
expectedInput := &dynamodb.DescribeTableInput{
TableName: aws.String("test-table"),
}
mockDB.EXPECT().DescribeTable(t.Context(), expectedInput, gomock.Any()).Return(&dynamodb.DescribeTableOutput{}, nil)
res, err := client.HealthCheck(ctx)
require.NoError(t, err)
h, ok := res.(*Health)
require.True(t, ok)
assert.Equal(t, "UP", h.Status)
assert.Equal(t, map[string]any{
"table": "test-table",
"region": "us-east-1",
}, h.Details)
}
func Test_ClientHealthCheckFailure(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
finish := deps.finish
defer finish()
expectedErr := errDynamoFailure
expectedInput := &dynamodb.DescribeTableInput{
TableName: aws.String("test-table"),
}
mockDB.EXPECT().DescribeTable(t.Context(), expectedInput, gomock.Any()).Return(nil, expectedErr)
res, err := client.HealthCheck(ctx)
require.Error(t, err)
assert.Equal(t, errStatusDown, err)
h, ok := res.(*Health)
require.True(t, ok)
assert.Equal(t, "DOWN", h.Status)
assert.Equal(t, map[string]any{
"table": "test-table",
"region": "us-east-1",
}, h.Details)
}
func Test_ToJSON(t *testing.T) {
testData := map[string]any{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
}
jsonStr, err := ToJSON(testData)
require.NoError(t, err)
assert.NotEmpty(t, jsonStr)
assert.Contains(t, jsonStr, "John Doe")
assert.Contains(t, jsonStr, "john@example.com")
}
func Test_ToJSONError(t *testing.T) {
// Create a value that cannot be marshaled to JSON
invalidData := make(chan int)
jsonStr, err := ToJSON(invalidData)
require.Error(t, err)
assert.Empty(t, jsonStr)
assert.Contains(t, err.Error(), "failed to marshal value to JSON")
}
func Test_FromJSON(t *testing.T) {
jsonStr := `{"name":"John Doe","age":30,"email":"john@example.com"}`
var result map[string]any
err := FromJSON(jsonStr, &result)
require.NoError(t, err)
assert.Equal(t, "John Doe", result["name"])
assert.InEpsilon(t, float64(30), result["age"], 0.01)
assert.Equal(t, "john@example.com", result["email"])
}
func Test_FromJSONError(t *testing.T) {
invalidJSON := `{"name":"John Doe","age":30,"email":}`
var result map[string]any
err := FromJSON(invalidJSON, &result)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to unmarshal JSON")
}
func Test_ClientNotConnected(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
finish := deps.finish
defer finish()
// Set connected to false
client.connected = false
// Test Set when not connected
err := client.Set(ctx, "key", "value")
require.Error(t, err)
assert.Contains(t, err.Error(), "client not connected")
// Test Get when not connected
_, err = client.Get(ctx, "key")
require.Error(t, err)
assert.Contains(t, err.Error(), "client not connected")
// Test Delete when not connected
err = client.Delete(ctx, "key")
require.Error(t, err)
assert.Contains(t, err.Error(), "client not connected")
// Test HealthCheck when not connected
health, err := client.HealthCheck(ctx)
require.Error(t, err)
assert.Contains(t, err.Error(), "status down")
h, ok := health.(*Health)
require.True(t, ok)
assert.Equal(t, "DOWN", h.Status)
assert.Contains(t, h.Details["error"], "client not connected")
}
func Test_ClientGetItemNotFound(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
key := "non-existent-key"
expectedInput := &dynamodb.GetItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
// Return empty item (key not found)
expectedOutput := &dynamodb.GetItemOutput{
Item: map[string]types.AttributeValue{},
}
mockDB.EXPECT().GetItem(ctx, expectedInput, gomock.Any()).Return(expectedOutput, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "GET",
)
result, err := client.Get(ctx, key)
require.Error(t, err)
assert.Contains(t, err.Error(), "key not found")
assert.Empty(t, result)
}
func Test_ClientConnect(t *testing.T) {
deps := setupTest(t)
client := deps.client
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
// Set up expectations for Connect
mockLogger.EXPECT().Debugf("connecting to DynamoDB table %v in region %v", "test-table", "us-east-1")
mockMetrics.EXPECT().NewHistogram("app_dynamodb_duration_ms", "Response time of DynamoDB queries in milliseconds.", gomock.Any())
mockLogger.EXPECT().Infof("connected to DynamoDB table %v in region %v", "test-table", "us-east-1")
// Call Connect
client.Connect()
// Verify client is connected
assert.True(t, client.connected)
}
func Test_ClientUseLogger(t *testing.T) {
deps := setupTest(t)
client := deps.client
finish := deps.finish
defer finish()
mockLogger := NewMockLogger(gomock.NewController(t))
client.UseLogger(mockLogger)
assert.Equal(t, mockLogger, client.logger)
}
func Test_ClientUseMetrics(t *testing.T) {
deps := setupTest(t)
client := deps.client
finish := deps.finish
defer finish()
mockMetrics := NewMockMetrics(gomock.NewController(t))
client.UseMetrics(mockMetrics)
assert.Equal(t, mockMetrics, client.metrics)
}
func Test_ClientUseTracer(t *testing.T) {
deps := setupTest(t)
client := deps.client
finish := deps.finish
defer finish()
mockTracer := noop.NewTracerProvider().Tracer("test")
client.UseTracer(mockTracer)
assert.Equal(t, mockTracer, client.tracer)
}
func Test_New(t *testing.T) {
configs := Configs{
Table: "test-table",
Region: "us-east-1",
Endpoint: "http://localhost:8000",
PartitionKeyName: "custom-pk",
}
client := New(configs)
assert.Equal(t, "test-table", client.configs.Table)
assert.Equal(t, "us-east-1", client.configs.Region)
assert.Equal(t, "http://localhost:8000", client.configs.Endpoint)
assert.Equal(t, "custom-pk", client.configs.PartitionKeyName)
assert.False(t, client.connected)
}
func Test_NewWithDefaultPartitionKey(t *testing.T) {
configs := Configs{
Table: "test-table",
Region: "us-east-1",
}
client := New(configs)
assert.Equal(t, "pk", client.configs.PartitionKeyName) // Should default to "pk"
}
func Test_ClientSetWithTracer(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
// Add tracer
mockTracer := noop.NewTracerProvider().Tracer("test")
client.UseTracer(mockTracer)
key := "test-key"
value := "test-value"
expectedInput := &dynamodb.PutItemInput{
TableName: aws.String("test-table"),
Item: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
"value": &types.AttributeValueMemberS{Value: value},
},
}
mockDB.EXPECT().PutItem(ctx, expectedInput, gomock.Any()).Return(&dynamodb.PutItemOutput{}, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", client.configs.Table,
"operation", "SET",
)
require.NoError(t, client.Set(ctx, key, value))
}
func Test_ClientGetWithTracer(t *testing.T) {
deps := setupTest(t)
ctx := deps.ctx
client := deps.client
mockDB := deps.mockDB
mockLogger := deps.mockLogger
mockMetrics := deps.mockMetrics
finish := deps.finish
defer finish()
// Add tracer
mockTracer := noop.NewTracerProvider().Tracer("test")
client.UseTracer(mockTracer)
key := "test-key"
expectedValue := "test-value"
expectedInput := &dynamodb.GetItemInput{
TableName: aws.String("test-table"),
Key: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
},
}
expectedOutput := &dynamodb.GetItemOutput{
Item: map[string]types.AttributeValue{
"pk": &types.AttributeValueMemberS{Value: key},
"value": &types.AttributeValueMemberS{Value: expectedValue},
},
}
mockDB.EXPECT().GetItem(ctx, expectedInput, gomock.Any()).Return(expectedOutput, nil)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_dynamodb_duration_ms",
gomock.Any(),
"table", "test-table",
"operation", "GET",
)
result, err := client.Get(ctx, key)
require.NoError(t, err)
assert.Equal(t, expectedValue, result)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/kv-store/dynamodb
go 1.25.0
require (
github.com/aws/aws-sdk-go-v2 v1.41.0
github.com/aws/aws-sdk-go-v2/config v1.32.6
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
github.com/aws/smithy-go v1.24.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/go.sum
================================================
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=
github.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=
github.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5 h1:mSBrQCXMjEvLHsYyJVbN8QQlcITXwHEuu+8mX9e2bSo=
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.53.5/go.mod h1:eEuD0vTf9mIzsSjGBFWIaNQwtH5/mzViJOVQfnMY5DE=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16 h1:8g4OLy3zfNzLV20wXmZgx+QumI9WhWHnd4GCdvETxs4=
github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.16/go.mod h1:5a78jwLMs7BaesU0UIhLfVy2ZmOEgOy6ewYQXKTD37Q=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/logger.go
================================================
package dynamodb
import (
"fmt"
"io"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Info(args ...any)
Infof(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Type string `json:"type"`
Duration int64 `json:"duration"`
Key string `json:"key"`
Value string `json:"value,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;162m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s \n",
l.Type, "DYNMO", l.Duration, l.Key+" "+l.Value)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/metrics.go
================================================
package dynamodb
import (
"context"
)
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/mock_dynamodb.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: dynamo.go
//
// Generated by this command:
//
// mockgen -source dynamo.go -destination=mock_dynamodb.go -package=dynamo
//
// Package dynamo is a generated GoMock package.
package dynamodb
import (
context "context"
reflect "reflect"
dynamodb "github.com/aws/aws-sdk-go-v2/service/dynamodb"
gomock "go.uber.org/mock/gomock"
)
// MockdynamoDBInterface is a mock of dynamoDBInterface interface.
type MockdynamoDBInterface struct {
ctrl *gomock.Controller
recorder *MockdynamoDBInterfaceMockRecorder
isgomock struct{}
}
// MockdynamoDBInterfaceMockRecorder is the mock recorder for MockdynamoDBInterface.
type MockdynamoDBInterfaceMockRecorder struct {
mock *MockdynamoDBInterface
}
// NewMockdynamoDBInterface creates a new mock instance.
func NewMockdynamoDBInterface(ctrl *gomock.Controller) *MockdynamoDBInterface {
mock := &MockdynamoDBInterface{ctrl: ctrl}
mock.recorder = &MockdynamoDBInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockdynamoDBInterface) EXPECT() *MockdynamoDBInterfaceMockRecorder {
return m.recorder
}
// DeleteItem mocks base method.
func (m *MockdynamoDBInterface) DeleteItem(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteItem", varargs...)
ret0, _ := ret[0].(*dynamodb.DeleteItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteItem indicates an expected call of DeleteItem.
func (mr *MockdynamoDBInterfaceMockRecorder) DeleteItem(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItem", reflect.TypeOf((*MockdynamoDBInterface)(nil).DeleteItem), varargs...)
}
// DescribeTable mocks base method.
func (m *MockdynamoDBInterface) DescribeTable(ctx context.Context, params *dynamodb.DescribeTableInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DescribeTable", varargs...)
ret0, _ := ret[0].(*dynamodb.DescribeTableOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DescribeTable indicates an expected call of DescribeTable.
func (mr *MockdynamoDBInterfaceMockRecorder) DescribeTable(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTable", reflect.TypeOf((*MockdynamoDBInterface)(nil).DescribeTable), varargs...)
}
// GetItem mocks base method.
func (m *MockdynamoDBInterface) GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetItem", varargs...)
ret0, _ := ret[0].(*dynamodb.GetItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetItem indicates an expected call of GetItem.
func (mr *MockdynamoDBInterfaceMockRecorder) GetItem(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockdynamoDBInterface)(nil).GetItem), varargs...)
}
// PutItem mocks base method.
func (m *MockdynamoDBInterface) PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, params}
for _, a := range optFns {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PutItem", varargs...)
ret0, _ := ret[0].(*dynamodb.PutItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutItem indicates an expected call of PutItem.
func (mr *MockdynamoDBInterfaceMockRecorder) PutItem(ctx, params any, optFns ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, params}, optFns...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItem", reflect.TypeOf((*MockdynamoDBInterface)(nil).PutItem), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=dynamo
//
// Package dynamo is a generated GoMock package.
package dynamodb
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/dynamodb/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=dynamo
//
// Package dynamo is a generated GoMock package.
package dynamodb
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/kv-store/nats
go 1.25.0
require (
github.com/nats-io/nats.go v1.48.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/kv-store/nats/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/kv-store/nats/interface.go
================================================
package nats
import (
"time"
"github.com/nats-io/nats.go"
)
type KeyValue interface {
Get(key string) (entry nats.KeyValueEntry, err error)
GetRevision(key string, revision uint64) (entry nats.KeyValueEntry, err error)
Put(key string, value []byte) (revision uint64, err error)
PutString(key string, value string) (revision uint64, err error)
Create(key string, value []byte) (revision uint64, err error)
Update(key string, value []byte, last uint64) (revision uint64, err error)
Delete(key string, opts ...nats.DeleteOpt) error
Purge(key string, opts ...nats.DeleteOpt) error
Watch(keys string, opts ...nats.WatchOpt) (nats.KeyWatcher, error)
WatchAll(opts ...nats.WatchOpt) (nats.KeyWatcher, error)
WatchFiltered(keys []string, opts ...nats.WatchOpt) (nats.KeyWatcher, error)
Keys(opts ...nats.WatchOpt) ([]string, error)
ListKeys(opts ...nats.WatchOpt) (nats.KeyLister, error)
History(key string, opts ...nats.WatchOpt) ([]nats.KeyValueEntry, error)
Bucket() string
PurgeDeletes(opts ...nats.PurgeOpt) error
Status() (nats.KeyValueStatus, error)
}
type JetStream interface {
AccountInfo() (*nats.AccountInfo, error)
}
// MockKeyValueEntry for testing.
type MockKeyValueEntry struct {
value []byte
}
func (*MockKeyValueEntry) Bucket() string { return "" }
func (*MockKeyValueEntry) Key() string { return "" }
func (m *MockKeyValueEntry) Value() []byte { return m.value }
func (*MockKeyValueEntry) Revision() uint64 { return 0 }
func (*MockKeyValueEntry) Created() time.Time { return time.Time{} }
func (*MockKeyValueEntry) Delta() uint64 { return 0 }
func (*MockKeyValueEntry) Operation() nats.KeyValueOp { return 0 }
================================================
FILE: pkg/gofr/datasource/kv-store/nats/logger.go
================================================
package nats
import (
"fmt"
"io"
)
const (
uuidLength = 36
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Info(args ...any)
Infof(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Type string `json:"type"`
Duration int64 `json:"duration"`
Key string `json:"key"`
Value string `json:"value,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
var description string
switch l.Type {
case "GET":
description = fmt.Sprintf("Fetching record from bucket '%s' with ID '%s'", l.Value, l.Key)
case "SET":
if len(l.Key) == uuidLength {
description = fmt.Sprintf("Creating new record in bucket '%s' with ID '%s'", l.Value, l.Key)
} else {
description = fmt.Sprintf("Updating record with ID '%s' in bucket '%s'", l.Key, l.Value)
}
case "DELETE":
description = fmt.Sprintf("Deleting record from bucket '%s' with ID '%s'", l.Value, l.Key)
}
fmt.Fprintf(writer, "%-32s \u001B[38;5;162mNATS\u001B[0m %8dμs \u001B[38;5;8m%s\u001B[0m\n",
l.Type,
l.Duration,
description)
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/metrics.go
================================================
package nats
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=nats
//
// Package nats is a generated GoMock package.
package nats
import (
reflect "reflect"
nats "github.com/nats-io/nats.go"
gomock "go.uber.org/mock/gomock"
)
// MockKeyValue is a mock of KeyValue interface.
type MockKeyValue struct {
ctrl *gomock.Controller
recorder *MockKeyValueMockRecorder
isgomock struct{}
}
// MockKeyValueMockRecorder is the mock recorder for MockKeyValue.
type MockKeyValueMockRecorder struct {
mock *MockKeyValue
}
// NewMockKeyValue creates a new mock instance.
func NewMockKeyValue(ctrl *gomock.Controller) *MockKeyValue {
mock := &MockKeyValue{ctrl: ctrl}
mock.recorder = &MockKeyValueMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockKeyValue) EXPECT() *MockKeyValueMockRecorder {
return m.recorder
}
// Bucket mocks base method.
func (m *MockKeyValue) Bucket() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bucket")
ret0, _ := ret[0].(string)
return ret0
}
// Bucket indicates an expected call of Bucket.
func (mr *MockKeyValueMockRecorder) Bucket() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bucket", reflect.TypeOf((*MockKeyValue)(nil).Bucket))
}
// Create mocks base method.
func (m *MockKeyValue) Create(key string, value []byte) (uint64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Create", key, value)
ret0, _ := ret[0].(uint64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Create indicates an expected call of Create.
func (mr *MockKeyValueMockRecorder) Create(key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockKeyValue)(nil).Create), key, value)
}
// Delete mocks base method.
func (m *MockKeyValue) Delete(key string, opts ...nats.DeleteOpt) error {
m.ctrl.T.Helper()
varargs := []any{key}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Delete", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Delete indicates an expected call of Delete.
func (mr *MockKeyValueMockRecorder) Delete(key any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{key}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKeyValue)(nil).Delete), varargs...)
}
// Get mocks base method.
func (m *MockKeyValue) Get(key string) (nats.KeyValueEntry, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", key)
ret0, _ := ret[0].(nats.KeyValueEntry)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockKeyValueMockRecorder) Get(key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKeyValue)(nil).Get), key)
}
// GetRevision mocks base method.
func (m *MockKeyValue) GetRevision(key string, revision uint64) (nats.KeyValueEntry, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetRevision", key, revision)
ret0, _ := ret[0].(nats.KeyValueEntry)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetRevision indicates an expected call of GetRevision.
func (mr *MockKeyValueMockRecorder) GetRevision(key, revision any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRevision", reflect.TypeOf((*MockKeyValue)(nil).GetRevision), key, revision)
}
// History mocks base method.
func (m *MockKeyValue) History(key string, opts ...nats.WatchOpt) ([]nats.KeyValueEntry, error) {
m.ctrl.T.Helper()
varargs := []any{key}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "History", varargs...)
ret0, _ := ret[0].([]nats.KeyValueEntry)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// History indicates an expected call of History.
func (mr *MockKeyValueMockRecorder) History(key any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{key}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "History", reflect.TypeOf((*MockKeyValue)(nil).History), varargs...)
}
// Keys mocks base method.
func (m *MockKeyValue) Keys(opts ...nats.WatchOpt) ([]string, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Keys", varargs...)
ret0, _ := ret[0].([]string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Keys indicates an expected call of Keys.
func (mr *MockKeyValueMockRecorder) Keys(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Keys", reflect.TypeOf((*MockKeyValue)(nil).Keys), opts...)
}
// ListKeys mocks base method.
func (m *MockKeyValue) ListKeys(opts ...nats.WatchOpt) (nats.KeyLister, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ListKeys", varargs...)
ret0, _ := ret[0].(nats.KeyLister)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListKeys indicates an expected call of ListKeys.
func (mr *MockKeyValueMockRecorder) ListKeys(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListKeys", reflect.TypeOf((*MockKeyValue)(nil).ListKeys), opts...)
}
// Purge mocks base method.
func (m *MockKeyValue) Purge(key string, opts ...nats.DeleteOpt) error {
m.ctrl.T.Helper()
varargs := []any{key}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Purge", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Purge indicates an expected call of Purge.
func (mr *MockKeyValueMockRecorder) Purge(key any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{key}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Purge", reflect.TypeOf((*MockKeyValue)(nil).Purge), varargs...)
}
// PurgeDeletes mocks base method.
func (m *MockKeyValue) PurgeDeletes(opts ...nats.PurgeOpt) error {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PurgeDeletes", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// PurgeDeletes indicates an expected call of PurgeDeletes.
func (mr *MockKeyValueMockRecorder) PurgeDeletes(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgeDeletes", reflect.TypeOf((*MockKeyValue)(nil).PurgeDeletes), opts...)
}
// Put mocks base method.
func (m *MockKeyValue) Put(key string, value []byte) (uint64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Put", key, value)
ret0, _ := ret[0].(uint64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Put indicates an expected call of Put.
func (mr *MockKeyValueMockRecorder) Put(key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockKeyValue)(nil).Put), key, value)
}
// PutString mocks base method.
func (m *MockKeyValue) PutString(key, value string) (uint64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutString", key, value)
ret0, _ := ret[0].(uint64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutString indicates an expected call of PutString.
func (mr *MockKeyValueMockRecorder) PutString(key, value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutString", reflect.TypeOf((*MockKeyValue)(nil).PutString), key, value)
}
// Status mocks base method.
func (m *MockKeyValue) Status() (nats.KeyValueStatus, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Status")
ret0, _ := ret[0].(nats.KeyValueStatus)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Status indicates an expected call of Status.
func (mr *MockKeyValueMockRecorder) Status() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockKeyValue)(nil).Status))
}
// Update mocks base method.
func (m *MockKeyValue) Update(key string, value []byte, last uint64) (uint64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Update", key, value, last)
ret0, _ := ret[0].(uint64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Update indicates an expected call of Update.
func (mr *MockKeyValueMockRecorder) Update(key, value, last any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockKeyValue)(nil).Update), key, value, last)
}
// Watch mocks base method.
func (m *MockKeyValue) Watch(keys string, opts ...nats.WatchOpt) (nats.KeyWatcher, error) {
m.ctrl.T.Helper()
varargs := []any{keys}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Watch", varargs...)
ret0, _ := ret[0].(nats.KeyWatcher)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Watch indicates an expected call of Watch.
func (mr *MockKeyValueMockRecorder) Watch(keys any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{keys}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockKeyValue)(nil).Watch), varargs...)
}
// WatchAll mocks base method.
func (m *MockKeyValue) WatchAll(opts ...nats.WatchOpt) (nats.KeyWatcher, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "WatchAll", varargs...)
ret0, _ := ret[0].(nats.KeyWatcher)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WatchAll indicates an expected call of WatchAll.
func (mr *MockKeyValueMockRecorder) WatchAll(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchAll", reflect.TypeOf((*MockKeyValue)(nil).WatchAll), opts...)
}
// WatchFiltered mocks base method.
func (m *MockKeyValue) WatchFiltered(keys []string, opts ...nats.WatchOpt) (nats.KeyWatcher, error) {
m.ctrl.T.Helper()
varargs := []any{keys}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "WatchFiltered", varargs...)
ret0, _ := ret[0].(nats.KeyWatcher)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// WatchFiltered indicates an expected call of WatchFiltered.
func (mr *MockKeyValueMockRecorder) WatchFiltered(keys any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{keys}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchFiltered", reflect.TypeOf((*MockKeyValue)(nil).WatchFiltered), varargs...)
}
// MockJts is a mock of Jts interface.
type MockJts struct {
ctrl *gomock.Controller
recorder *MockJtsMockRecorder
isgomock struct{}
}
// MockJtsMockRecorder is the mock recorder for MockJts.
type MockJtsMockRecorder struct {
mock *MockJts
}
// NewMockJts creates a new mock instance.
func NewMockJts(ctrl *gomock.Controller) *MockJts {
mock := &MockJts{ctrl: ctrl}
mock.recorder = &MockJtsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJts) EXPECT() *MockJtsMockRecorder {
return m.recorder
}
// AccountInfo mocks base method.
func (m *MockJts) AccountInfo() (*nats.AccountInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccountInfo")
ret0, _ := ret[0].(*nats.AccountInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AccountInfo indicates an expected call of AccountInfo.
func (mr *MockJtsMockRecorder) AccountInfo() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountInfo", reflect.TypeOf((*MockJts)(nil).AccountInfo))
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=nats
//
// Package nats is a generated GoMock package.
package nats
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=nats
//
// Package nats is a generated GoMock package.
package nats
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/nats.go
================================================
package nats
import (
"context"
"errors"
"fmt"
"time"
"github.com/nats-io/nats.go"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
errStatusDown = errors.New("status down")
errKeyNotFound = errors.New("key not found")
)
type Configs struct {
Server string
Bucket string
}
type jetStream struct {
nats.JetStreamContext
}
func (j jetStream) AccountInfo() (*nats.AccountInfo, error) {
return j.JetStreamContext.AccountInfo()
}
type Client struct {
conn *nats.Conn
js JetStream
kv nats.KeyValue
configs *Configs
tracer trace.Tracer
metrics Metrics
logger Logger
}
// New creates a new NATS-KV client with the provided configuration.
func New(configs Configs) *Client {
return &Client{configs: &configs}
}
// UseLogger sets the logger for the NATS-KV client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the NATS-KV client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for NATS-KV client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Connect establishes a connection to NATS-KV and registers metrics using the provided configuration when the client is created.
func (c *Client) Connect() {
c.logger.Debugf("connecting to NATS-KV Store at %v with bucket %q", c.configs.Server, c.configs.Bucket)
natsBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_nats_kv_stats", "Response time of NATS KV operations in milliseconds.", natsBuckets...)
nc, err := nats.Connect(c.configs.Server)
if err != nil {
c.logger.Errorf("error while connecting to NATS: %v", err)
return
}
c.conn = nc
c.logger.Debug("connection to NATS successful")
js, err := nc.JetStream()
if err != nil {
c.logger.Errorf("error while initializing JetStream: %v", err)
return
}
c.js = jetStream{js}
c.logger.Debug("jetStream initialized successfully")
kv, err := js.CreateKeyValue(&nats.KeyValueConfig{
Bucket: c.configs.Bucket,
})
if err != nil {
c.logger.Errorf("error while creating/accessing KV bucket: %v", err)
return
}
c.kv = kv
c.logger.Infof("successfully connected to NATS-KV Store at %s:%s ", c.configs.Server, c.configs.Bucket)
}
func (c *Client) Get(ctx context.Context, key string) (string, error) {
span := c.addTrace(ctx, "get", key)
defer c.sendOperationStats(time.Now(), "GET", "get", span, key)
entry, err := c.kv.Get(key)
if err != nil {
if errors.Is(err, nats.ErrKeyNotFound) {
return "", fmt.Errorf("%w: %s", errKeyNotFound, key)
}
return "", fmt.Errorf("failed to get key: %w", err)
}
return string(entry.Value()), nil
}
func (c *Client) Set(ctx context.Context, key, value string) error {
span := c.addTrace(ctx, "set", key)
defer c.sendOperationStats(time.Now(), "SET", "set", span, key, value)
_, err := c.kv.Put(key, []byte(value))
if err != nil {
return fmt.Errorf("failed to set key-value pair: %w", err)
}
return nil
}
func (c *Client) Delete(ctx context.Context, key string) error {
span := c.addTrace(ctx, "delete", key)
defer c.sendOperationStats(time.Now(), "DELETE", "delete", span, key)
err := c.kv.Delete(key)
if err != nil {
if errors.Is(err, nats.ErrKeyNotFound) {
return fmt.Errorf("%w: %s", errKeyNotFound, key)
}
return fmt.Errorf("failed to delete key: %w", err)
}
return nil
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
start := time.Now()
span := c.addTrace(ctx, "healthcheck", c.configs.Bucket)
h := &Health{
Details: make(map[string]any),
}
h.Details["url"] = c.configs.Server
h.Details["bucket"] = c.configs.Bucket
_, err := c.js.AccountInfo()
if err != nil {
h.Status = "DOWN"
c.logger.Debug(&Log{
Type: "HEALTH CHECK",
Key: "health",
Value: fmt.Sprintf("Connection failed for bucket '%s' at '%s'", c.configs.Bucket, c.configs.Server),
Duration: time.Since(start).Microseconds(),
})
if span != nil {
span.End()
}
return h, errStatusDown
}
h.Status = "UP"
c.logger.Debug(&Log{
Type: "HEALTH CHECK",
Key: "health",
Value: fmt.Sprintf("Checking connection status for bucket '%s' at '%s'", c.configs.Bucket, c.configs.Server),
Duration: time.Since(start).Microseconds(),
})
if span != nil {
span.End()
}
return h, nil
}
func (c *Client) sendOperationStats(start time.Time, methodType, method string, span trace.Span, kv ...string) {
duration := time.Since(start)
var key string
if len(kv) > 0 {
key = kv[0]
}
c.logger.Debug(&Log{
Type: methodType,
Duration: duration.Microseconds(),
Key: key,
Value: c.configs.Bucket,
})
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("natskv.%v.duration(μs)", method), duration.Microseconds()))
}
c.metrics.RecordHistogram(context.Background(), "app_nats_kv_stats", float64(duration.Milliseconds()),
"bucket", c.configs.Bucket,
"operation", methodType)
}
func (c *Client) addTrace(ctx context.Context, method, key string) trace.Span {
if c.tracer != nil {
_, span := c.tracer.Start(ctx, fmt.Sprintf("natskv-%v", method))
span.SetAttributes(attribute.String("natskv.key", key))
span.SetAttributes(attribute.String("operation", method))
return span
}
return nil
}
================================================
FILE: pkg/gofr/datasource/kv-store/nats/nats_test.go
================================================
package nats
import (
"context"
"errors"
"fmt"
"testing"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
var (
errFailedToSet = errors.New("failed to set")
errConnectionFailed = errors.New("connection failed")
)
func Test_ClientSet(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockKV.EXPECT().
Put("test_key", []byte("test_value")).
Return(uint64(1), nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "SET",
).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
err := cl.Set(context.Background(), "test_key", "test_value")
require.NoError(t, err)
}
func Test_ClientSetError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockKV.EXPECT().
Put("test_key", []byte("test_value")).
Return(uint64(0), errFailedToSet)
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "SET",
).AnyTimes()
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
err := cl.Set(context.Background(), "test_key", "test_value")
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to set key-value pair")
}
func Test_ClientGet(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockEntry := &MockKeyValueEntry{value: []byte("test_value")}
mockKV.EXPECT().
Get("test_key").
Return(mockEntry, nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "GET",
).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
val, err := cl.Get(context.Background(), "test_key")
require.NoError(t, err)
assert.Equal(t, "test_value", val)
}
func Test_ClientGetError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockKV.EXPECT().
Get("nonexistent_key").
Return(nil, nats.ErrKeyNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "GET",
).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
val, err := cl.Get(context.Background(), "nonexistent_key")
require.Error(t, err)
assert.Empty(t, val)
assert.Contains(t, err.Error(), "key not found")
}
func Test_ClientDelete(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockKV.EXPECT().
Delete("test_key").
Return(nil)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "DELETE",
).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
err := cl.Delete(context.Background(), "test_key")
require.NoError(t, err)
}
func Test_ClientDeleteError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockKV := NewMockKeyValue(ctrl)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockKV.EXPECT().
Delete("nonexistent_key").
Return(nats.ErrKeyNotFound)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(),
"app_nats_kv_stats",
gomock.Any(),
"bucket", configs.Bucket,
"operation", "DELETE",
).AnyTimes()
cl := Client{
kv: mockKV,
logger: mockLogger,
metrics: mockMetrics,
configs: configs,
}
err := cl.Delete(context.Background(), "nonexistent_key")
require.Error(t, err)
assert.Contains(t, err.Error(), "key not found")
}
func Test_ClientHealthCheck(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJts(ctrl)
mockLogger := NewMockLogger(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockJS.EXPECT().
AccountInfo().
Return(&nats.AccountInfo{}, nil)
mockLogger.EXPECT().
Debug(gomock.Any()).
Do(func(log *Log) {
assert.Equal(t, "HEALTH CHECK", log.Type)
assert.Equal(t, "health", log.Key)
assert.Equal(t, fmt.Sprintf("Checking connection status for bucket '%s' at '%s'",
configs.Bucket, configs.Server), log.Value)
}).
Times(1)
cl := Client{
js: mockJS,
logger: mockLogger,
configs: configs,
}
val, err := cl.HealthCheck(context.Background())
require.NoError(t, err)
health := val.(*Health)
assert.Equal(t, "UP", health.Status)
assert.Equal(t, configs.Server, health.Details["url"])
assert.Equal(t, configs.Bucket, health.Details["bucket"])
}
func Test_ClientHealthCheckFailure(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJts(ctrl)
mockLogger := NewMockLogger(ctrl)
configs := &Configs{
Server: "nats://localhost:4222",
Bucket: "test_bucket",
}
mockJS.EXPECT().
AccountInfo().
Return(nil, errConnectionFailed)
// Mock the Debug call for failed health check
mockLogger.EXPECT().
Debug(gomock.Any()).
Do(func(log *Log) {
assert.Equal(t, "HEALTH CHECK", log.Type)
assert.Equal(t, "health", log.Key)
assert.Equal(t, fmt.Sprintf("Connection failed for bucket '%s' at '%s'",
configs.Bucket, configs.Server), log.Value)
}).
Times(1)
cl := Client{
js: mockJS,
logger: mockLogger,
configs: configs,
}
val, err := cl.HealthCheck(context.Background())
require.Error(t, err)
require.Equal(t, errStatusDown, err)
health := val.(*Health)
assert.Equal(t, "DOWN", health.Status)
assert.Equal(t, configs.Server, health.Details["url"])
assert.Equal(t, configs.Bucket, health.Details["bucket"])
}
================================================
FILE: pkg/gofr/datasource/logger.go
================================================
package datasource
// Logger interface is used by datasource packages to log information about query execution.
// Developer Notes: Note that it's a reduced version of logging.Logger interface. We are not using that package to
// ensure that datasource package is not dependent on logging package. That way logging package should be easily able
// to import datasource package and provide a different "pretty" version for different log types defined here while
// avoiding the cyclical import issue. Idiomatically, interfaces should be defined by packages who are using it; unlike
// other languages. Also - accept interfaces, return concrete types.
type Logger interface {
Debug(args ...any)
Debugf(format string, args ...any)
Info(args ...any)
Infof(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Warn(args ...any)
Warnf(format string, args ...any)
}
================================================
FILE: pkg/gofr/datasource/mongo/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/mongo
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
go.mongodb.org/mongo-driver v1.17.9
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/text v0.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/mongo/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/mongo/logger.go
================================================
package mongo
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
type QueryLog struct {
Query string `json:"query"`
Duration int64 `json:"duration"`
Collection string `json:"collection,omitempty"`
Filter any `json:"filter,omitempty"`
ID any `json:"id,omitempty"`
Update any `json:"update,omitempty"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
if ql.Filter == nil {
ql.Filter = ""
}
if ql.ID == nil {
ql.ID = ""
}
if ql.Update == nil {
ql.Update = ""
}
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n",
clean(ql.Query), "MONGO", ql.Duration,
clean(strings.Join([]string{ql.Collection, fmt.Sprint(ql.Filter), fmt.Sprint(ql.ID), fmt.Sprint(ql.Update)}, " ")))
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/mongo/logger_test.go
================================================
package mongo
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoggingDataPresent(t *testing.T) {
queryLog := QueryLog{
Query: "find",
Duration: 12345,
Collection: "users",
Filter: map[string]string{"name": "John"},
ID: "123",
Update: map[string]string{"$set": "Doe"},
}
expected := "name:John"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
func TestLoggingEmptyData(t *testing.T) {
queryLog := QueryLog{
Query: "insert",
Duration: 6789,
}
expected := "name:John"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.NotContains(t, buf.String(), expected)
}
================================================
FILE: pkg/gofr/datasource/mongo/metrics.go
================================================
package mongo
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/mongo/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=mongo
//
// Package mongo is a generated GoMock package.
package mongo
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/mongo/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=mongo
//
// Package mongo is a generated GoMock package.
package mongo
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/mongo/mongo.go
================================================
package mongo
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strconv"
"time"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type Client struct {
*mongo.Database
uri string
database string
logger Logger
metrics Metrics
config *Config
tracer trace.Tracer
}
type Config struct {
Host string
User string
Password string
Port int
Database string
// Deprecated Provide Host User Password Port Instead and driver will generate the URI
URI string
ConnectionTimeout time.Duration
}
const defaultTimeout = 5 * time.Second
var (
errStatusDown = errors.New("status down")
errMissingField = errors.New("missing required field in config")
errIncorrectURI = errors.New("incorrect URI for MongoDB")
errParseHost = errors.New("failed to parse host from MongoDB URI")
)
/*
Developer Note: We could have accepted logger and metrics as part of the factory function `New`, but when mongo driver is
initialized in GoFr, We want to ensure that the user need not to provides logger and metrics and then connect to the database,
i.e. by default observability features gets initialized when used with GoFr.
*/
// New initializes MongoDB driver with the provided configuration.
// The Connect method must be called to establish a connection to MongoDB.
// Usage:
// client := New(config)
// client.UseLogger(loggerInstance)
// client.UseMetrics(metricsInstance)
// client.Connect().
//
//nolint:gocritic // Configs do not need to be passed by reference
func New(c Config) *Client {
return &Client{config: &c}
}
// UseLogger sets the logger for the MongoDB client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the MongoDB client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the MongoDB client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// Connect establishes a connection to MongoDB and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
uri, host, err := generateMongoURI(c.config)
if err != nil {
c.logger.Errorf("error generating MongoDB URI: %v", err)
return
}
c.logger.Debugf("connecting to MongoDB at %v to database %v", c.config.Host, c.config.Database)
timeout := c.config.ConnectionTimeout
if timeout == 0 {
timeout = defaultTimeout
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
m, err := mongo.Connect(ctx, options.Client().ApplyURI(uri))
if err != nil {
c.logger.Errorf("error while connecting to MongoDB, err:%v", err)
return
}
if err = m.Ping(ctx, nil); err != nil {
c.logger.Errorf("could not connect to MongoDB at %v due to err: %v", host, err)
return
}
mongoBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_mongo_stats", "Response time of MongoDB queries in milliseconds.", mongoBuckets...)
c.Database = m.Database(c.config.Database)
c.logger.Logf("connected to MongoDB at %v to database %v", host, c.config.Database)
}
func generateMongoURI(config *Config) (uri, host string, err error) {
if config.URI != "" {
host, err = getDBHost(config.URI)
if err != nil {
return "", "", err
}
return config.URI, host, nil
}
switch {
case config.Host == "":
return "", "", fmt.Errorf("%w: host is empty", errMissingField)
case config.Port == 0:
return "", "", fmt.Errorf("%w: port is empty", errMissingField)
case config.Database == "":
return "", "", fmt.Errorf("%w: database is empty", errMissingField)
}
u := &url.URL{
Scheme: "mongodb",
Host: net.JoinHostPort(config.Host, strconv.Itoa(config.Port)),
Path: "/" + url.PathEscape(config.Database),
}
if config.User != "" && config.Password != "" {
u.User = url.UserPassword(url.QueryEscape(config.User), url.QueryEscape(config.Password))
}
q := u.Query()
q.Set("authSource", "admin")
u.RawQuery = q.Encode()
return u.String(), u.Hostname(), nil
}
func getDBHost(uri string) (host string, err error) {
parsedURL, err := url.ParseRequestURI(uri)
if err != nil {
return "", err
}
if parsedURL.Scheme != "mongodb" {
return "", errIncorrectURI
}
if parsedURL.Hostname() == "" {
return "", errParseHost
}
return parsedURL.Hostname(), nil
}
// InsertOne inserts a single document into the specified collection.
func (c *Client) InsertOne(ctx context.Context, collection string, document any) (any, error) {
tracerCtx, span := c.addTrace(ctx, "insertOne", collection)
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 []any) ([]any, error) {
tracerCtx, span := c.addTrace(ctx, "insertMany", collection)
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 any) error {
tracerCtx, span := c.addTrace(ctx, "find", collection)
cur, err := c.Database.Collection(collection).Find(tracerCtx, filter)
if err != nil {
return err
}
defer cur.Close(ctx)
if err := cur.All(ctx, results); err != nil {
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 any) error {
tracerCtx, span := c.addTrace(ctx, "findOne", collection)
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 any) (int64, error) {
tracerCtx, span := c.addTrace(ctx, "updateByID", collection)
res, err := c.Database.Collection(collection).UpdateByID(tracerCtx, 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 any) error {
tracerCtx, span := c.addTrace(ctx, "updateOne", collection)
_, err := c.Database.Collection(collection).UpdateOne(tracerCtx, 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 any) (int64, error) {
tracerCtx, span := c.addTrace(ctx, "updateMany", collection)
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 any) (int64, error) {
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 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 any) (int64, error) {
tracerCtx, span := c.addTrace(ctx, "deleteOne", collection)
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 any) (int64, error) {
tracerCtx, span := c.addTrace(ctx, "deleteMany", collection)
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 {
tracerCtx, span := c.addTrace(ctx, "drop", collection)
err := c.Database.Collection(collection).Drop(tracerCtx)
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 {
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 err
}
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
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 {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// HealthCheck checks the health of the MongoDB client by pinging the database.
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.uri
h.Details["database"] = c.database
err := c.Database.Client().Ping(ctx, readpref.Primary())
if err != nil {
h.Status = "DOWN"
return &h, errStatusDown
}
h.Status = "UP"
return &h, nil
}
func (c *Client) StartSession() (any, error) {
defer c.sendOperationStats(&QueryLog{Query: "startSession"}, time.Now(), "", nil)
s, err := c.Client().StartSession()
ses := &session{s}
return ses, err
}
type session struct {
mongo.Session
}
func (s *session) StartTransaction() error {
return s.Session.StartTransaction()
}
type Transaction interface {
StartTransaction() error
AbortTransaction(context.Context) error
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
}
================================================
FILE: pkg/gofr/datasource/mongo/mongo_test.go
================================================
package mongo
import (
"context"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"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"
)
func Test_NewMongoClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
logger.EXPECT().Debugf(gomock.Any(), gomock.Any())
logger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any())
client := New(Config{Database: "test", Host: "localhost", Port: 27017, User: "admin", ConnectionTimeout: 1 * time.Second})
client.Database = &mongo.Database{}
client.UseLogger(logger)
client.UseMetrics(metrics)
client.Connect()
assert.NotNil(t, client)
}
func TestGenerateMongoURI(t *testing.T) {
tests := []struct {
name string
config Config
expectedURI string
expectedHost string
expectedError string
}{
{
name: "Valid Config",
config: Config{
User: "admin",
Password: "p@##word:",
Host: "localhost",
Port: 27017,
Database: "mydb",
},
expectedURI: "mongodb://admin:p%2540%2523%2523word%253A@localhost:27017/mydb?authSource=admin",
expectedHost: "localhost",
expectedError: "",
},
{
name: "Valid Config without authentication",
config: Config{
Host: "localhost",
Port: 27017,
Database: "mydb",
},
expectedURI: "mongodb://localhost:27017/mydb?authSource=admin",
expectedHost: "localhost",
expectedError: "",
},
{
name: "Predefined URI",
config: Config{
URI: "mongodb://admin:password@localhost:27017/mydb?authSource=admin",
},
expectedURI: "mongodb://admin:password@localhost:27017/mydb?authSource=admin",
expectedHost: "localhost",
expectedError: "",
},
{
name: "Empty Host",
config: Config{
User: "admin",
Password: "password",
Port: 27017,
Database: "mydb",
},
expectedURI: "",
expectedHost: "",
expectedError: "missing required field in config: host is empty",
},
{
name: "Invalid Port",
config: Config{
User: "admin",
Password: "password",
Host: "localhost",
Database: "mydb",
},
expectedURI: "",
expectedHost: "",
expectedError: "missing required field in config: port is empty",
},
{
name: "Empty Database",
config: Config{
User: "admin",
Password: "password",
Host: "localhost",
Port: 27017,
},
expectedURI: "",
expectedHost: "",
expectedError: "missing required field in config: database is empty",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
client := Client{config: &test.config}
uri, host, err := generateMongoURI(client.config)
assert.Equal(t, test.expectedURI, uri, "Unexpected URI")
assert.Equal(t, test.expectedHost, host, "Unexpected Host")
if test.expectedError != "" {
assert.EqualError(t, err, test.expectedError, "Unexpected error message")
} else {
assert.NoError(t, err, "Expected no error but got one")
}
})
}
}
func TestGetDBHost(t *testing.T) {
tests := []struct {
name string
uri string
expected string
expectedErr string
}{
{
name: "Valid URI with host and port",
uri: "mongodb://username:password@hostname:27017/database?authSource=admin",
expected: "hostname",
expectedErr: "",
},
{
name: "Valid URI with IP address as host",
uri: "mongodb://username:password@192.168.1.1:27017/database?authSource=admin",
expected: "192.168.1.1",
expectedErr: "",
},
{
name: "Invalid URI with no host",
uri: "mongodb://username:password@:27017/database?authSource=admin",
expected: "",
expectedErr: "failed to parse host from MongoDB URI",
},
{
name: "Empty URI",
uri: "",
expected: "",
expectedErr: "parse \"\": empty url",
},
{
name: "Malformed URI",
uri: "mongodb:/username:password@hostname:27017/database?authSource=admin",
expected: "",
expectedErr: "failed to parse host from MongoDB URI",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
host, err := getDBHost(tt.uri)
assert.Equal(t, tt.expected, host, "Test case: %s", tt.name)
if tt.expectedErr == "" {
assert.NoError(t, err, "Test case: %s", tt.name)
} else {
assert.EqualError(t, err, tt.expectedErr, "Test case: %s", tt.name)
}
})
}
}
func Test_NewMongoClientError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
logger.EXPECT().Errorf("error generating MongoDB URI: %v", gomock.Any())
client := New(Config{Host: "mongo", Database: "test"})
client.UseLogger(logger)
client.UseMetrics(metrics)
client.Connect()
assert.Nil(t, client.Database)
}
func Test_InsertCommands(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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)
logger.EXPECT().Debug(gomock.Any()).Times(3)
cl.logger = logger
mt.Run("insertOneSuccess", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
doc := map[string]any{"name": "Aryan"}
resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc)
assert.NotNil(t, resp)
assert.NoError(t, err)
})
mt.Run("insertOneError", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{
Index: 1,
Code: 11000,
Message: "duplicate key error",
}))
doc := map[string]any{"name": "Aryan"}
resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc)
assert.Nil(t, resp)
assert.Error(t, err)
})
mt.Run("insertManySuccess", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
doc := map[string]any{"name": "Aryan"}
resp, err := cl.InsertMany(context.Background(), mt.Coll.Name(), []any{doc, doc})
assert.NotNil(t, resp)
require.NoError(t, err)
})
mt.Run("insertManyError", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{
Index: 1,
Code: 11000,
Message: "duplicate key error",
}))
doc := map[string]any{"name": "Aryan"}
resp, err := cl.InsertMany(context.Background(), mt.Coll.Name(), []any{doc, doc})
assert.Nil(t, resp)
require.Error(t, err)
})
}
func Test_CreateCollection(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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())
logger.EXPECT().Debug(gomock.Any())
cl.logger = logger
mt.Run("createCollection", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
err := cl.CreateCollection(context.Background(), mt.Coll.Name())
require.NoError(t, err)
})
}
func Test_FindMultipleCommands(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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())
logger.EXPECT().Debug(gomock.Any())
cl.logger = logger
mt.Run("FindSuccess", func(mt *mtest.T) {
cl.Database = mt.DB
var foundDocuments []any
id1 := primitive.NewObjectID()
first := mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{
{Key: "_id", Value: id1},
{Key: "name", Value: "john"},
{Key: "email", Value: "john.doe@test.com"},
})
killCursors := mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch)
mt.AddMockResponses(first, killCursors)
mt.AddMockResponses(first)
err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments)
assert.NoError(t, err, "Unexpected error during Find operation")
})
mt.Run("FindCursorError", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, nil)
require.ErrorContains(t, err, "database response does not contain a cursor")
})
mt.Run("FindCursorParseError", func(mt *mtest.T) {
cl.Database = mt.DB
var foundDocuments []any
id1 := primitive.NewObjectID()
first := mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{
{Key: "_id", Value: id1},
{Key: "name", Value: "john"},
{Key: "email", Value: "john.doe@test.com"},
})
mt.AddMockResponses(first)
mt.AddMockResponses(first)
err := cl.Find(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments)
require.ErrorContains(t, err, "cursor.nextBatch should be an array but is a BSON invalid")
})
}
func Test_FindOneCommands(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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())
logger.EXPECT().Debug(gomock.Any())
cl.logger = logger
mt.Run("FindOneSuccess", func(mt *mtest.T) {
cl.Database = mt.DB
type user struct {
ID primitive.ObjectID
Name string
Email string
}
var foundDocuments user
expectedUser := user{
ID: primitive.NewObjectID(),
Name: "john",
Email: "john.doe@test.com",
}
mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, bson.D{
{Key: "_id", Value: expectedUser.ID},
{Key: "name", Value: expectedUser.Name},
{Key: "email", Value: expectedUser.Email},
}))
err := cl.FindOne(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments)
assert.Equal(t, expectedUser.Name, foundDocuments.Name)
assert.NoError(t, err)
})
mt.Run("FindOneError", func(mt *mtest.T) {
cl.Database = mt.DB
type user struct {
ID primitive.ObjectID
Name string
Email string
}
var foundDocuments user
mt.AddMockResponses(mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch))
err := cl.FindOne(context.Background(), mt.Coll.Name(), bson.D{{}}, &foundDocuments)
assert.Error(t, err)
})
}
func Test_UpdateCommands(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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)
logger.EXPECT().Debug(gomock.Any()).Times(3)
cl.logger = logger
mt.Run("updateByID", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
// Create a document to insert
resp, err := cl.UpdateByID(context.Background(), mt.Coll.Name(), "1", bson.M{"$set": bson.M{"name": "test"}})
assert.NotNil(t, resp)
assert.NoError(t, err)
})
mt.Run("updateOne", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
// Create a document to insert
err := cl.UpdateOne(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}}, bson.M{"$set": bson.M{"name": "testing"}})
assert.NoError(t, err)
})
mt.Run("updateMany", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
// Create a document to insert
_, err := cl.UpdateMany(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}},
bson.M{"$set": bson.M{"name": "testing"}})
assert.NoError(t, err)
})
}
func Test_CountDocuments(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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())
logger.EXPECT().Debug(gomock.Any())
cl.logger = logger
mt.Run("countDocuments", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
mt.AddMockResponses(mtest.CreateCursorResponse(1, "test.restaurants", mtest.FirstBatch, bson.D{{Key: "n", Value: 1}}))
// For count to work, mongo needs an index. So we need to create that. Index view should contain a key. Value does not matter
indexView := mt.Coll.Indexes()
_, err := indexView.CreateOne(context.Background(), mongo.IndexModel{
Keys: bson.D{{Key: "x", Value: 1}},
})
require.NoError(mt, err, "CreateOne error for index: %v", err)
resp, err := cl.CountDocuments(context.Background(), mt.Coll.Name(), bson.D{{Key: "name", Value: "test"}})
assert.Equal(t, int64(1), resp)
assert.NoError(t, err)
})
}
func Test_DeleteCommands(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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)
logger.EXPECT().Debug(gomock.Any()).Times(2)
cl.logger = logger
mt.Run("DeleteOne", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
resp, err := cl.DeleteOne(context.Background(), mt.Coll.Name(), bson.D{{}})
assert.Equal(t, int64(0), resp)
assert.NoError(t, err)
})
mt.Run("DeleteOneError", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{
Index: 1,
Code: 11000,
Message: "duplicate key error",
}))
resp, err := cl.DeleteOne(context.Background(), mt.Coll.Name(), bson.D{{}})
assert.Equal(t, int64(0), resp)
assert.Error(t, err)
})
mt.Run("DeleteMany", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
resp, err := cl.DeleteMany(context.Background(), mt.Coll.Name(), bson.D{{}})
assert.Equal(t, int64(0), resp)
assert.NoError(t, err)
})
mt.Run("DeleteManyError", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{
Index: 1,
Code: 11000,
Message: "duplicate key error",
}))
resp, err := cl.DeleteMany(context.Background(), mt.Coll.Name(), bson.D{{}})
assert.Equal(t, int64(0), resp)
assert.Error(t, err)
})
}
func Test_Drop(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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())
logger.EXPECT().Debug(gomock.Any())
cl.logger = logger
mt.Run("Drop", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
err := cl.Drop(context.Background(), mt.Coll.Name())
assert.NoError(t, err)
})
}
func TestClient_StartSession(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
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",
gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).Times(2)
logger.EXPECT().Debug(gomock.Any()).Times(2)
cl.logger = logger
mt.Run("StartSessionCommitTransactionSuccess", func(mt *mtest.T) {
cl.Database = mt.DB
// Add mock responses if necessary
mt.AddMockResponses(mtest.CreateSuccessResponse())
// Call the StartSession method
sess, err := cl.StartSession()
ses, ok := sess.(Transaction)
if ok {
err = ses.StartTransaction()
}
require.NoError(t, err)
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
doc := map[string]any{"name": "Aryan"}
resp, err := cl.InsertOne(context.Background(), mt.Coll.Name(), doc)
assert.NotNil(t, resp)
require.NoError(t, err)
err = ses.CommitTransaction(context.Background())
require.NoError(t, err)
ses.EndSession(context.Background())
// Assert that there was no error
require.NoError(t, err)
})
}
func Test_HealthCheck(t *testing.T) {
// Create a connected client using the mock database
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
ctrl := gomock.NewController(t)
defer ctrl.Finish()
metrics := NewMockMetrics(ctrl)
logger := NewMockLogger(ctrl)
cl := Client{metrics: metrics}
cl.logger = logger
mt.Run("HealthCheck Success", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateSuccessResponse())
resp, err := cl.HealthCheck(context.Background())
require.NoError(t, err)
assert.Contains(t, fmt.Sprint(resp), "UP")
})
mt.Run("HealthCheck Error", func(mt *mtest.T) {
cl.Database = mt.DB
mt.AddMockResponses(mtest.CreateWriteErrorsResponse(mtest.WriteError{
Index: 1,
Code: 11000,
Message: "duplicate key error",
}))
resp, err := cl.HealthCheck(context.Background())
require.ErrorIs(t, err, errStatusDown)
assert.Contains(t, fmt.Sprint(resp), "DOWN")
})
}
================================================
FILE: pkg/gofr/datasource/opentsdb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/opentsdb
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/opentsdb/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/opentsdb/interface.go
================================================
package opentsdb
import (
"context"
"net"
"net/http"
"time"
)
//nolint:unused // connection interface defines all the methods to mock the connection returned while healthcheck implementation.
type connection interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() net.Addr
RemoteAddr() net.Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
// httpClient is an interface that wraps the http.Client's Do method.
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// Response defines the common behaviors all the specific response for
// different rest-apis should obey.
// Currently, it is an abstraction used in Client.sendRequest()
// to stored the different kinds of response contents for all the rest-apis.
type response interface {
// getCustomParser can be used to retrieve a custom-defined parser.
// Returning nil means current specific Response instance doesn't
// need a custom-defined parse process, and just uses the default
// json unmarshal method to parse the contents of the http response.
getCustomParser(Logger) func(respCnt []byte) error
}
// Logger interface is used by opentsdb package to log information about request execution.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Logf(pattern string, args ...any)
Log(args ...any)
Errorf(pattern string, args ...any)
Fatal(args ...any)
}
type Metrics interface {
NewCounter(name, desc string)
NewHistogram(name, desc string, buckets ...float64)
IncrementCounter(ctx context.Context, name string, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/opentsdb/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=opentsdb
//
// Package opentsdb is a generated GoMock package.
package opentsdb
import (
context "context"
net "net"
http "net/http"
reflect "reflect"
time "time"
gomock "go.uber.org/mock/gomock"
)
// MockhttpClient is a mock of httpClient interface.
type MockhttpClient struct {
ctrl *gomock.Controller
recorder *MockhttpClientMockRecorder
}
// MockhttpClientMockRecorder is the mock recorder for MockhttpClient.
type MockhttpClientMockRecorder struct {
mock *MockhttpClient
}
// NewMockhttpClient creates a new mock instance.
func NewMockhttpClient(ctrl *gomock.Controller) *MockhttpClient {
mock := &MockhttpClient{ctrl: ctrl}
mock.recorder = &MockhttpClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockhttpClient) EXPECT() *MockhttpClientMockRecorder {
return m.recorder
}
// Do mocks base method.
func (m *MockhttpClient) Do(req *http.Request) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Do", req)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Do indicates an expected call of Do.
func (mr *MockhttpClientMockRecorder) Do(req any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockhttpClient)(nil).Do), req)
}
// Mockconnection is a mock of connection interface.
type Mockconnection struct {
ctrl *gomock.Controller
recorder *MockconnectionMockRecorder
}
// MockconnectionMockRecorder is the mock recorder for Mockconnection.
type MockconnectionMockRecorder struct {
mock *Mockconnection
}
// NewMockconnection creates a new mock instance.
func NewMockconnection(ctrl *gomock.Controller) *Mockconnection {
mock := &Mockconnection{ctrl: ctrl}
mock.recorder = &MockconnectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockconnection) EXPECT() *MockconnectionMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *Mockconnection) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockconnectionMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*Mockconnection)(nil).Close))
}
// LocalAddr mocks base method.
func (m *Mockconnection) LocalAddr() net.Addr {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "LocalAddr")
ret0, _ := ret[0].(net.Addr)
return ret0
}
// LocalAddr indicates an expected call of LocalAddr.
func (mr *MockconnectionMockRecorder) LocalAddr() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*Mockconnection)(nil).LocalAddr))
}
// Read mocks base method.
func (m *Mockconnection) Read(b []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", b)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockconnectionMockRecorder) Read(b any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Mockconnection)(nil).Read), b)
}
// RemoteAddr mocks base method.
func (m *Mockconnection) RemoteAddr() net.Addr {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoteAddr")
ret0, _ := ret[0].(net.Addr)
return ret0
}
// RemoteAddr indicates an expected call of RemoteAddr.
func (mr *MockconnectionMockRecorder) RemoteAddr() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*Mockconnection)(nil).RemoteAddr))
}
// SetDeadline mocks base method.
func (m *Mockconnection) SetDeadline(t time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDeadline", t)
ret0, _ := ret[0].(error)
return ret0
}
// SetDeadline indicates an expected call of SetDeadline.
func (mr *MockconnectionMockRecorder) SetDeadline(t any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*Mockconnection)(nil).SetDeadline), t)
}
// SetReadDeadline mocks base method.
func (m *Mockconnection) SetReadDeadline(t time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetReadDeadline", t)
ret0, _ := ret[0].(error)
return ret0
}
// SetReadDeadline indicates an expected call of SetReadDeadline.
func (mr *MockconnectionMockRecorder) SetReadDeadline(t any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*Mockconnection)(nil).SetReadDeadline), t)
}
// SetWriteDeadline mocks base method.
func (m *Mockconnection) SetWriteDeadline(t time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetWriteDeadline", t)
ret0, _ := ret[0].(error)
return ret0
}
// SetWriteDeadline indicates an expected call of SetWriteDeadline.
func (mr *MockconnectionMockRecorder) SetWriteDeadline(t any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*Mockconnection)(nil).SetWriteDeadline), t)
}
// Write mocks base method.
func (m *Mockconnection) Write(b []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", b)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Write indicates an expected call of Write.
func (mr *MockconnectionMockRecorder) Write(b any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Mockconnection)(nil).Write), b)
}
// Mockresponse is a mock of response interface.
type Mockresponse struct {
ctrl *gomock.Controller
recorder *MockresponseMockRecorder
}
// MockresponseMockRecorder is the mock recorder for Mockresponse.
type MockresponseMockRecorder struct {
mock *Mockresponse
}
// NewMockresponse creates a new mock instance.
func NewMockresponse(ctrl *gomock.Controller) *Mockresponse {
mock := &Mockresponse{ctrl: ctrl}
mock.recorder = &MockresponseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockresponse) EXPECT() *MockresponseMockRecorder {
return m.recorder
}
// getCustomParser mocks base method.
func (m *Mockresponse) getCustomParser(arg0 Logger) func([]byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "getCustomParser", arg0)
ret0, _ := ret[0].(func([]byte) error)
return ret0
}
// getCustomParser indicates an expected call of getCustomParser.
func (mr *MockresponseMockRecorder) getCustomParser(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getCustomParser", reflect.TypeOf((*Mockresponse)(nil).getCustomParser), arg0)
}
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Fatal mocks base method.
func (m *MockLogger) Fatal(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Fatal", varargs...)
}
// Fatal indicates an expected call of Fatal.
func (mr *MockLoggerMockRecorder) Fatal(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockLogger)(nil).Fatal), args...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// NewCounter mocks base method.
func (m *MockMetrics) NewCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewCounter", name, desc)
}
// NewCounter indicates an expected call of NewCounter.
func (mr *MockMetricsMockRecorder) NewCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), name, desc)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/opentsdb/observability.go
================================================
package opentsdb
import (
"context"
"fmt"
"io"
"regexp"
"strings"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// QueryLog handles logging with different levels.
type QueryLog struct {
Operation string `json:"operation"`
Duration int64 `json:"duration"`
Status *string `json:"status"`
Message *string `json:"message,omitempty"`
}
var regexpSpaces = regexp.MustCompile(`\s+`)
func clean(query *string) string {
if query == nil {
return ""
}
return strings.TrimSpace(regexpSpaces.ReplaceAllString(*query, " "))
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;148m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-10s \u001B[0m %-48s \n",
clean(&ql.Operation), "OPENTSDB", ql.Duration, clean(ql.Status), clean(ql.Message))
}
func sendOperationStats(
ctx context.Context,
logger Logger,
metrics Metrics,
host string,
start time.Time,
operation string,
status, message *string,
span trace.Span,
) {
duration := time.Since(start)
logger.Debug(&QueryLog{
Operation: operation,
Status: status,
Duration: duration.Microseconds(),
Message: message,
})
if span != nil {
span.SetAttributes(attribute.Int64(fmt.Sprintf("opentsdb.%v.duration", operation), duration.Microseconds()))
span.End()
}
if metrics != nil {
statusLabel := ""
if status != nil {
statusLabel = *status
}
labels := []string{"operation", operation}
if statusLabel != "" {
labels = append(labels, "status", statusLabel)
}
if host != "" {
labels = append(labels, "host", host)
}
metrics.RecordHistogram(ctx, opentsdbOperationDurationName, float64(duration.Milliseconds()), labels...)
metrics.IncrementCounter(ctx, opentsdbOperationTotalName, labels...)
}
}
func addTracer(ctx context.Context, tracer trace.Tracer, operation, typeName string) trace.Span {
if tracer == nil {
return nil
}
_, span := tracer.Start(ctx, fmt.Sprintf("opentsdb-%s", operation))
span.SetAttributes(
attribute.String(fmt.Sprintf("opentsdb-%s.operation", typeName), operation),
)
return span
}
func (c *Client) addTrace(ctx context.Context, operation string) trace.Span {
return addTracer(ctx, c.tracer, operation, "Client")
}
func (*AggregatorsResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "AggregatorRes")
}
func (*AnnotationResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "AnnotationRes")
}
func (*QueryResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "QueryResponse")
}
func (*QueryRespItem) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "QueryRespItem")
}
func (*QueryParam) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "QueryParam")
}
func (*QueryLastParam) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "QueryLastParam")
}
func (*QueryLastResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "QueryLastResponse")
}
func (*VersionResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "VersionResponse")
}
func (*PutResponse) addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span {
return addTracer(ctx, tracer, operation, "PutResponse")
}
================================================
FILE: pkg/gofr/datasource/opentsdb/opentsdb.go
================================================
// Package opentsdb provides a client implementation for interacting with OpenTSDB
// via its REST API.
package opentsdb
import (
"bytes"
"context"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// Predefined static errors.
var (
errInvalidResponseType = errors.New("invalid response type")
errInvalidQueryParam = errors.New("invalid query parameters")
errInvalidParam = errors.New("invalid parameter type")
)
const (
statusFailed = "FAIL"
statusSuccess = "SUCCESS"
defaultDialTime = 5 * time.Second // Default time for establishing TCP connections.
connectionTimeout = 30 * time.Second // Timeout for keeping connections alive.
// API paths for OpenTSDB endpoints.
putPath = "/api/put"
aggregatorPath = "/api/aggregators"
versionPath = "/api/version"
annotationPath = "/api/annotation"
queryPath = "/api/query"
queryLastPath = "/api/query/last"
putRespWithSummary = "summary" // Summary response for PUT operations.
putRespWithDetails = "details" // Detailed response for PUT operations.
// The three keys in the rateOption parameter of the QueryParam.
queryRateOptionCounter = "counter" // The corresponding value type is bool
queryRateOptionCounterMax = "counterMax" // The corresponding value type is int,int64
queryRateOptionResetValue = "resetValue" // The corresponding value type is int,int64
anQueryStartTime = "start_time"
anQueryTSUid = "tsuid"
// The below three constants are used in /put.
defaultMaxPutPointsNum = 75
defaultDetectDeltaNum = 3
// Unit is bytes, and assumes that config items of 'tsd.http.request.enable_chunked = true'
// and 'tsd.http.request.max_chunk = 40960' are all in the opentsdb.conf.
defaultMaxContentLength = 40960
opentsdbOperationDurationName = "app_opentsdb_operation_duration"
opentsdbOperationTotalName = "app_opentsdb_operation_total"
)
//nolint:gochecknoglobals // this variable is being set again with a mockserver response for testing HealthCheck endpoint.
var dialTimeout = net.DialTimeout
// Client is the implementation of the OpenTSDBClient interface,
// which includes context-aware functionality.
type Client struct {
endpoint string
client httpClient
config Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
type Config struct {
// The host of the target opentsdb, is a required non-empty string which is
// in the format of ip:port without http:// prefix or a domain.
Host string
// A pointer of http.Transport is used by the opentsdb client.
// This value is optional, and if it is not set, client.defaultTransport, which
// enables tcp keepalive mode, will be used in the opentsdb client.
Transport *http.Transport
// The maximal number of datapoints which will be inserted into the opentsdb
// via one calling of /api/put method.
// This value is optional, and if it is not set, client.defaultMaxPutPointsNum
// will be used in the opentsdb client.
MaxPutPointsNum int
// The detect delta number of datapoints which will be used in client.Put()
// to split a large group of datapoints into small batches.
// This value is optional, and if it is not set, client.defaultDetectDeltaNum
// will be used in the opentsdb client.
DetectDeltaNum int
// The maximal body content length per /api/put method to insert datapoints
// into opentsdb.
// This value is optional, and if it is not set, client.defaultMaxPutPointsNum
// will be used in the opentsdb client.
MaxContentLength int
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// New initializes a new instance of Opentsdb with provided configuration.
func New(config Config) *Client {
return &Client{config: config}
}
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// registerMetrics initializes OpenTSDB metrics once when a metrics provider is injected.
func (c *Client) registerMetrics() {
if c.metrics == nil {
return
}
durationBuckets := []float64{
1, // 1 ms
5, // 5 ms
10, // 10 ms
50, // 50 ms
100, // 100 ms
250, // 250 ms
500, // 500 ms
1000, // 1 s
2000, // 2 s
5000, // 5 s
}
c.metrics.NewHistogram(
opentsdbOperationDurationName,
"Duration of OpenTSDB operations in milliseconds.",
durationBuckets...,
)
c.metrics.NewCounter(
opentsdbOperationTotalName,
"Total OpenTSDB operations.",
)
}
// Connect initializes an HTTP client for OpenTSDB using the provided configuration.
// If the configuration is invalid or the endpoint is unreachable, an error is logged.
func (c *Client) Connect() {
c.registerMetrics()
span := c.addTrace(context.Background(), "Connect")
if span != nil {
span.SetAttributes(attribute.Int64(fmt.Sprintf("opentsdb.%v", "Connect"), 0))
span.End()
}
c.logger.Debugf("connecting to OpenTSDB at host %s", c.config.Host)
// Set default values for optional configuration fields.
c.initializeClient()
// Initialize the OpenTSDB client with the given configuration.
c.endpoint = fmt.Sprintf("http://%s", c.config.Host)
res := VersionResponse{}
err := c.version(context.Background(), &res)
if err != nil {
c.logger.Errorf("error while connecting to OpenTSDB: %v", err)
return
}
c.logger.Logf("connected to OpenTSDB at %s", c.endpoint)
}
func (c *Client) PutDataPoints(ctx context.Context, datas any, queryParam string, resp any) error {
span := c.addTrace(ctx, "PutDataPoints")
status := statusFailed
message := "Put request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "PutDataPoints", &status, &message, span)
putResp, ok := resp.(*PutResponse)
if !ok {
return fmt.Errorf("%w: Must be *PutResponse", errInvalidResponseType)
}
datapoints, ok := datas.([]DataPoint)
if !ok {
return fmt.Errorf("%w: Must be []DataPoint", errInvalidResponseType)
}
err := validateDataPoint(datapoints)
if err != nil {
message = err.Error()
return err
}
if !isValidPutParam(queryParam) {
message = "the given query param is invalid."
return errInvalidQueryParam
}
putEndpoint := fmt.Sprintf("%s%s", c.endpoint, putPath)
if !isEmptyPutParam(queryParam) {
putEndpoint = fmt.Sprintf("%s?%s", putEndpoint, queryParam)
}
tempResp, err := c.getResponse(ctx, putEndpoint, datapoints, &message)
if err != nil {
return err
}
if len(tempResp.Errors) > 0 {
return parsePutErrorMsg(tempResp)
}
status = statusSuccess
message = fmt.Sprintf("put request to url %q processed successfully", putEndpoint)
*putResp = *tempResp
return nil
}
func (c *Client) QueryDataPoints(ctx context.Context, parameters, resp any) error {
span := c.addTrace(ctx, "QueryDataPoints")
status := statusFailed
message := "QueryDatapoints request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "Query", &status, &message, span)
param, ok := parameters.(*QueryParam)
if !ok {
return fmt.Errorf("%w: Must be *QueryParam", errInvalidQueryParam)
}
queryResp, ok := resp.(*QueryResponse)
if !ok {
return fmt.Errorf("%w: Must be *QueryResponse", errInvalidResponseType)
}
if !isValidQueryParam(param) {
message = "invalid query parameters"
return errInvalidQueryParam
}
queryEndpoint := fmt.Sprintf("%s%s", c.endpoint, queryPath)
reqBodyCnt, err := getQueryBodyContents(param)
if err != nil {
message = fmt.Sprintf("getQueryBodyContents error: %s", err)
return err
}
if err = c.sendRequest(ctx, http.MethodPost, queryEndpoint, reqBodyCnt, queryResp); err != nil {
message = fmt.Sprintf("error processing Query request at url %q: %s ", queryEndpoint, err)
return err
}
status = statusSuccess
message = fmt.Sprintf("query request at url %q processed successfully", queryEndpoint)
return nil
}
func (c *Client) QueryLatestDataPoints(ctx context.Context, parameters, resp any) error {
span := c.addTrace(ctx, "QueryLastDataPoints")
status := statusFailed
message := "QueryLatestDataPoints request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "QueryLastDataPoints", &status, &message, span)
param, ok := parameters.(*QueryLastParam)
if !ok {
return fmt.Errorf("%w: Must be a *QueryLastParam type", errInvalidParam)
}
queryResp, ok := resp.(*QueryLastResponse)
if !ok {
return fmt.Errorf("%w: Must be a *QueryLastResponse type", errInvalidResponseType)
}
if !isValidQueryLastParam(param) {
message = "invalid query last param"
return errInvalidQueryParam
}
queryEndpoint := fmt.Sprintf("%s%s", c.endpoint, queryLastPath)
reqBodyCnt, err := getQueryBodyContents(param)
if err != nil {
message = fmt.Sprint("error retrieving body contents: ", err)
return err
}
if err = c.sendRequest(ctx, http.MethodPost, queryEndpoint, reqBodyCnt, queryResp); err != nil {
message = fmt.Sprintf("error processing LatestQuery request at url %q: %s ", queryEndpoint, err)
return err
}
status = statusSuccess
message = fmt.Sprintf("querylast request to url %q processed successfully", queryEndpoint)
c.logger.Logf("querylast request processed successfully")
return nil
}
func (c *Client) QueryAnnotation(ctx context.Context, queryAnnoParam map[string]any, resp any) error {
span := c.addTrace(ctx, "QueryAnnotation")
status := statusFailed
message := "QueryAnnotation request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "QueryAnnotation", &status, &message, span)
annResp, ok := resp.(*AnnotationResponse)
if !ok {
return fmt.Errorf("%w: Must be *AnnotationResponse", errInvalidResponseType)
}
if len(queryAnnoParam) == 0 {
message = "annotation query parameter is empty"
return fmt.Errorf("%w: %s", errInvalidQueryParam, message)
}
buffer := bytes.NewBuffer(nil)
queryURL := url.Values{}
for k, v := range queryAnnoParam {
value, ok := v.(string)
if ok {
queryURL.Add(k, value)
}
}
buffer.WriteString(queryURL.Encode())
annoEndpoint := fmt.Sprintf("%s%s?%s", c.endpoint, annotationPath, buffer.String())
if err := c.sendRequest(ctx, http.MethodGet, annoEndpoint, "", annResp); err != nil {
message = fmt.Sprintf("error processing AnnotationQuery request: %s", err.Error())
return err
}
status = statusSuccess
message = fmt.Sprintf("Annotation query sent to url: %s", annoEndpoint)
c.logger.Log("Annotation query processed successfully")
return nil
}
func (c *Client) PostAnnotation(ctx context.Context, annotation, resp any) error {
return c.operateAnnotation(ctx, annotation, resp, http.MethodPost, "PostAnnotation")
}
func (c *Client) PutAnnotation(ctx context.Context, annotation, resp any) error {
return c.operateAnnotation(ctx, annotation, resp, http.MethodPut, "PutAnnotation")
}
func (c *Client) DeleteAnnotation(ctx context.Context, annotation, resp any) error {
return c.operateAnnotation(ctx, annotation, resp, http.MethodDelete, "DeleteAnnotation")
}
func (c *Client) GetAggregators(ctx context.Context, resp any) error {
span := c.addTrace(ctx, "GetAggregators")
status := statusFailed
message := "GetAggregators request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "GetAggregators", &status, &message, span)
aggreResp, ok := resp.(*AggregatorsResponse)
if !ok {
return fmt.Errorf("%w: Must be a *AggregatorsResponse", errInvalidResponseType)
}
aggregatorsEndpoint := fmt.Sprintf("%s%s", c.endpoint, aggregatorPath)
if err := c.sendRequest(ctx, http.MethodGet, aggregatorsEndpoint, "", aggreResp); err != nil {
message = fmt.Sprintf("error retrieving aggregators from url: %s", aggregatorsEndpoint)
return err
}
status = statusSuccess
message = fmt.Sprintf("aggregators retrieved from url: %s", aggregatorsEndpoint)
c.logger.Log("aggregators fetched successfully")
return nil
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
span := c.addTrace(ctx, "HealthCheck")
status := statusFailed
message := "HealthCheck request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "HealthCheck", &status, &message, span)
h := Health{
Details: make(map[string]any),
}
conn, err := dialTimeout("tcp", c.config.Host, defaultDialTime)
if err != nil {
h.Status = "DOWN"
message = fmt.Sprintf("OpenTSDB is unreachable: %v", err)
return nil, err
}
if conn != nil {
defer conn.Close()
}
h.Details["host"] = c.endpoint
ver := &VersionResponse{}
err = c.version(ctx, ver)
if err != nil {
message = err.Error()
return nil, err
}
h.Details["version"] = ver.VersionInfo["version"]
status = statusSuccess
h.Status = "UP"
message = "connection to OpenTSDB is alive"
return &h, nil
}
================================================
FILE: pkg/gofr/datasource/opentsdb/opentsdb_test.go
================================================
package opentsdb
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
var (
errConnection = errors.New("connection error")
errRequestFailed = errors.New("request failed")
)
func TestUseMetricsRegistersMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(Config{Host: "localhost:4242"})
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().NewHistogram(opentsdbOperationDurationName, gomock.Any(), gomock.Any()).Times(1)
mockMetrics.EXPECT().NewCounter(opentsdbOperationTotalName, gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
}
func TestSendOperationStatsEmitsMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), opentsdbOperationDurationName, gomock.Any(), gomock.Any()).Times(1)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), opentsdbOperationTotalName, gomock.Any()).Times(1)
status := statusSuccess
message := "ok"
sendOperationStats(context.Background(), mockLogger, mockMetrics, "localhost:4242", time.Now(), "Query", &status, &message, nil)
}
func TestSendRequestSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
mockResponse := http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`["sum","avg"]`)),
}
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&mockResponse, nil).
Times(1)
parsedResp := AggregatorsResponse{}
err := client.sendRequest(context.Background(), "GET", "http://localhost:4242/aggregators", "", &parsedResp)
require.NoError(t, err)
assert.Equal(t, []string{"sum", "avg"}, parsedResp.Aggregators)
}
func TestSendRequestFailure(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(nil, errRequestFailed).
Times(1)
parsedResp := AggregatorsResponse{}
err := client.sendRequest(context.Background(), "GET", "http://localhost:4242/aggregators", "", &parsedResp)
require.Error(t, err)
assert.Contains(t, err.Error(), "request failed")
}
func TestGetCustomParser(t *testing.T) {
client, _ := setOpenTSDBTest(t)
resp := &AggregatorsResponse{}
parser := resp.getCustomParser(client.logger)
err := parser([]byte(`["sum","avg"]`))
require.NoError(t, err)
assert.Equal(t, []string{"sum", "avg"}, resp.Aggregators)
}
// setOpenTSDBTest initializes an Client for testing.
func setOpenTSDBTest(t *testing.T) (*Client, *MockhttpClient) {
t.Helper()
opentsdbCfg := Config{
Host: "localhost:4242",
MaxContentLength: 4096,
MaxPutPointsNum: 1000,
DetectDeltaNum: 10,
}
tsdbClient := New(opentsdbCfg)
tracer := otel.GetTracerProvider().Tracer("gofr-opentsdb")
tsdbClient.UseTracer(tracer)
mocklogger := NewMockLogger(gomock.NewController(t))
tsdbClient.UseLogger(mocklogger)
mocklogger.EXPECT().Logf(gomock.Any(), gomock.Any()).AnyTimes()
mocklogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mocklogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mocklogger.EXPECT().Log(gomock.Any()).AnyTimes()
tsdbClient.config.Host = strings.TrimSpace(tsdbClient.config.Host)
if tsdbClient.config.Host == "" {
tsdbClient.logger.Errorf("the OpentsdbEndpoint in the given configuration cannot be empty.")
}
mockhttp := NewMockhttpClient(gomock.NewController(t))
tsdbClient.client = mockhttp
// Set default values for optional configuration fields.
if tsdbClient.config.MaxPutPointsNum <= 0 {
tsdbClient.config.MaxPutPointsNum = defaultMaxPutPointsNum
}
if tsdbClient.config.DetectDeltaNum <= 0 {
tsdbClient.config.DetectDeltaNum = defaultDetectDeltaNum
}
if tsdbClient.config.MaxContentLength <= 0 {
tsdbClient.config.MaxContentLength = defaultMaxContentLength
}
// Initialize the OpenTSDB client with the given configuration.
tsdbClient.endpoint = fmt.Sprintf("http://%s", tsdbClient.config.Host)
return tsdbClient, mockhttp
}
func TestPutSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
PutDataPointNum := 4
name := []string{"cpu", "disk", "net", "mem"}
cpuDatas := make([]DataPoint, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for i := 0; i < PutDataPointNum; i++ {
val, _ := rand.Int(rand.Reader, big.NewInt(100))
data := DataPoint{
Metric: name[i%len(name)],
Timestamp: time.Now().Unix(),
Value: val.Int64(),
Tags: tags,
}
cpuDatas = append(cpuDatas, data)
}
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"StatusCode":200,"failed":0,"success":4}`)),
}, nil).Times(1)
resp := &PutResponse{}
err := client.PutDataPoints(context.Background(), cpuDatas, "details", resp)
require.NoError(t, err)
require.NotNil(t, resp)
require.Equal(t, int64(len(cpuDatas)), resp.Success)
}
func TestPutInvalidDataPoint(t *testing.T) {
client, _ := setOpenTSDBTest(t)
dataPoints := []DataPoint{
{
Metric: "",
Timestamp: 0,
Value: 0,
Tags: map[string]string{},
},
}
resp := &PutResponse{}
err := client.PutDataPoints(context.Background(), dataPoints, "", resp)
require.ErrorIs(t, err, errInvalidDataPoint)
require.ErrorContains(t, err, "please give a valid value")
}
func TestPutInvalidQueryParam(t *testing.T) {
client, _ := setOpenTSDBTest(t)
dataPoints := []DataPoint{
{
Metric: "metric1",
Timestamp: time.Now().Unix(),
Value: 100,
Tags: map[string]string{"tag1": "value1"},
},
}
resp := &PutResponse{}
err := client.PutDataPoints(context.Background(), dataPoints, "invalid_param", resp)
require.ErrorIs(t, err, errInvalidQueryParam)
}
func TestPutErrorResponse(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
dataPoints := []DataPoint{
{
Metric: "invalid_metric_name#$%",
Timestamp: time.Now().Unix(),
Value: 100,
Tags: map[string]string{"tag1": "value1"},
},
}
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusBadRequest,
Body: io.NopCloser(strings.NewReader(`{"StatusCode":400,"error":"Invalid metric name"}`)),
}, nil).Times(1)
resp := &PutResponse{}
err := client.PutDataPoints(context.Background(), dataPoints, "", resp)
require.ErrorIs(t, err, errResp)
require.ErrorContains(t, err, "status code: 400")
}
func TestPostQuerySuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
st1 := time.Now().Unix() - 3600
st2 := time.Now().Unix()
queryParam := QueryParam{
Start: st1,
End: st2,
}
name := []string{"cpu", "disk", "net", "mem"}
subqueries := make([]SubQuery, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for _, metric := range name {
subQuery := SubQuery{
Aggregator: "sum",
Metric: metric,
Tags: tags,
}
subqueries = append(subqueries, subQuery)
}
queryParam.Queries = subqueries
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`[{"metric":"net","timestamp":1728836485000,"value":` +
`"19.499737232159088","tags":{"demo-name":"opentsdb-test","host":"gofr-host","try-name":` +
`"gofr-sample"},"tsuid":"000003000001000001000002000007000003000008"},{"metric":"disk",` +
`"timestamp":1728836485000,"value":"98.53097270356102","tags":{"demo-name":"opentsdb-test",` +
`"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000002000001000001000002000007000003000008"}` +
`,{"metric":"cpu","timestamp":1728836485000,"value":"49.47446557839882","tags":{"demo-name":"opentsdb` +
`-test","host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000001000001000001000002000007000003000008"}` +
`,{"metric":"mem","timestamp":1728836485000,"value":"28.62340008609452","tags":{"demo-name":"opentsdb-test",` +
`"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000004000001000001000002000007000003000008"}]`)),
}, nil).Times(1)
queryResp := &QueryResponse{}
err := client.QueryDataPoints(context.Background(), &queryParam, queryResp)
require.NoError(t, err)
require.NotNil(t, queryResp)
require.Len(t, queryResp.QueryRespCnts, 4)
}
func TestPostQueryLastSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
name := []string{"cpu", "disk", "net", "mem"}
subqueriesLast := make([]SubQueryLast, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for _, metric := range name {
subQueryLast := SubQueryLast{
Metric: metric,
Tags: tags,
}
subqueriesLast = append(subqueriesLast, subQueryLast)
}
queryLastParam := QueryLastParam{
Queries: subqueriesLast,
ResolveNames: true,
BackScan: 24,
}
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`[{"metric":"net","timestamp":1728836485000,"value":` +
`"19.499737232159088","tags":{"demo-name":"opentsdb-test","host":"gofr-host","try-name":` +
`"gofr-sample"},"tsuid":"000003000001000001000002000007000003000008"},{"metric":"disk",` +
`"timestamp":1728836485000,"value":"98.53097270356102","tags":{"demo-name":"opentsdb-test",` +
`"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000002000001000001000002000007000003000008"}` +
`,{"metric":"cpu","timestamp":1728836485000,"value":"49.47446557839882","tags":{"demo-name":"opentsdb` +
`-test","host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000001000001000001000002000007000003000008"}` +
`,{"metric":"mem","timestamp":1728836485000,"value":"28.62340008609452","tags":{"demo-name":"opentsdb-test",` +
`"host":"gofr-host","try-name":"gofr-sample"},"tsuid":"000004000001000001000002000007000003000008"}]`)),
}, nil).Times(1)
queryLastResp := &QueryLastResponse{}
err := client.QueryLatestDataPoints(context.Background(), &queryLastParam, queryLastResp)
require.NoError(t, err)
require.NotNil(t, queryLastResp)
require.Len(t, queryLastResp.QueryRespCnts, 4)
}
func TestPostQueryDeleteSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
st1 := time.Now().Unix() - 3600
st2 := time.Now().Unix()
queryParam := QueryParam{
Start: st1,
End: st2,
Delete: true,
}
name := []string{"cpu", "disk", "net", "mem"}
subqueries := make([]SubQuery, 0)
tags := map[string]string{
"host": "gofr-host",
"try-name": "gofr-sample",
"demo-name": "opentsdb-test",
}
for _, metric := range name {
subQuery := SubQuery{
Aggregator: "sum",
Metric: metric,
Tags: tags,
}
subqueries = append(subqueries, subQuery)
}
queryParam.Queries = subqueries
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`[]`)),
}, nil).Times(1)
deleteResp := &QueryResponse{}
err := client.QueryDataPoints(context.Background(), &queryParam, deleteResp)
require.NoError(t, err)
require.NotNil(t, deleteResp)
}
func TestGetAggregatorsSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
expectedResponse := `["sum","avg","max","min","count"]`
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body with aggregators
}, nil).Times(1)
aggreResp := &AggregatorsResponse{}
err := client.GetAggregators(context.Background(), aggreResp)
require.NoError(t, err)
require.NotNil(t, aggreResp)
var aggregators []string
err = json.Unmarshal([]byte(expectedResponse), &aggregators)
require.NoError(t, err)
require.ElementsMatch(t, aggregators, aggreResp.Aggregators) // Assuming your response has an Aggregators field
}
func TestGetVersionSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
expectedResponse := `{"short_revision":"","repo":"/opt/opentsdb/opentsdb-2.4.0/build",` +
`"host":"a0d1ce2d1fd7","version":"2.4.0","full_revision":"","repo_status":"MODIFIED"` +
`,"user":"root","branch":"","timestamp":"1607178614"}`
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body for version
}, nil).Times(1)
versionResp := &VersionResponse{}
err := client.version(context.Background(), versionResp)
require.NoError(t, err)
require.NotNil(t, versionResp)
var versionData struct {
Version string `json:"version"`
}
err = json.Unmarshal([]byte(expectedResponse), &versionData)
require.NoError(t, err)
require.Equal(t, versionData.Version, versionResp.VersionInfo["version"])
}
func TestUpdateAnnotationSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
custom := map[string]string{
"owner": "gofr",
"host": "gofr-host",
}
addedST := time.Now().Unix()
addedTsuid := "000001000001000002"
anno := Annotation{
StartTime: addedST,
TSUID: addedTsuid,
Description: "gofrf test annotation",
Notes: "These would be details about the event, the description is just a summary",
Custom: custom,
}
expectedResponse := `{"tsuid":"000001000001000002","description":"gofrf test annotation","notes":` +
`"These would be details about the event, the description is just a summary","custom":{"host":` +
`"gofr-host","owner":"gofr"},"startTime":` + fmt.Sprintf("%d", addedST) + `,"endTime":0}`
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(expectedResponse)), // Response body for the annotation
}, nil).Times(1)
queryAnnoResp := &AnnotationResponse{}
err := client.PostAnnotation(context.Background(), &anno, queryAnnoResp)
require.NoError(t, err)
require.NotNil(t, queryAnnoResp)
require.Equal(t, anno.TSUID, queryAnnoResp.TSUID)
require.Equal(t, anno.StartTime, queryAnnoResp.StartTime)
require.Equal(t, anno.Description, queryAnnoResp.Description)
require.Equal(t, anno.Notes, queryAnnoResp.Notes)
require.Equal(t, anno.Custom, queryAnnoResp.Custom)
}
func TestQueryAnnotationSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
custom := map[string]string{
"owner": "gofr",
"host": "gofr-host",
}
addedST := time.Now().Unix()
addedTsuid := "000001000001000002"
anno := Annotation{
StartTime: addedST,
TSUID: addedTsuid,
Description: "gofr test annotation",
Notes: "These would be details about the event, the description is just a summary",
Custom: custom,
}
mockResponse := `{"tsuid":"000001000001000002","description":"gofr test annotation","notes":"These` +
` would be details about the event, the description is just a summary","custom":{"host"` +
`:"gofr-host","owner":"gofr"},"startTime":1728841614,"endTime":0}`
mockHTTP.EXPECT().
Do(gomock.Any()).
DoAndReturn(func(*http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(mockResponse)), // Fresh body each time
}, nil
}).Times(2)
postResp := &AnnotationResponse{}
err := client.PostAnnotation(context.Background(), &anno, postResp)
require.NoError(t, err)
require.NotNil(t, postResp)
queryAnnoMap := map[string]any{
anQueryStartTime: addedST,
anQueryTSUid: addedTsuid,
}
queryResp := &AnnotationResponse{}
err = client.QueryAnnotation(context.Background(), queryAnnoMap, queryResp)
require.NoError(t, err)
require.NotNil(t, queryResp)
}
func TestDeleteAnnotationSuccess(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
custom := map[string]string{
"owner": "gofr",
"host": "gofr-host",
}
addedST := time.Now().Unix()
addedTsuid := "000001000001000002"
anno := Annotation{
StartTime: addedST,
TSUID: addedTsuid,
Description: "gofr-host test annotation",
Notes: "These would be details about the event, the description is just a summary",
Custom: custom,
}
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"tsuid": "000001000001000002",
"description": "gofr-host test annotation",
"notes": "These would be details about the event, the description is just a summary",
"custom": {"host": "gofr-host", "owner": "gofr"},
"startTime": 1728843749,
"endTime": 0
}`)),
}, nil).Times(1)
postResp := &AnnotationResponse{}
err := client.PostAnnotation(context.Background(), &anno, postResp)
require.NoError(t, err)
require.NotNil(t, postResp)
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusNoContent,
Body: http.NoBody,
}, nil).Times(1)
deleteResp := &AnnotationResponse{}
err = client.DeleteAnnotation(context.Background(), &anno, deleteResp)
require.NoError(t, err)
require.NotNil(t, deleteResp)
require.Empty(t, deleteResp.TSUID)
require.Empty(t, deleteResp.StartTime)
require.Empty(t, deleteResp.Description)
}
func TestHealthCheck_Success(t *testing.T) {
client, mockHTTP := setOpenTSDBTest(t)
mockHTTP.EXPECT().
Do(gomock.Any()).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{"version": "2.9.0"}`)),
}, nil).
Times(1)
mockConn := NewMockconnection(gomock.NewController(t))
mockConn.EXPECT().Close()
dialTimeout = func(_, _ string, _ time.Duration) (net.Conn, error) {
return mockConn, nil
}
resp, err := client.HealthCheck(context.Background())
require.NoError(t, err, "Expected no error during health check")
require.NotNil(t, resp, "Expected response to be not nil")
require.Equal(t, "UP", resp.(*Health).Status, "Expected status to be UP")
require.Equal(t, "2.9.0", resp.(*Health).Details["version"], "Expected version to be 2.9.0")
}
func TestHealthCheck_Failure(t *testing.T) {
client, _ := setOpenTSDBTest(t)
dialTimeout = func(_, _ string, _ time.Duration) (net.Conn, error) {
return nil, errConnection
}
resp, err := client.HealthCheck(context.Background())
require.Nil(t, resp, "Expected response to be nil")
require.EqualError(t, err, "connection error", "Expected error during health check")
}
================================================
FILE: pkg/gofr/datasource/opentsdb/preprocess.go
================================================
package opentsdb
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"strings"
)
var errInvalidDataPoint = errors.New("invalid data points")
// QueryParam is the structure used to hold the querying parameters when calling /api/query.
// Each attributes in QueryParam matches the definition in
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html.
type QueryParam struct {
// The start time for the query. This can be a relative or absolute timestamp.
// The data type can only be string, int, or int64.
// The value is required with non-zero value of the target type.
Start any `json:"start"`
// An end time for the query. If not supplied, the TSD will assume the local
// system time on the server. This may be a relative or absolute timestamp.
// The data type can only be string, or int64.
// The value is optional.
End any `json:"end,omitempty"`
// One or more sub queries used to select the time series to return.
// These may be metric m or TSUID tsuids queries
// The value is required with at least one element
Queries []SubQuery `json:"queries"`
// An optional value is used to show whether to return annotations with a query.
// The default is to return annotations for the requested timespan but this flag can disable the return.
// This affects both local and global notes and overrides globalAnnotations
NoAnnotations bool `json:"noAnnotations,omitempty"`
// An optional value is used to show whether the query should retrieve global
// annotations for the requested timespan.
GlobalAnnotations bool `json:"globalAnnotations,omitempty"`
// An optional value is used to show whether to output data point timestamps in milliseconds or seconds.
// If this flag is not provided and there are multiple data points within a second,
// those data points will be down sampled using the query's aggregation function.
MsResolution bool `json:"msResolution,omitempty"`
// An optional value is used to show whether to output the TSUIDs associated with time series in the results.
// If multiple time series were aggregated into one set, multiple TSUIDs will be returned in a sorted manner.
ShowTSUIDs bool `json:"showTSUIDs,omitempty"`
// An optional value is used to show whether can be passed to the JSON with a POST to delete any data point
// that match the given query.
Delete bool `json:"delete,omitempty"`
}
// SubQuery is the structure used to hold the subquery parameters when calling /api/query.
// Each attributes in SubQuery matches the definition in
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html.
type SubQuery struct {
// The name of an aggregation function to use.
// The value is required with non-empty one in the range of
// the response of calling /api/aggregators.
//
// By default, the potential values and corresponding descriptions are as following:
// "sum": Adds all of the data points for a timestamp.
// "min": Picks the smallest data point for each timestamp.
// "max": Picks the largest data point for each timestamp.
// "avg": Averages the values for the data points at each timestamp.
Aggregator string `json:"aggregator"`
// The name of a metric stored in the system.
// The value is required with non-empty value.
Metric string `json:"metric"`
// An optional value is used to show whether the data should be
// converted into deltas before returning. This is useful if the metric is a
// continuously incrementing counter, and you want to view the rate of change between data points.
Rate bool `json:"rate,omitempty"`
// rateOptions represents monotonically increasing counter handling options.
// The value is optional.
// Currently, there is only three kind of value can be set to this map:
// Only three keys can be set into the rateOption parameter of the QueryParam is
// queryRateOptionCounter (value type is bool), queryRateOptionCounterMax (value type is int,int64)
// queryRateOptionResetValue (value type is int,int64)
RateParams map[string]any `json:"rateOptions,omitempty"`
// An optional value downsampling function to reduce the amount of data returned.
DownSample string `json:"downsample,omitempty"`
// An optional value to drill down to specific time series or group results by tag,
// supply one or more map values in the same format as the query string. Tags are converted to filters in 2.2.
// Note that if no tags are specified, all metrics in the system will be aggregated into the results.
// It will be deprecated in OpenTSDB 2.2.
Tags map[string]string `json:"tags,omitempty"`
// An optional value used to filter the time series emitted in the results.
// Note that if no filters are specified, all time series for the given
// metric will be aggregated into the results.
Filters []Filter `json:"filters,omitempty"`
}
// Filter is the structure used to hold the filter parameters when calling /api/query.
// Each attributes in Filter matches the definition in
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html.
type Filter struct {
// The name of the filter to invoke. The value is required with a non-empty
// value in the range of calling /api/config/filters.
Type string `json:"type"`
// The tag key to invoke the filter on, required with a non-empty value
Tagk string `json:"tagk"`
// The filter expression to evaluate and depends on the filter being used, required with a non-empty value
FilterExp string `json:"filter"`
// An optional value to show whether to group the results by each value matched by the filter.
// By default, all values matching the filter will be aggregated into a single series.
GroupBy bool `json:"groupBy"`
}
// DataPoint is the structure used to hold the values of a metric item. Each attributes
// in DataPoint matches the definition in [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/put.html.
type DataPoint struct {
// The name of the metric which is about to be stored, and is required with non-empty value.
Metric string `json:"metric"`
// A Unix epoch style timestamp in seconds or milliseconds.
// The timestamp must not contain non-numeric characters.
// One can use time.Now().Unix() to set this attribute.
// This attribute is also required with non-zero value.
Timestamp int64 `json:"timestamp"`
// The real type of Value only could be int, int64, float64, or string, and is required.
Value any `json:"value"`
// A map of tag name/tag value pairs. At least one pair must be supplied.
// Don't use too many tags, keep it to a fairly small number, usually up to 4 or 5 tags
// (By default, OpenTSDB supports a maximum of 8 tags, which can be modified by add
// configuration item 'tsd.storage.max_tags' in opentsdb.conf).
Tags map[string]string `json:"tags"`
}
// PutError holds the error message for each putting DataPoint instance. Only calling PUT() with "details"
// query parameter, the response of the failed put data operation can contain an array PutError instance
// to show the details for each failure.
type PutError struct {
Data DataPoint `json:"datapoint"`
ErrorMsg string `json:"error"`
}
// PutResponse acts as the implementation of Response in the /api/put scene.
// It holds the status code and the response values defined in
// the [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/put.html.
type PutResponse struct {
Failed int64 `json:"failed"`
Success int64 `json:"success"`
Errors []PutError `json:"errors,omitempty"`
}
func (c *Client) getResponse(ctx context.Context, putEndpoint string, datapoints []DataPoint,
message *string) (*PutResponse, error) {
marshaled, err := json.Marshal(datapoints)
if err != nil {
*message = fmt.Sprintf("getPutBodyContents error: %s", err)
c.logger.Errorf(*message)
}
reqBodyCnt := string(marshaled)
putResp := PutResponse{}
if err = c.sendRequest(ctx, http.MethodPost, putEndpoint, reqBodyCnt, &putResp); err != nil {
*message = fmt.Sprintf("error processing Put request at url %q: %s", putEndpoint, err)
return nil, err
}
return &putResp, nil
}
func parsePutErrorMsg(resp *PutResponse) error {
buf := bytes.Buffer{}
buf.WriteString(fmt.Sprintf("Failed to put %d datapoint(s) into opentsdb \n", resp.Failed))
if len(resp.Errors) > 0 {
for _, putError := range resp.Errors {
str, err := json.Marshal(putError)
if err != nil {
return err
}
buf.WriteString(fmt.Sprintf("\t%s\n", str))
}
}
return fmt.Errorf("%w: %s", errUnexpected, buf.String())
}
func validateDataPoint(datas []DataPoint) error {
if len(datas) == 0 {
return fmt.Errorf("%w: datapoints are empty", errInvalidDataPoint)
}
for _, data := range datas {
if !isValidDataPoint(&data) {
return fmt.Errorf("%w: please give a valid value", errInvalidDataPoint)
}
}
return nil
}
func isValidDataPoint(data *DataPoint) bool {
if data.Metric == "" || data.Timestamp == 0 || len(data.Tags) < 1 || data.Value == nil {
return false
}
switch data.Value.(type) {
case int64:
case int:
case float64:
case float32:
case string:
default:
return false
}
return true
}
func isValidPutParam(param string) bool {
if isEmptyPutParam(param) {
return true
}
param = strings.TrimSpace(param)
if param != putRespWithSummary && param != putRespWithDetails {
return false
}
return true
}
func isEmptyPutParam(param string) bool {
return strings.TrimSpace(param) == ""
}
// QueryLastParam is the structure used to hold
// the querying parameters when calling /api/query/last.
// Each attributes in QueryLastParam matches the definition in
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html.
type QueryLastParam struct {
// One or more sub queries used to select the time series to return.
// These may be metric m or TSUID tsuids queries
// The value is required with at least one element
Queries []SubQueryLast `json:"queries"`
// An optional flag is used to determine whether or not to resolve the TSUIDs of results to
// their metric and tag names. The default value is false.
ResolveNames bool `json:"resolveNames"`
// An optional number of hours is used to search in the past for data. If set to 0 then the
// timestamp of the meta data counter for the time series is used.
BackScan int `json:"backScan"`
}
// SubQueryLast is the structure used to hold the subquery parameters when calling /api/query/last.
// Each attributes in SubQueryLast matches the definition in
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html.
type SubQueryLast struct {
// The name of a metric stored in the system.
// The value is required with non-empty value.
Metric string `json:"metric"`
// An optional value to drill down to specific time series or group results by tag,
// supply one or more map values in the same format as the query string. Tags are converted to filters in 2.2.
// Note that if no tags are specified, all metrics in the system will be aggregated into the results.
// It will be deprecated in OpenTSDB 2.2.
Tags map[string]string `json:"tags,omitempty"`
}
func getQueryBodyContents(param any) (string, error) {
result, err := json.Marshal(param)
if err != nil {
return "", fmt.Errorf("failed to marshal query param: %w", err)
}
return string(result), nil
}
func isValidQueryParam(param *QueryParam) bool {
if len(param.Queries) == 0 {
return false
}
if !isValidTimePoint(param.Start) {
return false
}
for _, query := range param.Queries {
if !areValidParams(&query) {
return false
}
}
return true
}
func areValidParams(query *SubQuery) bool {
if query.Aggregator == "" || query.Metric == "" {
return false
}
for k := range query.RateParams {
if k != queryRateOptionCounter && k != queryRateOptionCounterMax && k != queryRateOptionResetValue {
return false
}
}
return true
}
func isValidTimePoint(timePoint any) bool {
if timePoint == nil {
return false
}
switch v := timePoint.(type) {
case int:
return v > 0
case int64:
return v > 0
case string:
return v != ""
}
return false
}
func isValidQueryLastParam(param *QueryLastParam) bool {
if len(param.Queries) == 0 {
return false
}
for _, query := range param.Queries {
if query.Metric == "" {
return false
}
}
return true
}
func (c *Client) initializeClient() {
// defaultTransport defines the default HTTP transport settings,
// including connection timeouts and idle connections.
var defaultTransport = &http.Transport{
MaxIdleConnsPerHost: 10,
DialContext: (&net.Dialer{
Timeout: defaultDialTime,
KeepAlive: connectionTimeout,
}).DialContext,
}
c.config.Host = strings.TrimSpace(c.config.Host)
if c.config.Host == "" {
c.logger.Fatal("the OpenTSDB Endpoint in the given configuration cannot be empty.")
}
// Use custom transport settings if provided, otherwise, use the default transport.
transport := c.config.Transport
if transport == nil {
transport = defaultTransport
}
c.client = &http.Client{
Transport: transport,
}
if c.config.MaxPutPointsNum <= 0 {
c.config.MaxPutPointsNum = defaultMaxPutPointsNum
}
if c.config.DetectDeltaNum <= 0 {
c.config.DetectDeltaNum = defaultDetectDeltaNum
}
if c.config.MaxContentLength <= 0 {
c.config.MaxContentLength = defaultMaxContentLength
}
}
================================================
FILE: pkg/gofr/datasource/opentsdb/response.go
================================================
package opentsdb
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"go.opentelemetry.io/otel/trace"
)
var (
errResp = errors.New("error response from OpenTSDB server")
errInvalidArgument = errors.New("invalid argument type")
errUnexpected = errors.New("unexpected error")
)
// AggregatorsResponse acts as the implementation of Response in the /api/aggregators.
// It holds the status code and the response values defined in the
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/aggregators.html.
type AggregatorsResponse struct {
StatusCode int
Aggregators []string
}
// VersionResponse is the struct implementation for /api/version.
type VersionResponse struct {
StatusCode int
VersionInfo map[string]any
}
// Annotation holds parameters for querying or managing annotations via the /api/annotation endpoint in OpenTSDB.
// Used for logging notes on events at specific times, often tied to time series data, mainly for graphing or API queries.
type Annotation struct {
// StartTime is the Unix epoch timestamp (in seconds) for when the event occurred. This is required.
StartTime int64 `json:"startTime,omitempty"`
// EndTime is the optional Unix epoch timestamp (in seconds) for when the event ended, if applicable.
EndTime int64 `json:"endTime,omitempty"`
// TSUID is the optional time series identifier if the annotation is linked to a specific time series.
TSUID string `json:"tsuid,omitempty"`
// Description is a brief, optional summary of the event (recommended to keep under 25 characters for display purposes).
Description string `json:"description,omitempty"`
// Notes is an optional, detailed description of the event.
Notes string `json:"notes,omitempty"`
// Custom is an optional key/value map to store any additional fields and their values.
Custom map[string]string `json:"custom,omitempty"`
}
// AnnotationResponse encapsulates the response data and status when interacting with the /api/annotation endpoint.
type AnnotationResponse struct {
// Annotation holds the associated annotation object.
Annotation
// ErrorInfo contains details about any errors that occurred during the request.
ErrorInfo map[string]any `json:"error,omitempty"`
}
// QueryResponse acts as the implementation of Response in the /api/query scene.
// It holds the status code and the response values defined in the
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html.
type QueryResponse struct {
QueryRespCnts []QueryRespItem `json:"queryRespCnts"`
ErrorMsg map[string]any `json:"error"`
}
// QueryRespItem acts as the implementation of Response in the /api/query scene.
// It holds the response item defined in the
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/index.html.
type QueryRespItem struct {
// Name of the metric retrieved for the time series
Metric string `json:"metric"`
// A list of tags only returned when the results are for a single time series.
// If results are aggregated, this value may be null or an empty map
Tags map[string]string `json:"tags"`
// If more than one time series were included in the result set, i.e. they were aggregated,
// this will display a list of tag names that were found in common across all time series.
// Note that: Api Doc uses 'aggregatedTags', but actual response uses 'aggregateTags'
AggregatedTags []string `json:"aggregateTags"`
// Retrieved data points after being processed by the aggregators. Each data point consists
// of a timestamp and a value, the format determined by the serializer.
// For the JSON serializer, the timestamp will always be a Unix epoch style integer followed
// by the value as an integer or a floating point.
// For example, the default output is "dps"{"":}.
// By default, the timestamps will be in seconds. If the msResolution flag is set, then the
// timestamps will be in milliseconds.
//
// Because the elements of map is out of order, using common way to iterate Dps will not get
// data points with timestamps out of order.
// So be aware that one should use '(qri *QueryRespItem) GetDataPoints() []*DataPoint' to
// acquire the real ascending data points.
Dps map[string]any `json:"dps"`
// If the query retrieved annotations for time series over the requested timespan, they will
// be returned in this group. Annotations for every time series will be merged into one set
// and sorted by start_time. Aggregator functions do not affect annotations, all annotations
// will be returned for the span.
// The value is optional.
Annotations []Annotation `json:"annotations,omitempty"`
// If requested by the user, the query will scan for global annotations during
// the timespan and the results returned in this group.
// The value is optional.
GlobalAnnotations []Annotation `json:"globalAnnotations,omitempty"`
}
// QueryLastResponse acts as the implementation of Response in the /api/query/last scene.
// It holds the status code and the response values defined in the
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html.
type QueryLastResponse struct {
QueryRespCnts []QueryRespLastItem `json:"queryRespCnts,omitempty"`
ErrorMsg map[string]any `json:"error"`
}
// QueryRespLastItem acts as the implementation of Response in the /api/query/last scene.
// It holds the response item defined in the
// [OpenTSDB Official Docs]: http://opentsdb.net/docs/build/html/api_http/query/last.html.
type QueryRespLastItem struct {
// Name of the metric retrieved for the time series.
// Only returned if resolve was set to true.
Metric string `json:"metric"`
// A list of tags only returned when the results are for a single time series.
// If results are aggregated, this value may be null or an empty map.
// Only returned if resolve was set to true.
Tags map[string]string `json:"tags"`
// A Unix epoch timestamp, in milliseconds, when the data point was written.
Timestamp int64 `json:"timestamp"`
// The value of the data point enclosed in quotation marks as a string
Value string `json:"value"`
// The hexadecimal TSUID for the time series
TSUID string `json:"tsuid"`
}
func (*PutResponse) getCustomParser(Logger) func(respCnt []byte) error {
return nil
}
func (queryResp *QueryResponse) getCustomParser(logger Logger) func(respCnt []byte) error {
return queryParserHelper(logger, queryResp, "GetCustomParser-Query")
}
func (queryLastResp *QueryLastResponse) getCustomParser(logger Logger) func(respCnt []byte) error {
return queryParserHelper(logger, queryLastResp, "GetCustomParser-QueryLast")
}
func (verResp *VersionResponse) getCustomParser(logger Logger) func(respCnt []byte) error {
return customParserHelper("GetCustomParser-VersionResp", logger,
func(resp []byte) error {
v := make(map[string]any, 0)
err := json.Unmarshal(resp, &v)
if err != nil {
return err
}
verResp.VersionInfo = v
return nil
})
}
func (annotResp *AnnotationResponse) getCustomParser(logger Logger) func(respCnt []byte) error {
return customParserHelper("getCustomParser-Annotation", logger,
func(resp []byte) error {
if len(resp) == 0 {
return nil
}
return json.Unmarshal(resp, &annotResp)
})
}
func (aggreResp *AggregatorsResponse) getCustomParser(logger Logger) func(respCnt []byte) error {
return customParserHelper("GetCustomParser-Aggregator", logger,
func(resp []byte) error {
j := make([]string, 0)
err := json.Unmarshal(resp, &j)
if err != nil {
return err
}
aggreResp.Aggregators = j
return nil
})
}
type genericResponse interface {
addTrace(ctx context.Context, tracer trace.Tracer, operation string) trace.Span
}
// sendRequest dispatches an HTTP request to the OpenTSDB server, using the provided
// method, URL, and body content. It returns the parsed response or an error, if any.
func (c *Client) sendRequest(ctx context.Context, method, url, reqBodyCnt string, parsedResp response) error {
// Create the HTTP request, attaching the context if available.
req, err := http.NewRequestWithContext(ctx, method, url, strings.NewReader(reqBodyCnt))
if ctx != nil {
req = req.WithContext(ctx)
}
if err != nil {
errRequestCreation := fmt.Errorf("failed to create request for %s %s: %w", method, url, err)
return errRequestCreation
}
// Set the request headers.
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
// Send the request and handle the response.
resp, err := c.client.Do(req)
if err != nil {
sendRequestErr := fmt.Errorf("failed to send request for %s %s: %w", method, url, err)
return sendRequestErr
}
defer resp.Body.Close()
// Read and parse the response.
jsonBytes, err := io.ReadAll(resp.Body)
if err != nil {
errReading := fmt.Errorf("failed to read response body for %s %s: %w", method, url, err)
return errReading
}
parser := parsedResp.getCustomParser(c.logger)
if parser == nil {
// Use the default JSON unmarshaller if no custom parser is provided.
if err = json.Unmarshal(jsonBytes, parsedResp); err != nil {
errUnmarshalling := fmt.Errorf("failed to unmarshal response body for %s %s: %w", method, url, err)
return errUnmarshalling
}
} else {
// Use the custom parser if available.
if err := parser(jsonBytes); err != nil {
return fmt.Errorf("failed to parse response body through custom parser %s %s: %w", method, url, err)
}
}
if resp.StatusCode >= 400 && resp.StatusCode < 500 {
return fmt.Errorf("%w, status code: %d", errResp, resp.StatusCode)
}
return nil
}
func (c *Client) version(ctx context.Context, verResp *VersionResponse) error {
span := c.addTrace(ctx, "Version")
status := statusFailed
message := "version request failed"
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), "Version", &status, &message, span)
verEndpoint := fmt.Sprintf("%s%s", c.endpoint, versionPath)
if err := c.sendRequest(ctx, http.MethodGet, verEndpoint, "", verResp); err != nil {
message = fmt.Sprintf("error while processing request at URL %s: %s", verEndpoint, err)
return err
}
status = statusSuccess
message = fmt.Sprintf("OpenTSDB version %v", verResp.VersionInfo["version"])
return nil
}
// isValidOperateMethod checks if the provided HTTP method is valid for
// operations such as POST, PUT, or DELETE.
func (*Client) isValidOperateMethod(method string) bool {
method = strings.TrimSpace(strings.ToUpper(method))
if method == "" {
return false
}
validMethods := []string{http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPut}
for _, validMethod := range validMethods {
if method == validMethod {
return true
}
}
return false
}
func customParserHelper(operation string, logger Logger, unmarshalFunc func([]byte) error) func([]byte) error {
return func(result []byte) error {
err := unmarshalFunc(result)
if err != nil {
logger.Errorf("unmarshal %s error: %s", operation, err)
return err
}
return nil
}
}
func queryParserHelper(logger Logger, obj genericResponse,
methodName string) func(respCnt []byte) error {
return customParserHelper(methodName, logger, func(resp []byte) error {
originRespStr := string(resp)
var respStr string
if strings.HasPrefix(string(resp), "[") && strings.HasSuffix(string(resp), "]") {
respStr = fmt.Sprintf(`{"queryRespCnts":%s}`, originRespStr)
} else {
respStr = originRespStr
}
return json.Unmarshal([]byte(respStr), obj)
})
}
func (c *Client) operateAnnotation(ctx context.Context, queryAnnotation, resp any, method, operation string) error {
span := c.addTrace(ctx, operation)
status := statusFailed
message := fmt.Sprintf("%v request failed", operation)
defer sendOperationStats(ctx, c.logger, c.metrics, c.config.Host, time.Now(), operation, &status, &message, span)
annotation, ok := queryAnnotation.(*Annotation)
if !ok {
return fmt.Errorf("%w: Must be *Annotation", errInvalidArgument)
}
annResp, ok := resp.(*AnnotationResponse)
if !ok {
return fmt.Errorf("%w: Must be *AnnotationResponse", errInvalidResponseType)
}
if !c.isValidOperateMethod(method) {
message = fmt.Sprintf("invalid annotation operation method: %s", method)
return fmt.Errorf("%w: %s", errUnexpected, message)
}
annoEndpoint := fmt.Sprintf("%s%s", c.endpoint, annotationPath)
resultBytes, err := json.Marshal(annotation)
if err != nil {
message = fmt.Sprintf("marshal annotation response error: %s", err)
return fmt.Errorf("%w: %s", errUnexpected, message)
}
if err = c.sendRequest(ctx, method, annoEndpoint, string(resultBytes), annResp); err != nil {
message = fmt.Sprintf("error processing %s annotation request to url %q: %s", method, annoEndpoint, err.Error())
return err
}
status = statusSuccess
message = fmt.Sprintf("%s: %s annotation request to url %q processed successfully", operation, method, annoEndpoint)
c.logger.Log("%s request successful", operation)
return nil
}
================================================
FILE: pkg/gofr/datasource/oracle/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/oracle
go 1.25.0
require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/godror/godror v0.50.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.5.3 // indirect
cloud.google.com/go/pubsub v1.50.1 // indirect
cloud.google.com/go/pubsub/v2 v2.0.0 // indirect
filippo.io/edwards25519 v1.1.1 // indirect
github.com/VictoriaMetrics/easyproto v0.1.4 // indirect
github.com/XSAM/otelsql v0.41.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/eclipse/paho.mqtt.golang v1.5.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/godror/knownpb v0.3.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lib/pq v1.11.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pierrec/lz4/v4 v4.1.22 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/segmentio/kafka-go v0.4.50 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.1.2 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/api v0.270.0 // indirect
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.46.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/oracle/go.sum
================================================
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c=
cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ=
cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk=
cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
cloud.google.com/go/pubsub v1.50.1 h1:fzbXpPyJnSGvWXF1jabhQeXyxdbCIkXTpjXHy7xviBM=
cloud.google.com/go/pubsub v1.50.1/go.mod h1:6YVJv3MzWJUVdvQXG081sFvS0dWQOdnV+oTo++q/xFk=
cloud.google.com/go/pubsub/v2 v2.0.0 h1:0qS6mRJ41gD1lNmM/vdm6bR7DQu6coQcVwD+VPf0Bz0=
cloud.google.com/go/pubsub/v2 v2.0.0/go.mod h1:0aztFxNzVQIRSZ8vUr79uH2bS3jwLebwK6q1sgEub+E=
filippo.io/edwards25519 v1.1.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4=
github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc=
github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc=
github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710=
github.com/XSAM/otelsql v0.41.0 h1:uZifjQhZhv5EDYJh+IVk1DiYxQZJBlNSen0MBFnfxB8=
github.com/XSAM/otelsql v0.41.0/go.mod h1:NMQT0PiKoFILp9QgjQz+D5mvW+9mT0suR7OejqrtMaM=
github.com/alicebob/miniredis/v2 v2.37.0 h1:RheObYW32G1aiJIj81XVt78ZHJpHonHLHW7OLIshq68=
github.com/alicebob/miniredis/v2 v2.37.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-redis/redismock/v9 v9.2.0 h1:ZrMYQeKPECZPjOj5u9eyOjg8Nnb0BS9lkVIZ6IpsKLw=
github.com/go-redis/redismock/v9 v9.2.0/go.mod h1:18KHfGDK4Y6c2R0H38EUGWAdc7ZQS9gfYxc94k7rWT0=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/godror/godror v0.50.0 h1:c0ZnGSDFT12E8HJfQwxtqcmybaIkbqACNk4lIfkkESc=
github.com/godror/godror v0.50.0/go.mod h1:kTMcxZzRw73RT5kn9v3JkBK4kHI6dqowHotqV72ebU8=
github.com/godror/knownpb v0.3.0 h1:+caUdy8hTtl7X05aPl3tdL540TvCcaQA6woZQroLZMw=
github.com/godror/knownpb v0.3.0/go.mod h1:PpTyfJwiOEAzQl7NtVCM8kdPCnp3uhxsZYIzZ5PV4zU=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
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.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos=
github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU=
github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE=
github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/segmentio/kafka-go v0.4.50 h1:mcyC3tT5WeyWzrFbd6O374t+hmcu1NKt2Pu1L3QaXmc=
github.com/segmentio/kafka-go v0.4.50/go.mod h1:Y1gn60kzLEEaW28YshXyk2+VCUKbJ3Qr6DrnT3i4+9E=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.einride.tech/aip v0.73.0 h1:bPo4oqBo2ZQeBKo4ZzLb1kxYXTY1ysJhpvQyfuGzvps=
go.einride.tech/aip v0.73.0/go.mod h1:Mj7rFbmXEgw0dq1dqJ7JGMvYCZZVxmGOR3S4ZcV5LvQ=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0 h1:rbRJ8BBoVMsQShESYZ0FkvcITu8X8QNwJogcLUmDNNw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.62.0/go.mod h1:ru6KHrNtNHxM4nD/vd6QrLVWgKhxPYgblq4VAtNawTQ=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0 h1:U++6AfUpXXSILim4iH6Jb2oeK/mp7J4lNzzyO8Cx4Zw=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.66.0/go.mod h1:HVNUDNMGMeykut/2GZ++AZjglCqew/+Hf4lxRVqFFxQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs=
go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
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=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/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-20190412213103-97732733099d/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-20210615035016-665e8c7367d1/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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4=
google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 h1:VQZ/yAbAtjkHgH80teYd2em3xtIkkHd7ZhqfH2N9CsM=
google.golang.org/genproto v0.0.0-20260128011058-8636f8732409/go.mod h1:rxKD3IEILWEu3P44seeNOAwZN4SaoKaQ/2eTg4mM6EM=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
================================================
FILE: pkg/gofr/datasource/oracle/interface.go
================================================
package oracle
import "context"
type Connection interface {
Select(ctx context.Context, dest any, query string, args ...any) error
Exec(ctx context.Context, query string, args ...any) error
Ping(ctx context.Context) error
}
type Txn interface {
ExecContext(ctx context.Context, query string, args ...any) error
SelectContext(ctx context.Context, dest any, query string, args ...any) error
Commit() error
Rollback() error
}
================================================
FILE: pkg/gofr/datasource/oracle/logger.go
================================================
package oracle
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debugf(pattern string, args ...any)
Debug(args ...any)
Logf(pattern string, args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Type string `json:"type"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Args []any `json:"args,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "%-10s ORACLE %8dµs %s\n", l.Type, l.Duration, clean(l.Query))
}
func clean(query string) string {
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
return strings.TrimSpace(query)
}
================================================
FILE: pkg/gofr/datasource/oracle/logger_test.go
================================================
package oracle
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLoggingDataPresent(t *testing.T) {
queryLog := Log{
Type: "SELECT",
Query: "SELECT * FROM users",
Duration: 12345,
}
expected := "SELECT"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
================================================
FILE: pkg/gofr/datasource/oracle/metrics.go
================================================
package oracle
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
NewGauge(name, desc string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/oracle/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=oracle
//
// Package oracle is a generated GoMock package.
package oracle
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockConnection is a mock of Connection interface.
type MockConnection struct {
ctrl *gomock.Controller
recorder *MockConnectionMockRecorder
isgomock struct{}
}
// MockConnectionMockRecorder is the mock recorder for MockConnection.
type MockConnectionMockRecorder struct {
mock *MockConnection
}
// NewMockConnection creates a new mock instance.
func NewMockConnection(ctrl *gomock.Controller) *MockConnection {
mock := &MockConnection{ctrl: ctrl}
mock.recorder = &MockConnectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConnection) EXPECT() *MockConnectionMockRecorder {
return m.recorder
}
// Exec mocks base method.
func (m *MockConnection) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockConnectionMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockConnection)(nil).Exec), varargs...)
}
// Ping mocks base method.
func (m *MockConnection) Ping(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping.
func (mr *MockConnectionMockRecorder) Ping(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockConnection)(nil).Ping), ctx)
}
// Select mocks base method.
func (m *MockConnection) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockConnectionMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockConnection)(nil).Select), varargs...)
}
// MockTxn is a mock of Txn interface.
type MockTxn struct {
ctrl *gomock.Controller
recorder *MockTxnMockRecorder
isgomock struct{}
}
// MockTxnMockRecorder is the mock recorder for MockTxn.
type MockTxnMockRecorder struct {
mock *MockTxn
}
// NewMockTxn creates a new mock instance.
func NewMockTxn(ctrl *gomock.Controller) *MockTxn {
mock := &MockTxn{ctrl: ctrl}
mock.recorder = &MockTxnMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTxn) EXPECT() *MockTxnMockRecorder {
return m.recorder
}
// Commit mocks base method.
func (m *MockTxn) Commit() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Commit")
ret0, _ := ret[0].(error)
return ret0
}
// Commit indicates an expected call of Commit.
func (mr *MockTxnMockRecorder) Commit() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockTxn)(nil).Commit))
}
// ExecContext mocks base method.
func (m *MockTxn) ExecContext(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecContext", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecContext indicates an expected call of ExecContext.
func (mr *MockTxnMockRecorder) ExecContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockTxn)(nil).ExecContext), varargs...)
}
// Rollback mocks base method.
func (m *MockTxn) Rollback() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rollback")
ret0, _ := ret[0].(error)
return ret0
}
// Rollback indicates an expected call of Rollback.
func (mr *MockTxnMockRecorder) Rollback() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rollback", reflect.TypeOf((*MockTxn)(nil).Rollback))
}
// SelectContext mocks base method.
func (m *MockTxn) SelectContext(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "SelectContext", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// SelectContext indicates an expected call of SelectContext.
func (mr *MockTxnMockRecorder) SelectContext(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectContext", reflect.TypeOf((*MockTxn)(nil).SelectContext), varargs...)
}
================================================
FILE: pkg/gofr/datasource/oracle/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=oracle
//
// Package oracle is a generated GoMock package.
package oracle
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/oracle/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=oracle
//
// Package oracle is a generated GoMock package.
package oracle
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, desc)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, desc)
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/datasource/oracle/oracle.go
================================================
package oracle
import (
"context"
"database/sql"
"errors"
"fmt"
"reflect"
"time"
// Import for Oracle driver registration.
_ "github.com/godror/godror"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/container"
)
var (
_ container.OracleDB = (*Client)(nil)
_ container.OracleTx = (*oracleTx)(nil)
)
type Config struct {
Host string
Port int
Username string
Password string
Service string // or SID.
ConnectionString string
}
type Client struct {
conn Connection
config Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
var (
errStatusDown = errors.New("status down")
errInvalidDestType = errors.New("dest must be *[]map[string]any")
errNoConnection = errors.New("oracle connection not established")
errInvalidConnType = errors.New("invalid connection type")
)
const (
StatusUp = "UP"
StatusDown = "DOWN"
)
func New(config *Config) *Client {
return &Client{config: *config}
}
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
func (c *Client) Connect() {
// Validation: check if host is non-empty.
if c.config.Host == "" {
c.logger.Errorf("invalid OracleDB host: host is empty")
return
}
// Validation: check if port is within a valid range.
if c.config.Port <= 0 || c.config.Port > 65535 {
c.logger.Errorf("invalid OracleDB port: %v", c.config.Port)
return
}
c.logger.Debugf("connecting to OracleDB using connection string")
dsn := fmt.Sprintf(`user=%q password=%q connectString=%q libDir="/Users/zopdev/oracle-client/lib" wallet_location="/Users/zopdev/wallet"`,
c.config.Username, c.config.Password, c.config.ConnectionString)
db, err := sql.Open("godror", dsn)
if err != nil {
c.logger.Errorf("error while connecting to OracleDB: %v", err)
return
}
c.conn = &sqlConn{db: db}
if err = c.conn.Ping(context.Background()); err != nil {
c.logger.Errorf("ping failed with error %v", err)
} else {
c.logger.Logf("successfully connected to OracleDB")
}
}
// Exec executes a non-query SQL statement (such as INSERT, UPDATE, or DELETE) against the Oracle database.
// It enables callers to run statements that modify data or schema without returning any result sets.
// This includes common operations like data mutation, transaction management, or schema changes (DDL).
// The method provides a standardized entry point for write and schema operations across gofr’s supported databases,
// ensuring consistent usage patterns and compatibility with the gofr datasource interface conventions.
func (c *Client) Exec(ctx context.Context, query string, args ...any) error {
tracedCtx, span := c.addTrace(ctx, "exec", query)
err := c.conn.Exec(tracedCtx, query, args...)
defer c.sendOperationStats(time.Now(), "Exec", query, "exec", span, args...)
return err
}
// Select executes a SELECT query and scans the resulting rows into dest.
// The dest parameter should be a pointer to a slice or other suitable container.
// Query parameters can be passed via args to replace placeholders.
func (c *Client) Select(ctx context.Context, dest any, query string, args ...any) error {
tracedCtx, span := c.addTrace(ctx, "select", query)
if reflect.TypeOf(dest).Kind() != reflect.Ptr || reflect.TypeOf(dest).Elem().Kind() != reflect.Slice {
return errInvalidDestType
}
err := c.conn.Select(tracedCtx, dest, query, args...)
defer c.sendOperationStats(time.Now(), "Select", query, "select", span, args...)
return err
}
// oracleTx wraps a sql.Tx to implement the Txn interface.
type oracleTx struct {
tx *sql.Tx
logger Logger
}
// Begin starts a new transaction.
func (c *Client) Begin() (container.OracleTx, error) {
if c.conn == nil {
return nil, errNoConnection
}
start := time.Now()
// Get the underlying SQL DB.
sqlConn, ok := c.conn.(*sqlConn)
if !ok {
return nil, errInvalidConnType
}
// Begin a new SQL transaction.
tx, err := sqlConn.db.BeginTx(context.Background(), nil)
if err != nil {
c.logger.Errorf("failed to begin transaction: %v", err)
return nil, err
}
c.logger.Debug(&Log{
Type: "Begin",
Duration: time.Since(start).Microseconds(),
})
return &oracleTx{tx: tx, logger: c.logger}, nil
}
func (t *oracleTx) ExecContext(ctx context.Context, query string, args ...any) error {
start := time.Now()
_, err := t.tx.ExecContext(ctx, query, args...)
if t.logger != nil {
t.logger.Debug(&Log{
Type: "Tx-Exec",
Query: query,
Duration: time.Since(start).Microseconds(),
Args: args,
})
}
return err
}
// Extract the row scanning logic to reduce complexity.
func scanRows(rows *sql.Rows) ([]map[string]any, error) {
columns, err := rows.Columns()
if err != nil {
return nil, err
}
var results []map[string]any
for rows.Next() {
values := make([]any, len(columns))
valuePtrs := make([]any, len(columns))
for i := range columns {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return nil, err
}
rowMap := make(map[string]any)
for columnIndex, columnName := range columns {
rowMap[columnName] = values[columnIndex]
}
results = append(results, rowMap)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return results, nil
}
func (t *oracleTx) SelectContext(ctx context.Context, dest any, query string, args ...any) error {
start := time.Now()
if reflect.TypeOf(dest).Kind() != reflect.Ptr || reflect.TypeOf(dest).Elem().Kind() != reflect.Slice {
return errInvalidDestType
}
rows, err := t.tx.QueryContext(ctx, query, args...)
if err != nil {
return err
}
defer rows.Close()
results, err := scanRows(rows)
if err != nil {
return err
}
// Set the result to dest
type Destination = []map[string]any
p, ok := dest.(*Destination)
if !ok {
return errInvalidDestType
}
*p = results
if t.logger != nil {
t.logger.Debug(&Log{
Type: "Tx-Select",
Query: query,
Duration: time.Since(start).Microseconds(),
Args: args,
})
}
return nil
}
func (t *oracleTx) Commit() error {
start := time.Now()
err := t.tx.Commit()
if t.logger != nil {
t.logger.Debug(&Log{
Type: "Tx-Commit",
Duration: time.Since(start).Microseconds(),
})
if err != nil {
t.logger.Errorf("transaction commit failed: %v", err)
}
}
return err
}
func (t *oracleTx) Rollback() error {
start := time.Now()
err := t.tx.Rollback()
if t.logger != nil {
t.logger.Debug(&Log{
Type: "Tx-Rollback",
Duration: time.Since(start).Microseconds(),
})
if err != nil {
t.logger.Errorf("transaction rollback failed: %v", err)
}
}
return err
}
// sendOperationStats collects and sends operation metrics for monitoring purposes.
// It tracks execution times, counts, and error occurrences related to database operations.
func (c *Client) sendOperationStats(start time.Time, methodType, query, method string, span trace.Span, args ...any) {
duration := time.Since(start).Microseconds()
c.logger.Debug(&Log{
Type: methodType,
Query: query,
Duration: duration,
Args: args,
})
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("oracle.%v.duration", method), duration))
}
}
type Health struct {
Status string `json:"status,omitempty"`
// Details provide additional runtime metadata (host, service) to aid debugging.
Details map[string]any `json:"details,omitempty"`
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.config.Host
h.Details["database"] = c.config.Service
err := c.conn.Ping(ctx)
if err != nil {
h.Status = StatusDown
return &h, errStatusDown
}
h.Status = StatusUp
return &h, nil
}
// addTrace adds tracing information to the current context or operation.
// It records metadata such as correlation IDs or span details for distributed tracing.
func (c *Client) addTrace(ctx context.Context, method, query string) (context.Context, trace.Span) {
if c.tracer != nil {
ctxWithTrace, span := c.tracer.Start(ctx, fmt.Sprintf("oracle-%v", method))
span.SetAttributes(
attribute.String("oracle.query", query),
)
return ctxWithTrace, span
}
return ctx, nil
}
type sqlConn struct{ db *sql.DB }
func (s *sqlConn) Exec(ctx context.Context, query string, args ...any) error {
_, err := s.db.ExecContext(ctx, query, args...)
return err
}
func (s *sqlConn) Select(ctx context.Context, dest any, query string, args ...any) error {
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return err
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
return err
}
var results []map[string]any
for rows.Next() {
values := make([]any, len(columns))
valuePtrs := make([]any, len(columns))
for i := range columns {
valuePtrs[i] = &values[i]
}
if err := rows.Scan(valuePtrs...); err != nil {
return err
}
rowMap := make(map[string]any)
for columnIndex, columnName := range columns {
rowMap[columnName] = values[columnIndex]
}
results = append(results, rowMap)
}
if rows.Err() != nil {
return rows.Err()
}
// Set the result to dest (must be *[]map[string]any).
type Destination = []map[string]any
p, ok := dest.(*Destination)
if !ok {
return errInvalidDestType
}
*p = results
return nil
}
func (s *sqlConn) Ping(ctx context.Context) error { return s.db.PingContext(ctx) }
================================================
FILE: pkg/gofr/datasource/oracle/oracle_test.go
================================================
package oracle
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
)
var (
errExecTest = errors.New("exec err")
errSelectTest = errors.New("select err")
errPingTest = errors.New("ping error")
errTableNotExist = errors.New("ORA-00942: table or view does not exist")
errSomeTest = errors.New("some error")
errQueryTest = errors.New("query error")
)
func getOracleTestConnection(t *testing.T) (*MockConnection, *MockLogger, Client) {
t.Helper()
ctrl := gomock.NewController(t)
mockConn := NewMockConnection(ctrl)
mockMetric := NewMockMetrics(ctrl)
mockLogger := NewMockLogger(ctrl)
c := Client{conn: mockConn, config: Config{
Host: "localhost",
Port: 1521,
Username: "system",
Password: "password",
Service: "FREEPDB1",
}, logger: mockLogger, metrics: mockMetric}
return mockConn, mockLogger, c
}
func Test_Oracle_HealthUP(t *testing.T) {
mockConn, _, c := getOracleTestConnection(t)
mockConn.EXPECT().Ping(gomock.Any()).Return(nil)
resp, _ := c.HealthCheck(t.Context())
health, ok := resp.(*Health)
require.True(t, ok)
assert.Equal(t, "UP", health.Status)
}
func Test_Oracle_HealthDOWN(t *testing.T) {
mockConn, _, c := getOracleTestConnection(t)
mockConn.EXPECT().Ping(gomock.Any()).Return(sql.ErrConnDone)
resp, err := c.HealthCheck(t.Context())
require.ErrorIs(t, err, errStatusDown)
health, ok := resp.(*Health)
require.True(t, ok)
assert.Equal(t, "DOWN", health.Status)
}
func Test_Oracle_Exec(t *testing.T) {
mockConn, mockLogger, c := getOracleTestConnection(t)
ctx := t.Context()
mockConn.EXPECT().Exec(ctx, "INSERT INTO users (id, name) VALUES (?, ?)", 1, "user").Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
err := c.Exec(ctx, "INSERT INTO users (id, name) VALUES (?, ?)", 1, "user")
require.NoError(t, err)
}
func Test_Oracle_Select(t *testing.T) {
mockConn, mockLogger, c := getOracleTestConnection(t)
type User struct {
ID int
Name string
}
ctx := t.Context()
var users []User
mockConn.EXPECT().Select(ctx, &users, "SELECT * FROM users").Return(nil)
mockLogger.EXPECT().Debug(gomock.Any())
err := c.Select(ctx, &users, "SELECT * FROM users")
require.NoError(t, err)
}
func Test_New_ReturnsClient(t *testing.T) {
cfg := Config{Host: "h", Port: 1, Username: "u", Password: "p", Service: "s"}
c := New(&cfg)
require.NotNil(t, c)
assert.Equal(t, cfg, c.config)
}
func Test_UseLogger_SetsLoggerWhenCorrectType(t *testing.T) {
c := New(&Config{})
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLog := NewMockLogger(ctrl)
c.UseLogger(mockLog)
assert.Equal(t, mockLog, c.logger)
c.UseLogger("not a logger")
// logger should remain unchanged.
assert.Equal(t, mockLog, c.logger)
}
func Test_UseMetrics_SetsMetricsWhenCorrectType(t *testing.T) {
c := New(&Config{})
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
c.UseMetrics(mockMetrics)
assert.Equal(t, mockMetrics, c.metrics)
c.UseMetrics(123) // ignored.
assert.Equal(t, mockMetrics, c.metrics)
}
func Test_UseTracer_SetsTracerWhenCorrectType(t *testing.T) {
c := New(&Config{})
tracerMock := noop.NewTracerProvider().Tracer("test") // or custom mock.
c.UseTracer(tracerMock)
assert.Equal(t, tracerMock, c.tracer)
c.UseTracer("wrong")
// Should ignore, tracer remains tracerMock.
assert.Equal(t, tracerMock, c.tracer)
}
func Test_Connect_SuccessAndFailure(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{Host: "h", Port: 1, Username: "u", Password: "p", Service: "s"})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
// --- Fail sql.Open ---
c.config.Username = "baduser"
mockLogger.EXPECT().Debugf(gomock.Any())
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
// --- Success ---
c.config.Username = "system"
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Logf(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
c.Connect()
require.NotNil(t, c.conn)
}
func Test_Exec_ErrorPropagation(t *testing.T) {
mockConn, mockLogger, c := getOracleTestConnection(t)
ctx := t.Context()
mockLogger.EXPECT().Debug(gomock.Any())
mockConn.EXPECT().Exec(ctx, "QUERY", gomock.Any()).Return(errExecTest)
err := c.Exec(ctx, "QUERY", 123)
require.Error(t, err)
assert.Contains(t, err.Error(), errExecTest.Error())
}
func Test_Select_InvalidDestType(t *testing.T) {
mockConn, _, c := getOracleTestConnection(t)
mockConn.EXPECT().Select(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
err := c.Select(t.Context(), "invalid-type", "SELECT 1")
require.Equal(t, errInvalidDestType, err)
}
func Test_Select_ErrorPropagation(t *testing.T) {
mockConn, mockLogger, c := getOracleTestConnection(t)
ctx := t.Context()
mockLogger.EXPECT().Debug(gomock.Any())
mockConn.EXPECT().Select(ctx, gomock.Any(), "QUERY", gomock.Any()).Return(errSelectTest)
var result []map[string]any
err := c.Select(ctx, &result, "QUERY", 123)
require.Error(t, err)
assert.Contains(t, err.Error(), errSelectTest.Error())
}
func Test_addTrace_WithAndWithoutTracer(t *testing.T) {
c := New(&Config{})
ctx := t.Context()
ctx2, span := c.addTrace(ctx, "method", "query")
assert.Nil(t, span)
assert.Equal(t, ctx, ctx2)
tracerMock := noop.NewTracerProvider().Tracer("test")
c.UseTracer(tracerMock)
ctx3, span2 := c.addTrace(ctx, "method", "query")
require.NotNil(t, span2)
span2.End() // manually end.
assert.NotEqual(t, ctx, ctx3) // ctx with span.
}
func Test_sendOperationStats_WithAndWithoutSpan(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
c := New(&Config{})
c.UseLogger(mockLogger)
c.UseMetrics(mockMetrics)
mockLogger.EXPECT().Debug(gomock.Any())
// span == nil case; no call to span.End().
c.sendOperationStats(time.Now(), "Exec", "SELECT 1", "exec", nil)
// With mock span.
tracer := noop.NewTracerProvider().Tracer("test")
c.UseTracer(tracer)
_, span := c.addTrace(t.Context(), "exec", "SELECT 1")
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
c.sendOperationStats(time.Now(), "Exec", "SELECT 1", "exec", span)
}
func Test_Ping_ReturnsErrorOrNil(t *testing.T) {
mockConn, _, c := getOracleTestConnection(t)
ctx := t.Context()
mockConn.EXPECT().Ping(ctx).Return(nil)
err := c.conn.Ping(ctx)
require.NoError(t, err)
mockConn.EXPECT().Ping(ctx).Return(errPingTest)
err = c.conn.Ping(ctx)
require.Error(t, err)
}
func Test_LoggingWithDebugf_Errorf_Logf(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debugf("debug pattern %s", gomock.Any())
mockLogger.EXPECT().Errorf("error pattern %s", gomock.Any())
mockLogger.EXPECT().Logf("log pattern %s", gomock.Any())
mockLogger.Debugf("debug pattern %s", "arg")
mockLogger.Errorf("error pattern %s", "arg")
mockLogger.Logf("log pattern %s", "arg")
}
func Test_MetricsCalls(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
ctx := t.Context()
mockMetrics.EXPECT().NewHistogram("name", "desc", gomock.Any()).Times(1)
mockMetrics.EXPECT().NewGauge("gauge", "desc").Times(1)
mockMetrics.EXPECT().RecordHistogram(ctx, "hist", float64(123), "label").Times(1)
mockMetrics.EXPECT().SetGauge("gauge", float64(456), "label").Times(1)
mockMetrics.NewHistogram("name", "desc", 0.1, 1.0)
mockMetrics.NewGauge("gauge", "desc")
mockMetrics.RecordHistogram(ctx, "hist", 123, "label")
mockMetrics.SetGauge("gauge", 456, "label")
}
func Test_sqlConn_Exec(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
s := &sqlConn{db: db}
mock.ExpectExec("INSERT INTO users").WithArgs(1, "gofr").
WillReturnResult(sqlmock.NewResult(1, 1))
err = s.Exec(t.Context(), "INSERT INTO users (id, name) VALUES (?, ?)", 1, "gofr")
require.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_sqlConn_Select(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
s := &sqlConn{db: db}
rows := sqlmock.NewRows([]string{"id", "name"}).
AddRow(1, "gofr").
AddRow(2, "dev")
mock.ExpectQuery("SELECT id, name FROM users").WillReturnRows(rows)
var result []map[string]any
err = s.Select(t.Context(), &result, "SELECT id, name FROM users")
require.NoError(t, err)
assert.Len(t, result, 2)
assert.Equal(t, "gofr", result[0]["name"])
assert.Equal(t, int64(1), result[0]["id"])
assert.Equal(t, "dev", result[1]["name"])
assert.Equal(t, int64(2), result[1]["id"])
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_Oracle_InvalidHostName(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{
Host: "invalid.hostname",
Port: 1521,
Username: "system",
Password: "password",
Service: "FREEPDB1",
})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
mockLogger.EXPECT().Debugf(gomock.Any())
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
c.Connect()
}
func Test_sqlConn_InvalidInsertQuery(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
s := &sqlConn{db: db}
mock.ExpectExec("INSERT INTO bad_table").WillReturnError(errTableNotExist)
err = s.Exec(t.Context(), "INSERT INTO bad_table (id) VALUES (?)", 1)
require.Error(t, err)
assert.Contains(t, err.Error(), "table or view does not exist")
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_Oracle_ConnectionTimeout(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{
Host: "10.255.255.1",
Port: 1521,
Username: "system",
Password: "password",
Service: "FREEPDB1",
})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
mockLogger.EXPECT().Debugf(gomock.Any())
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
c.Connect()
}
func Test_Oracle_ConnectionError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{
Host: "localhost",
Port: 1521,
Username: "wrong_user",
Password: "wrong_pass",
Service: "FREEPDB1",
})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
mockLogger.EXPECT().Debugf(gomock.Any())
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
c.Connect()
}
func Test_Connect_InvalidHost(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{Host: "", Port: 1521, Username: "u", Password: "p", Service: "s"})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
mockLogger.EXPECT().Errorf("invalid OracleDB host: host is empty")
c.Connect()
}
func Test_Connect_InvalidPort(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c := New(&Config{Host: "h", Port: 0, Username: "u", Password: "p", Service: "s"})
mockLogger := NewMockLogger(ctrl)
c.UseLogger(mockLogger)
mockLogger.EXPECT().Errorf("invalid OracleDB port: %v", 0)
c.Connect()
}
func Test_sqlConn_Exec_Errors(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
s := &sqlConn{db: db}
mock.ExpectExec("BAD QUERY").WillReturnError(errSomeTest)
err := s.Exec(t.Context(), "BAD QUERY")
require.Error(t, err)
}
func Test_sqlConn_Select_ColumnsError(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
s := &sqlConn{db: db}
mock.ExpectQuery("SELECT").WillReturnError(errQueryTest)
var dest []map[string]any
err := s.Select(t.Context(), &dest, "SELECT * FROM dual")
require.Error(t, err)
}
func Test_sqlConn_Ping(t *testing.T) {
db, _, _ := sqlmock.New()
defer db.Close()
s := &sqlConn{db: db}
err := s.Ping(t.Context())
require.NoError(t, err)
}
func Test_Oracle_Begin_Success(t *testing.T) {
// Create a mock DB with sqlmock
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
// Create client with mocked connection
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
c := Client{
conn: &sqlConn{db: db},
logger: mockLogger,
}
// Expect Begin to be called and debug log to be recorded
mock.ExpectBegin()
mockLogger.EXPECT().Debug(gomock.Any())
// Call Begin
tx, err := c.Begin()
require.NoError(t, err)
require.NotNil(t, tx)
// Verify expectations
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_Oracle_Begin_NoConnection(t *testing.T) {
c := Client{conn: nil}
tx, err := c.Begin()
require.Error(t, err)
require.Nil(t, tx)
assert.ErrorIs(t, err, errNoConnection)
}
// func Test_Oracle_Begin_InvalidConnType(t *testing.T) {
// c := Client{
// conn: &struct{}{}, // Not a sqlConn
// }
//
// tx, err := c.Begin()
// require.Error(t, err)
// require.Nil(t, tx)
// assert.ErrorIs(t, err, errInvalidConnType)
// }
func Test_OracleTx_ExecContext(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
// Begin a transaction
mock.ExpectBegin()
sqlTx, err := db.BeginTx(context.Background(), nil)
require.NoError(t, err)
// Create mock logger
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any())
// Create oracle transaction
tx := &oracleTx{tx: sqlTx, logger: mockLogger}
// Set up expectations for the exec
mock.ExpectExec("INSERT INTO users").WithArgs(1, "test").
WillReturnResult(sqlmock.NewResult(1, 1))
// Execute the query
err = tx.ExecContext(context.Background(), "INSERT INTO users (id, name) VALUES (?, ?)", 1, "test")
require.NoError(t, err)
// Verify expectations
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_OracleTx_SelectContext(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
// Begin a transaction
mock.ExpectBegin()
sqlTx, err := db.BeginTx(context.Background(), nil)
require.NoError(t, err)
// Create mock logger
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any())
// Create oracle transaction
tx := &oracleTx{tx: sqlTx, logger: mockLogger}
// Set up expectations for the query
columns := []string{"id", "name"}
rows := sqlmock.NewRows(columns).
AddRow(1, "gofr").
AddRow(2, "dev")
mock.ExpectQuery("SELECT id, name FROM users").WillReturnRows(rows)
// Execute the query
var result []map[string]any
err = tx.SelectContext(context.Background(), &result, "SELECT id, name FROM users")
require.NoError(t, err)
// Verify results
require.Len(t, result, 2)
assert.Equal(t, "gofr", result[0]["name"])
assert.Equal(t, int64(1), result[0]["id"])
// Verify expectations
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_OracleTx_Commit(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
mock.ExpectBegin()
sqlTx, err := db.BeginTx(context.Background(), nil)
require.NoError(t, err)
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any())
tx := &oracleTx{tx: sqlTx, logger: mockLogger}
mock.ExpectCommit()
err = tx.Commit()
require.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func Test_OracleTx_Rollback(t *testing.T) {
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()
mock.ExpectBegin()
sqlTx, err := db.BeginTx(context.Background(), nil)
require.NoError(t, err)
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug(gomock.Any())
tx := &oracleTx{tx: sqlTx, logger: mockLogger}
mock.ExpectRollback()
err = tx.Rollback()
require.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/eventhub.go
================================================
package eventhub
import (
"context"
"errors"
"strings"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs/checkpoints"
"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// code reference from https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-go-get-started-send
// metrics are being registered in the container, and we are using the same metrics so we have not re-registered the metrics here.
// It is different from other datasources.
var (
ErrNoMsgReceived = errors.New("no message received")
ErrTopicMismatch = errors.New("topic should be same as Event Hub name")
errClientNotConnected = errors.New("eventhub client not connected")
errEmptyTopic = errors.New("topic name cannot be empty")
)
const (
defaultQueryTimeout = 30 * time.Second
eventHubPropsTimeout = 2 * time.Second
basicTierMaxPartitions = 2
basicTierReceiveTimeout = 3 * time.Second
)
type Config struct {
ConnectionString string
ContainerConnectionString string
StorageServiceURL string
StorageContainerName string
EventhubName string
// if not provided, it will read from the $Default consumergroup.
ConsumerGroup string
// the following configs are for advance setup of the Event Hub.
StorageOptions *container.ClientOptions
BlobStoreOptions *checkpoints.BlobStoreOptions
ConsumerOptions *azeventhubs.ConsumerClientOptions
ProducerOptions *azeventhubs.ProducerClientOptions
}
type Client struct {
producer *azeventhubs.ProducerClient
consumer *azeventhubs.ConsumerClient
// we are using a processor such that to keep consuming the events from all the different partitions.
processor *azeventhubs.Processor
// a checkpoint is being called while committing the event received from the event.
checkPoint *checkpoints.BlobStore
// processorCtx is being stored such that to gracefully shutting down the application.
processorCtx context.CancelFunc
cfg Config
logger Logger
metrics Metrics
tracer trace.Tracer
}
// New Creates the client for Event Hub.
//
//nolint:gocritic // cfg is a configuration struct.
func New(cfg Config) *Client {
return &Client{
cfg: cfg,
}
}
//nolint:gocritic // cfg is a configuration struct.
func (c *Client) validConfigs(cfg Config) bool {
ok := true
if cfg.EventhubName == "" {
ok = false
c.logger.Error("eventhubName cannot be an empty")
}
if cfg.ConnectionString == "" {
ok = false
c.logger.Error("connectionString cannot be an empty")
}
if cfg.StorageServiceURL == "" {
ok = false
c.logger.Error("storageServiceURL cannot be an empty")
}
if cfg.StorageContainerName == "" {
ok = false
c.logger.Error("storageContainerName cannot be an empty")
}
if cfg.ContainerConnectionString == "" {
ok = false
c.logger.Error("containerConnectionString cannot be an empty")
}
return ok
}
// UseLogger sets the logger for the Event Hub client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the Event Hub client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the Event Hub client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Connect establishes a connection to Event Hub and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
if !c.validConfigs(c.cfg) {
return
}
if c.cfg.ConsumerGroup == "" {
c.cfg.ConsumerGroup = azeventhubs.DefaultConsumerGroup
c.logger.Debugf("Using default consumer group: %s", c.cfg.ConsumerGroup)
} else {
c.logger.Debugf("Using provided consumer group: %s", c.cfg.ConsumerGroup)
}
c.logger.Debug("Event Hub connection started using connection string")
producerClient, err := azeventhubs.NewProducerClientFromConnectionString(c.cfg.ConnectionString,
c.cfg.EventhubName, c.cfg.ProducerOptions)
if err != nil {
c.logger.Errorf("error occurred while creating producer client %v", err)
return
}
c.logger.Debug("Event Hub producer client setup success")
containerClient, err := container.NewClientFromConnectionString(c.cfg.ContainerConnectionString, c.cfg.StorageContainerName,
c.cfg.StorageOptions)
if err != nil {
c.logger.Errorf("error occurred while creating container client %v", err)
return
}
c.logger.Debug("Event Hub container client setup success")
// create a checkpoint store that will be used by the event hub
checkpointStore, err := checkpoints.NewBlobStore(containerClient, c.cfg.BlobStoreOptions)
if err != nil {
c.logger.Errorf("error occurred while creating blobstore %v", err)
return
}
c.logger.Debug("Event Hub blobstore client setup success")
// create a consumer client using a connection string to the namespace and the event hub
consumerClient, err := azeventhubs.NewConsumerClientFromConnectionString(c.cfg.ConnectionString, c.cfg.EventhubName,
c.cfg.ConsumerGroup, c.cfg.ConsumerOptions)
if err != nil {
c.logger.Errorf("error occurred while creating consumer client %v", err)
return
}
c.logger.Debug("Event Hub consumer client setup success")
// create a processor to receive and process events
processor, err := azeventhubs.NewProcessor(consumerClient, checkpointStore, nil)
if err != nil {
c.logger.Errorf("error occurred while creating processor %v", err)
return
}
c.logger.Debug("Event Hub processor setup success")
processorCtx, processorCancel := context.WithCancel(context.TODO())
c.processorCtx = processorCancel
// it is being run in a go-routine as it is a never ending process and has to be kept running to subscribe to events.
go func() {
if err = processor.Run(processorCtx); err != nil {
c.logger.Errorf("error occurred while running processor %v", err)
return
}
c.logger.Debug("Event Hub processor running successfully")
}()
c.processor = processor
c.producer = producerClient
c.consumer = consumerClient
c.checkPoint = checkpointStore
c.logger.Debug("Event Hub client initialization complete")
}
// Subscribe checks all partitions for the first available event and returns it.
func (c *Client) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
if c.producer == nil || c.consumer == nil || c.processor == nil {
return nil, errClientNotConnected
}
// Try processor approach first
partitionClient := c.processor.NextPartitionClient(ctx)
if partitionClient != nil {
return c.processEventsFromPartitionClient(ctx, topic, partitionClient)
}
// Fallback to direct consumer approach if processor doesn't have partition clients ready
return c.subscribeDirectFromConsumer(ctx, topic)
}
// processEventsFromPartitionClient processes events using the processor partition client.
func (c *Client) processEventsFromPartitionClient(ctx context.Context, topic string,
partitionClient *azeventhubs.ProcessorPartitionClient) (*pubsub.Message, error) {
defer closePartitionResources(ctx, partitionClient)
timeout := c.getReceiveTimeout()
receiveCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "subscription_name", partitionClient.PartitionID())
start := time.Now()
// ReceiveEvents signature: ReceiveEvents(ctx context.Context, count int, options *ReceiveEventsOptions) ([]*ReceivedEventData, error)
// Note: ReceiveEventsOptions is nil for default behavior.
events, err := partitionClient.ReceiveEvents(receiveCtx, 1, nil)
if err != nil {
if !errors.Is(err, context.DeadlineExceeded) {
c.logger.Debugf("Error receiving events from partition %s: %v", partitionClient.PartitionID(), err)
}
return nil, nil
}
if len(events) == 0 {
return nil, nil
}
// Create message from the first event
msg := pubsub.NewMessage(ctx)
msg.Value = events[0].Body
msg.Committer = &Message{
event: events[0],
processor: partitionClient,
logger: c.logger,
}
msg.Topic = topic
msg.MetaData = events[0].EventData
end := time.Since(start)
c.logger.Debug(&Log{
Mode: "SUB",
MessageValue: strings.Join(strings.Fields(string(msg.Value)), " "),
Topic: topic,
Host: c.cfg.EventhubName + ":" + c.cfg.ConsumerGroup + ":" + partitionClient.PartitionID(),
PubSubBackend: "EVHUB",
Time: end.Microseconds(),
})
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic, "subscription_name", partitionClient.PartitionID())
return msg, nil
}
// subscribeDirectFromConsumer uses consumer client directly as fallback.
func (c *Client) subscribeDirectFromConsumer(ctx context.Context, topic string) (*pubsub.Message, error) {
// Get partition information
props, err := c.consumer.GetEventHubProperties(ctx, nil)
if err != nil {
c.logger.Errorf("Failed to get Event Hub properties: %v", err)
return nil, err
}
// Try each partition for available messages - use LATEST to avoid old messages
for _, partitionID := range props.PartitionIDs {
msg, err := c.tryReadFromPartition(ctx, partitionID, topic)
if err != nil {
c.logger.Debugf("Error reading from partition %s: %v", partitionID, err)
continue
}
if msg != nil {
return msg, nil
}
}
return nil, nil
}
// tryReadFromPartition attempts to read a single message from specified partition.
func (c *Client) tryReadFromPartition(ctx context.Context, partitionID, topic string) (*pubsub.Message, error) {
// Create partition client for direct read with LATEST position to avoid old messages.
partitionClient, err := c.consumer.NewPartitionClient(partitionID, &azeventhubs.PartitionClientOptions{
StartPosition: azeventhubs.StartPosition{
Latest: to.Ptr(true), // Use Latest to only get new messages
},
})
if err != nil {
return nil, err
}
defer partitionClient.Close(ctx)
timeout := c.getReceiveTimeout()
receiveCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "subscription_name", partitionID)
start := time.Now()
events, err := partitionClient.ReceiveEvents(receiveCtx, 1, nil)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return nil, err
}
if len(events) == 0 {
return nil, nil // No message available in this partition
}
// Create message from event
msg := pubsub.NewMessage(ctx)
msg.Value = events[0].Body
msg.Committer = &Message{
event: events[0],
processor: nil, // Not using processor for direct reads
logger: c.logger,
}
msg.Topic = topic
msg.MetaData = events[0].EventData
end := time.Since(start)
c.logger.Debug(&Log{
Mode: "SUB",
MessageValue: strings.Join(strings.Fields(string(msg.Value)), " "),
Topic: topic,
Host: c.cfg.EventhubName + ":" + c.cfg.ConsumerGroup + ":" + partitionID,
PubSubBackend: "EVHUB",
Time: end.Microseconds(),
})
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic, "subscription_name", partitionID)
return msg, nil
}
// getReceiveTimeout returns appropriate timeout based on Event Hub characteristics.
func (c *Client) getReceiveTimeout() time.Duration {
// Check if this might be basic tier by examining partition count
if c.isLikelyBasicTier() {
return basicTierReceiveTimeout
}
return time.Second
}
// isLikelyBasicTier detects basic tier characteristics.
func (c *Client) isLikelyBasicTier() bool {
if c.consumer == nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), eventHubPropsTimeout)
defer cancel()
props, err := c.consumer.GetEventHubProperties(ctx, nil)
if err != nil {
return false // Default to standard behavior on error
}
// Basic tier typically has fewer partitions
return len(props.PartitionIDs) <= basicTierMaxPartitions
}
func closePartitionResources(ctx context.Context, partitionClient *azeventhubs.ProcessorPartitionClient) {
partitionClient.Close(ctx)
}
func (c *Client) Publish(ctx context.Context, topic string, message []byte) error {
if topic != c.cfg.EventhubName {
return ErrTopicMismatch
}
c.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
newBatchOptions := &azeventhubs.EventDataBatchOptions{}
batch, err := c.producer.NewEventDataBatch(ctx, newBatchOptions)
if err != nil {
c.logger.Errorf("failed to create event batch %v", err)
return err
}
data := []*azeventhubs.EventData{{
Body: message,
}}
for i := 0; i < len(data); i++ {
err = batch.AddEventData(data[i], nil)
if err != nil {
c.logger.Debugf("failed to add event data to batch %v", err)
}
}
start := time.Now()
// send the batch of events to the event hub
if err := c.producer.SendEventDataBatch(ctx, batch, nil); err != nil {
return err
}
end := time.Since(start)
c.logger.Debug(&Log{
Mode: "PUB",
MessageValue: strings.Join(strings.Fields(string(message)), " "),
Topic: topic,
Host: c.cfg.EventhubName,
PubSubBackend: "EVHUB",
Time: end.Microseconds(),
})
c.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
func (c *Client) Health() datasource.Health {
c.logger.Error("health-check not implemented for Event Hub")
return datasource.Health{}
}
func (c *Client) CreateTopic(_ context.Context, name string) error {
// For Event Hub, creating topics is not supported, but we don't want to fail migrations
if name == "gofr_migrations" {
return nil
}
c.logger.Error("topic creation is not supported in Event Hub")
return nil
}
func (c *Client) DeleteTopic(context.Context, string) error {
c.logger.Error("topic deletion is not supported in Event Hub")
return nil
}
// Query retrieves messages from Azure Event Hub.
func (c *Client) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if c.consumer == nil {
return nil, errClientNotConnected
}
if query == "" {
return nil, errEmptyTopic
}
if query != c.cfg.EventhubName {
return nil, ErrTopicMismatch
}
startPosition, limit := c.parseQueryArgs(args...)
// Use provided context or add default timeout
readCtx := ctx
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
var cancel context.CancelFunc
readCtx, cancel = context.WithTimeout(ctx, defaultQueryTimeout)
defer cancel()
}
return c.readMessages(readCtx, startPosition, limit)
}
func (c *Client) GetEventHubName() string {
return c.cfg.EventhubName
}
// Close safely closes all Event Hub clients and resources.
func (c *Client) Close() error {
var lastErr error
// Close producer if it exists
if c.producer != nil {
if err := c.producer.Close(context.Background()); err != nil {
c.logger.Errorf("failed to close Event Hub producer: %v", err)
lastErr = err
}
}
// Close consumer if it exists
if c.consumer != nil {
if err := c.consumer.Close(context.Background()); err != nil {
c.logger.Errorf("failed to close Event Hub consumer: %v", err)
lastErr = err
}
}
// Cancel processor context if it exists
if c.processorCtx != nil {
c.processorCtx()
c.logger.Debug("Event Hub processor context canceled")
}
return lastErr
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/eventhub_test.go
================================================
package eventhub
import (
"context"
"net"
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
"github.com/coder/websocket"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/testutil"
)
func TestConnect(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(NewMockMetrics(ctrl))
client.UseTracer(otel.GetTracerProvider().Tracer("gofr-eventhub"))
client.Connect()
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Connection Failed")
}
func TestConfigValidation(t *testing.T) {
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
client := New(Config{})
client.UseLogger(mockLogger)
mockLogger.EXPECT().Error("eventhubName cannot be an empty")
mockLogger.EXPECT().Error("connectionString cannot be an empty")
mockLogger.EXPECT().Error("storageServiceURL cannot be an empty")
mockLogger.EXPECT().Error("storageContainerName cannot be an empty")
mockLogger.EXPECT().Error("containerConnectionString cannot be an empty")
client.Connect()
require.True(t, mockLogger.ctrl.Satisfied(), "Config Validation Failed")
}
func TestConnect_ProducerError(t *testing.T) {
ctrl := gomock.NewController(t)
logs := testutil.StdoutOutputForFunc(func() {
cfg := getTestConfigs()
cfg.ConnectionString += ";EntityPath="
client := New(cfg)
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
client.UseMetrics(NewMockMetrics(ctrl))
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf("error occurred while creating producer client %v", gomock.Any())
client.Connect()
})
require.NotContains(t, logs, "Error")
}
func TestConnect_ContainerError(t *testing.T) {
ctrl := gomock.NewController(t)
logs := testutil.StdoutOutputForFunc(func() {
cfg := getTestConfigs()
cfg.ContainerConnectionString += ""
client := New(cfg)
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
client.UseMetrics(NewMockMetrics(ctrl))
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf("error occurred while creating container client %v", gomock.Any())
client.Connect()
})
require.NotContains(t, logs, "Error")
}
func TestPublish_FailedBatchCreation(t *testing.T) {
// TODO: This test is skipped due to long runtime and occasional panic, causing pipeline failures.
// It needs modification in the future.
t.Skip("disabled on 2024-12-11, TODO: cause of occasional panic in this test needs to be addressed.")
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
mockMetrics.EXPECT().IncrementCounter(t.Context(), "app_pubsub_publish_total_count", "topic", client.cfg.EventhubName)
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
err := client.Publish(t.Context(), client.cfg.EventhubName, []byte("my-message"))
require.ErrorContains(t, err, "failed to WebSocket dial: failed to send handshake request: ",
"Eventhub Publish Failed Batch Creation")
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Publish Failed Batch Creation")
}
func TestPublish_FailedInvalidTopic(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
err := client.Publish(t.Context(), "random topic", []byte("my-message"))
require.Equal(t, "topic should be same as Event Hub name", err.Error(), "Event Hub Publish Failed Invalid Topic")
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Publish Failed Invalid Topic")
}
func Test_CreateTopic(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
mockLogger.EXPECT().Error("topic creation is not supported in Event Hub")
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
err := client.CreateTopic(t.Context(), "random-topic")
require.NoError(t, err, "Event Hub Topic Creation not allowed failed")
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Topic Creation not allowed failed")
}
func Test_DeleteTopic(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
mockLogger.EXPECT().Error("topic deletion is not supported in Event Hub")
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
err := client.DeleteTopic(t.Context(), "random-topic")
require.NoError(t, err, "Event Hub Topic Deletion not allowed failed")
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Topic Deletion not allowed failed")
}
func Test_HealthCheck(t *testing.T) {
ctrl := gomock.NewController(t)
client := New(getTestConfigs())
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug("Event Hub connection started using connection string")
mockLogger.EXPECT().Debug("Event Hub producer client setup success")
mockLogger.EXPECT().Debug("Event Hub container client setup success")
mockLogger.EXPECT().Debug("Event Hub blobstore client setup success")
mockLogger.EXPECT().Debug("Event Hub consumer client setup success")
mockLogger.EXPECT().Debug("Event Hub processor setup success")
mockLogger.EXPECT().Debug("Event Hub processor running successfully").AnyTimes()
mockLogger.EXPECT().Error("health-check not implemented for Event Hub")
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.Connect()
_ = client.Health()
require.True(t, mockLogger.ctrl.Satisfied(), "Event Hub Topic Deletion not allowed failed")
}
func getTestConfigs() Config {
newWebSocketConnFn := func(ctx context.Context, args azeventhubs.WebSocketConnParams) (net.Conn, error) {
opts := &websocket.DialOptions{
Subprotocols: []string{"amqp"},
}
wssConn, _, err := websocket.Dial(ctx, args.Host, opts)
if err != nil {
return nil, err
}
return websocket.NetConn(ctx, wssConn, websocket.MessageBinary), nil
}
// For more details on the configuration refer :
// https://github.com/Azure/azure-sdk-for-go/blob/main/sdk/messaging/azeventhubs/consumer_client_test.go
return Config{
ConnectionString: "Endpoint=sb://.servicebus.windows.net/;SharedAccessKeyName=;SharedAccessKey=",
ContainerConnectionString: "DefaultEndpointsProtocol=https;AccountName=;AccountKey=" +
"SGVsbG8gV29ybGQ=",
StorageServiceURL: "core.windows.net",
StorageContainerName: "",
EventhubName: "event-hub-name",
ConsumerOptions: &azeventhubs.ConsumerClientOptions{
RetryOptions: azeventhubs.RetryOptions{},
},
ProducerOptions: &azeventhubs.ProducerClientOptions{
NewWebSocketConn: newWebSocketConnFn,
},
}
}
func TestGetEventHubName(t *testing.T) {
expectedName := "test-event-hub"
client := New(Config{
EventhubName: expectedName,
})
require.Equal(t, expectedName, client.GetEventHubName(),
"GetEventHubName should return the configured EventhubName")
}
func TestQuery_Failures(t *testing.T) {
testCases := []struct {
name string
setupClient func() *Client
query string
expectedError error
}{
{
name: "consumer_not_connected",
setupClient: func() *Client {
return New(Config{
EventhubName: "test-hub",
})
},
query: "test-hub",
expectedError: errClientNotConnected,
},
{
name: "empty_topic",
setupClient: func() *Client {
client := New(Config{
EventhubName: "test-hub",
})
client.consumer = &azeventhubs.ConsumerClient{}
return client
},
query: "",
expectedError: errEmptyTopic,
},
{
name: "topic_mismatch",
setupClient: func() *Client {
client := New(Config{
EventhubName: "test-hub",
})
client.consumer = &azeventhubs.ConsumerClient{} // Just needs to be non-nil
return client
},
query: "different-hub",
expectedError: ErrTopicMismatch,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
client := tc.setupClient()
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
result, err := client.Query(t.Context(), tc.query)
require.Nil(t, result, "Result should be nil for failure case: %s", tc.name)
require.Equal(t, tc.expectedError, err, "Error should match expected for case: %s", tc.name)
})
}
}
func TestQuery_ContextWithDeadline(t *testing.T) {
// Test that when context has deadline, we respect it
ctrl := gomock.NewController(t)
client := New(Config{
EventhubName: "test-hub",
})
client.consumer = &azeventhubs.ConsumerClient{} // Just needs to be non-nil
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
// Execute Query (will fail with ErrTopicMismatch before it gets to the deadline handling)
_, err := client.Query(ctx, "different-hub")
// Verify it failed for the right reason
require.Equal(t, ErrTopicMismatch, err)
require.True(t, mockLogger.ctrl.Satisfied())
}
func Test_ValidConfigs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
client := New(Config{})
client.UseLogger(mockLogger)
mockLogger.EXPECT().Error("eventhubName cannot be an empty")
mockLogger.EXPECT().Error("connectionString cannot be an empty")
mockLogger.EXPECT().Error("storageServiceURL cannot be an empty")
mockLogger.EXPECT().Error("storageContainerName cannot be an empty")
mockLogger.EXPECT().Error("containerConnectionString cannot be an empty")
valid := client.validConfigs(Config{})
require.False(t, valid, "validConfigs should return false for invalid configuration")
}
func Test_Health(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
client := New(getTestConfigs())
client.UseLogger(mockLogger)
mockLogger.EXPECT().Error("health-check not implemented for Event Hub")
health := client.Health()
require.Equal(t, datasource.Health{}, health, "Health should return an empty datasource.Health struct")
}
func TestCreateTopic_ForMigrations(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
client := New(getTestConfigs())
client.UseLogger(mockLogger)
err := client.CreateTopic(t.Context(), "gofr_migrations")
require.NoError(t, err, "CreateTopic should not return an error for 'gofr_migrations'")
}
func Test_GetEventHubName(t *testing.T) {
expectedName := "test-event-hub"
client := New(Config{
EventhubName: expectedName,
})
actualName := client.GetEventHubName()
require.Equal(t, expectedName, actualName, "GetEventHubName should return the configured EventhubName")
}
func TestConnect_ConsumerGroupDefaults(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cfg := getTestConfigs()
cfg.ConsumerGroup = ""
client := New(cfg)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debugf("Using default consumer group: %s", azeventhubs.DefaultConsumerGroup)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(NewMockMetrics(ctrl))
client.Connect()
require.Equal(t, azeventhubs.DefaultConsumerGroup, client.cfg.ConsumerGroup,
"Client should automatically switch to $Default consumer group when config is empty")
}
func TestConnect_ConsumerGroupProvided(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
cfg := getTestConfigs()
expectedGroup := "my-custom-group"
cfg.ConsumerGroup = expectedGroup
client := New(cfg)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debugf("Using provided consumer group: %s", expectedGroup)
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
client.UseLogger(mockLogger)
client.UseMetrics(NewMockMetrics(ctrl))
client.Connect()
require.Equal(t, expectedGroup, client.cfg.ConsumerGroup, "Client should respect the provided consumer group")
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/pubsub/eventhub
go 1.25.0
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1
github.com/coder/websocket v1.8.13
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/go-amqp v1.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/go.sum
================================================
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0 h1:BwmN55GUUfwFPSd44bxBVkFD8yJAp+LLjGRjSnpbeUM=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs v1.4.0/go.mod h1:OowfWwCcXlcn1Nkk6oTxeCuGNRElKtYpzkF1/gZ42Ig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0 h1:4hGvxD72TluuFIXVr8f4XkKZfqAa7Pj61t0jmQ7+kes=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/eventhub/armeventhub v1.3.0/go.mod h1:TSH7DcFItwAufy0Lz+Ft2cyopExCpxbOxI5SkH4dRNo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8=
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/helper.go
================================================
package eventhub
import (
"context"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
)
// parseQueryArgs parses the query arguments.
func (*Client) parseQueryArgs(args ...any) (startPosition azeventhubs.StartPosition, limit int) {
startPosition = defaultStartPosition()
limit = 10
if len(args) > 0 {
startPosition = parseStartPositionArg(args[0])
}
if len(args) > 1 {
limit = parseLimitArg(args[1], limit)
}
return startPosition, limit
}
func defaultStartPosition() azeventhubs.StartPosition {
earliest := true
return azeventhubs.StartPosition{Earliest: &earliest}
}
func parseStartPositionArg(arg any) azeventhubs.StartPosition {
switch v := arg.(type) {
case int64:
if v > 0 {
return azeventhubs.StartPosition{
SequenceNumber: &v,
Inclusive: true,
}
}
case string:
if v == "latest" {
latest := true
return azeventhubs.StartPosition{
Latest: &latest,
}
}
case time.Time:
return azeventhubs.StartPosition{
EnqueuedTime: &v,
}
}
return defaultStartPosition()
}
func parseLimitArg(arg any, limit int) int {
if val, ok := arg.(int); ok && val > 0 {
return val
}
return limit
}
// readMessages reads messages from Event Hub partitions.
func (c *Client) readMessages(ctx context.Context, startPosition azeventhubs.StartPosition, limit int) ([]byte, error) {
partitions, err := c.consumer.GetEventHubProperties(ctx, nil)
if err != nil {
return nil, err
}
var result []byte
messagesRead := 0
// Read from partitions sequentially until we get enough messages
for _, partitionID := range partitions.PartitionIDs {
if messagesRead >= limit {
break
}
remaining := limit - messagesRead
messages := c.readFromPartition(ctx, partitionID, startPosition, remaining)
for _, msg := range messages {
if len(result) > 0 {
result = append(result, '\n')
}
result = append(result, msg...)
messagesRead++
if messagesRead >= limit {
break
}
}
}
return result, nil
}
func (c *Client) readFromPartition(ctx context.Context, partitionID string,
startPosition azeventhubs.StartPosition, maxMessages int) [][]byte {
pc, err := c.createPartitionClient(partitionID, startPosition)
if err != nil {
return nil
}
defer pc.Close(ctx)
return receiveMessages(ctx, pc, maxMessages)
}
func (c *Client) createPartitionClient(partitionID string,
startPosition azeventhubs.StartPosition) (*azeventhubs.PartitionClient, error) {
return c.consumer.NewPartitionClient(partitionID, &azeventhubs.PartitionClientOptions{
StartPosition: startPosition,
})
}
func receiveMessages(ctx context.Context, pc *azeventhubs.PartitionClient,
maxMessages int) [][]byte {
var messages [][]byte
for len(messages) < maxMessages {
if ctx.Err() != nil {
break
}
events, err := pc.ReceiveEvents(ctx, maxMessages-len(messages), nil)
if err != nil || len(events) == 0 {
break
}
messages = appendMessages(messages, events, maxMessages)
}
return messages
}
func appendMessages(messages [][]byte, events []*azeventhubs.ReceivedEventData, maxMessages int) [][]byte {
for _, e := range events {
messages = append(messages, e.Body)
if len(messages) >= maxMessages {
break
}
}
return messages
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/helper_test.go
================================================
package eventhub
import (
"testing"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
"github.com/stretchr/testify/require"
)
func TestParseQueryArgs(t *testing.T) {
client := New(Config{})
now := time.Now()
tests := []struct {
name string
args []any
expectedStart azeventhubs.StartPosition
expectedLimit int
}{
{
name: "Default values",
args: nil,
expectedStart: azeventhubs.StartPosition{Earliest: boolPtr(true)},
expectedLimit: 10,
},
{
name: "With sequence number",
args: []any{int64(5)},
expectedStart: azeventhubs.StartPosition{SequenceNumber: int64Ptr(5), Inclusive: true},
expectedLimit: 10,
},
{
name: "With latest",
args: []any{"latest"},
expectedStart: azeventhubs.StartPosition{Latest: boolPtr(true)},
expectedLimit: 10,
},
{
name: "With enqueued time",
args: []any{now},
expectedStart: azeventhubs.StartPosition{EnqueuedTime: &now},
expectedLimit: 10,
},
{
name: "With limit",
args: []any{int64(5), 20},
expectedStart: azeventhubs.StartPosition{SequenceNumber: int64Ptr(5), Inclusive: true},
expectedLimit: 20,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
startPosition, limit := client.parseQueryArgs(tt.args...)
require.Equal(t, tt.expectedStart, startPosition, "Start position mismatch")
require.Equal(t, tt.expectedLimit, limit, "Limit mismatch")
})
}
}
func boolPtr(b bool) *bool {
return &b
}
func int64Ptr(i int64) *int64 {
return &i
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/logger.go
================================================
package eventhub
import (
"fmt"
"io"
)
// Logger interface with required methods.
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
Error(args ...any)
Fatal(args ...any)
Errorf(pattern string, args ...any)
}
type Log struct {
Mode string `json:"mode"`
MessageValue string `json:"messageValue"`
Topic string `json:"topic"`
Host string `json:"host"`
PubSubBackend string `json:"pubSubBackend"`
Time int64 `json:"time"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-4s%s \u001b[38;5;101m\n",
l.Topic, l.PubSubBackend, l.Time, l.Mode, l.MessageValue)
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/logger_test.go
================================================
package eventhub
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := Log{
Mode: "PUB",
MessageValue: `{"myorder":"1"}`,
Topic: "test-topic",
Host: "localhost",
PubSubBackend: "AZHUB",
Time: 10,
}
logger := NewMockLogger(gomock.NewController(t))
logger.EXPECT().Log(gomock.Any())
logger.Log(queryLog)
b := make([]byte, 100)
writer := bytes.NewBuffer(b)
queryLog.PrettyPrint(writer)
require.Contains(t, writer.String(), "test-topic")
require.Contains(t, writer.String(), "AZHUB")
require.Contains(t, writer.String(), `{"myorder":"1"}`)
require.True(t, logger.ctrl.Satisfied(), "Test_PrettyPrint Failed!")
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/message.go
================================================
package eventhub
import (
"context"
"github.com/Azure/azure-sdk-for-go/sdk/messaging/azeventhubs"
)
type Message struct {
event *azeventhubs.ReceivedEventData
processor *azeventhubs.ProcessorPartitionClient
logger Logger
}
func (a *Message) Commit() {
// Update the checkpoint with the latest event received
if a.processor != nil {
err := a.processor.UpdateCheckpoint(context.Background(), a.event, nil)
if err != nil {
a.logger.Errorf("failed to acknowledge event with eventID %v: %v", a.event.MessageID, err)
return
}
a.logger.Debugf("Message committed via processor checkpoint (MessageID: %v)", a.event.MessageID)
} else {
a.logger.Debugf("Message acknowledged (direct read mode)")
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/metrics.go
================================================
package eventhub
import (
"context"
)
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=eventhub
//
// Package eventhub is a generated GoMock package.
package eventhub
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Fatal mocks base method.
func (m *MockLogger) Fatal(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Fatal", varargs...)
}
// Fatal indicates an expected call of Fatal.
func (mr *MockLoggerMockRecorder) Fatal(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fatal", reflect.TypeOf((*MockLogger)(nil).Fatal), args...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/eventhub/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=eventhub
//
// Package eventhub is a generated GoMock package.
package eventhub
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/google.go
================================================
// Package google provides a client for interacting with Google Cloud Pub/Sub.This package facilitates interaction with
// Google Cloud Pub/Sub, allowing publishing and subscribing to topics, managing subscriptions, and handling messages.
package google
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
gcPubSub "cloud.google.com/go/pubsub"
"google.golang.org/api/iterator"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
var (
errProjectIDNotProvided = errors.New("google project id not provided")
errSubscriptionNotProvided = errors.New("subscription name not provided")
errClientNotConnected = errors.New("google pubsub client is not connected")
errTopicName = errors.New("empty topic name")
)
const (
defaultRetryInterval = 10 * time.Second
messageBufferSize = 100
)
type Config struct {
ProjectID string
SubscriptionName string
}
type googleClient struct {
Config
client Client
logger pubsub.Logger
metrics Metrics
receiveChan map[string]chan *pubsub.Message
subStarted map[string]struct{}
mu sync.RWMutex
}
const (
defaultQueryTimeout = 30 * time.Second
defaultMessageLimit = 10
)
//nolint:revive // We do not want anyone using the client without initialization steps.
func New(conf Config, logger pubsub.Logger, metrics Metrics) *googleClient {
err := validateConfigs(&conf)
if err != nil {
logger.Errorf("could not configure google pubsub, error: %v", err)
return nil
}
logger.Debugf("connecting to google pubsub client with projectID '%s' and subscriptionName '%s", conf.ProjectID, conf.SubscriptionName)
var client googleClient
client.Config = conf
client.logger = logger
client.metrics = metrics
client.receiveChan = make(map[string]chan *pubsub.Message)
client.subStarted = make(map[string]struct{})
client.mu = sync.RWMutex{}
gClient, err := connect(conf, logger)
if err != nil {
go retryConnect(conf, logger, &client)
return &client
}
client.client = gClient
return &client
}
func connect(conf Config, logger pubsub.Logger) (*gcPubSub.Client, error) {
client, err := gcPubSub.NewClient(context.Background(), conf.ProjectID)
if err != nil {
logger.Errorf("could not create Google PubSub client, error: %v", err)
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryInterval)
defer cancel()
it := client.Topics(ctx)
_, err = it.Next()
if err != nil {
if errors.Is(err, iterator.Done) {
logger.Debugf("no topics found in Google PubSub")
return client, nil
}
logger.Errorf("google pubsub connection validation failed, error: %v", err)
return nil, err
}
logger.Logf("connected to google pubsub client, projectID: %s", client.Project())
return client, nil
}
func (g *googleClient) Publish(ctx context.Context, topic string, message []byte) error {
ctx, span, traceAttrs := startPublishSpan(ctx, topic)
defer span.End()
g.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
t, err := g.getTopic(ctx, topic)
if err != nil {
g.logger.Errorf("could not create topic '%s', error: %v", topic, err)
return err
}
start := time.Now()
result := t.Publish(ctx, &gcPubSub.Message{
Data: message,
Attributes: traceAttrs,
PublishTime: time.Now(),
})
end := time.Since(start)
_, err = result.Get(ctx)
if err != nil {
g.logger.Errorf("error publishing to google topic '%s', error: %v", topic, err)
return err
}
g.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: g.ProjectID,
PubSubBackend: "GCP",
Time: end.Microseconds(),
})
g.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
func (g *googleClient) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
var end time.Duration
if g.client == nil {
return nil, nil
}
if !g.isConnected() {
time.Sleep(defaultRetryInterval)
return nil, errClientNotConnected
}
g.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "subscription_name", g.Config.SubscriptionName)
if _, ok := g.subStarted[topic]; !ok {
t, err := g.getTopic(ctx, topic)
if err != nil {
return nil, err
}
subscription, err := g.getSubscription(ctx, t)
if err != nil {
return nil, err
}
start := time.Now()
processMessage := func(ctx context.Context, msg *gcPubSub.Message) {
m := pubsub.NewMessage(ctx)
end = time.Since(start)
m.Topic = topic
m.Value = msg.Data
m.MetaData = msg.Attributes
m.Committer = newGoogleMessage(msg)
g.mu.Lock()
defer g.mu.Unlock()
g.receiveChan[topic] <- m
}
// initialize the channel before we can start receiving on it
g.mu.Lock()
g.receiveChan[topic] = make(chan *pubsub.Message)
g.mu.Unlock()
go func() {
err = subscription.Receive(ctx, processMessage)
if err != nil {
g.logger.Errorf("error getting a message from google: %s", err.Error())
}
}()
g.subStarted[topic] = struct{}{}
}
select {
case m := <-g.receiveChan[topic]:
// Create span with links to producer span from message attributes
spanCtx, span := startSubscribeSpan(ctx, topic, extractMessageAttrs(m.MetaData))
defer span.End()
g.metrics.IncrementCounter(spanCtx, "app_pubsub_subscribe_success_count", "topic", topic, "subscription_name",
g.Config.SubscriptionName)
g.logger.Debug(&pubsub.Log{
Mode: "SUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(m.Value),
Topic: topic,
Host: g.Config.ProjectID,
PubSubBackend: "GCP",
Time: end.Microseconds(),
})
return m, nil
case <-ctx.Done():
return nil, nil
}
}
func (g *googleClient) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if !g.isConnected() {
return nil, errClientNotConnected
}
if query == "" {
return nil, errTopicName
}
timeout, limit := parseQueryArgs(args...)
// Get topic and subscription
topic, err := g.getTopic(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to get topic: %w", err)
}
subscription, err := g.getQuerySubscription(ctx, topic)
if err != nil {
return nil, fmt.Errorf("failed to get subscription: %w", err)
}
msgChan := make(chan []byte, messageBufferSize)
queryCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Start receiving messages
go func() {
defer close(msgChan)
receiveCtx, receiveCancel := context.WithTimeout(queryCtx, timeout)
defer receiveCancel()
err := subscription.Receive(receiveCtx, func(_ context.Context, msg *gcPubSub.Message) {
defer msg.Ack()
select {
case msgChan <- msg.Data:
case <-receiveCtx.Done():
return
default:
// Channel might be full, try non-blocking send
g.logger.Debugf("Query: message channel full for topic %s", query)
}
})
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
g.logger.Debugf("Query: receive ended for topic %s: %v", query, err)
}
}()
// Collect messages
return g.collectMessages(queryCtx, msgChan, limit), nil
}
func (g *googleClient) getTopic(ctx context.Context, topic string) (*gcPubSub.Topic, error) {
if g.client == nil {
return nil, errClientNotConnected
}
t := g.client.Topic(topic)
// check if topic exists, if not create the topic
if ok, err := t.Exists(ctx); !ok || err != nil {
_, errTopicCreate := g.client.CreateTopic(ctx, topic)
if errTopicCreate != nil {
return nil, errTopicCreate
}
}
return t, nil
}
func (g *googleClient) getSubscription(ctx context.Context, topic *gcPubSub.Topic) (*gcPubSub.Subscription, error) {
if g.client == nil {
return nil, errClientNotConnected
}
subscription := g.client.Subscription(g.SubscriptionName + "-" + topic.ID())
// check if subscription already exists or not
ok, err := subscription.Exists(context.Background())
if err != nil {
g.logger.Errorf("unable to check the existence of subscription, error: %v", err.Error())
return nil, err
}
// if subscription is not present, create a new
if !ok {
subscription, err = g.client.CreateSubscription(ctx, g.SubscriptionName+"-"+topic.ID(), gcPubSub.SubscriptionConfig{
Topic: topic,
})
if err != nil {
return nil, err
}
}
return subscription, nil
}
func (g *googleClient) DeleteTopic(ctx context.Context, name string) error {
if g.client == nil {
return errClientNotConnected
}
err := g.client.Topic(name).Delete(ctx)
if err != nil && strings.Contains(err.Error(), "Topic not found") {
return nil
}
return err
}
func (g *googleClient) CreateTopic(ctx context.Context, name string) error {
if g.client == nil {
return errClientNotConnected
}
_, err := g.client.CreateTopic(ctx, name)
if err != nil && strings.Contains(err.Error(), "Topic already exists") {
return nil
}
return err
}
func (g *googleClient) Close() error {
if g.client != nil {
return g.client.Close()
}
for _, c := range g.receiveChan {
close(c)
}
return nil
}
func retryConnect(conf Config, logger pubsub.Logger, g *googleClient) {
for {
client, err := connect(conf, logger)
if err == nil {
g.mu.Lock()
g.client = client
g.mu.Unlock()
logger.Logf("connected to google pubsub client, projectID: %s", conf.ProjectID)
return
}
logger.Errorf("could not connect to Google PubSub, error: %v", err)
time.Sleep(defaultRetryInterval)
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/google_test.go
================================================
package google
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
gcPubSub "cloud.google.com/go/pubsub"
"cloud.google.com/go/pubsub/pstest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/api/iterator"
"google.golang.org/api/option"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
var (
errTopicExists = errors.New("topic already exists")
errTopicExistsWrapped = fmt.Errorf("Topic already exists: %w", errTopicExists)
errTestSentinel = errors.New("test-error")
)
func getGoogleClient(t *testing.T) *gcPubSub.Client {
t.Helper()
srv := pstest.NewServer()
conn, err := grpc.NewClient(srv.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Errorf("could not initialize a connection to dummy server")
}
client, err := gcPubSub.NewClient(t.Context(), "project", option.WithGRPCConn(conn))
if err != nil {
t.Errorf("could not initialize a test client")
}
return client
}
func TestGoogleClient_New_InvalidConfig(t *testing.T) {
var g *googleClient
ctrl := gomock.NewController(t)
defer ctrl.Finish()
out := testutil.StderrOutputForFunc(func() {
logger := logging.NewMockLogger(logging.ERROR)
g = New(Config{}, logger, NewMockMetrics(ctrl))
})
assert.Nil(t, g)
assert.Contains(t, out, "could not configure google pubsub")
}
func TestGoogleClient_New_EmptyClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
config := Config{ProjectID: "test", SubscriptionName: "test"}
client := New(config, logger, mockMetrics)
require.Nil(t, client.client, "TestGoogleClient_New_EmptyClient Failed!")
require.Equal(t, config, client.Config, "TestGoogleClient_New_EmptyClient Failed!")
}
func TestGoogleClient_Publish_Success(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
topic := "test-topic"
message := []byte("test message")
out := testutil.StdoutOutputForFunc(func() {
g := &googleClient{
logger: logging.NewMockLogger(logging.DEBUG),
client: client,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
metrics: mockMetrics,
}
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "topic", topic)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_success_count", "topic", topic)
err := g.Publish(t.Context(), topic, message)
require.NoError(t, err)
})
assert.Contains(t, out, "PUB")
assert.Contains(t, out, "test message")
assert.Contains(t, out, "test-topic")
assert.Contains(t, out, "test")
assert.Contains(t, out, "GCP")
}
func TestGoogleClient_PublishTopic_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
g := &googleClient{client: getGoogleClient(t), Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
}, metrics: mockMetrics, logger: logging.NewMockLogger(logging.DEBUG)}
defer g.client.Close()
ctx, cancel := context.WithCancel(t.Context())
cancel()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "topic", "test-topic")
err := g.Publish(ctx, "test-topic", []byte(""))
require.ErrorContains(t, err, "context canceled")
}
func TestGoogleClient_getTopic_Success(t *testing.T) {
g := &googleClient{client: getGoogleClient(t), Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
}}
defer g.client.Close()
topic, err := g.getTopic(t.Context(), "test-topic")
require.NoError(t, err)
assert.Equal(t, "test-topic", topic.ID())
}
func TestGoogleClient_getTopic_Error(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
cancel()
g := &googleClient{client: getGoogleClient(t), Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
}}
defer g.client.Close()
topic, err := g.getTopic(ctx, "test-topic")
assert.Nil(t, topic)
require.ErrorContains(t, err, "context canceled")
}
func TestGoogleClient_getSubscription(t *testing.T) {
g := &googleClient{client: getGoogleClient(t), Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
}}
defer g.client.Close()
topic, _ := g.client.CreateTopic(t.Context(), "test-topic")
sub, err := g.getSubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub)
}
func Test_validateConfigs(t *testing.T) {
testCases := []struct {
desc string
input *Config
expErr error
}{
{desc: "project id not provided", input: &Config{}, expErr: errProjectIDNotProvided},
{desc: "subscription not provided", input: &Config{ProjectID: "test"}, expErr: errSubscriptionNotProvided},
{desc: "success", input: &Config{ProjectID: "test", SubscriptionName: "subs"}, expErr: nil},
}
for _, tc := range testCases {
err := validateConfigs(tc.input)
require.ErrorIs(t, err, tc.expErr)
}
}
func TestGoogleClient_CloseReturnsError(t *testing.T) {
g := &googleClient{
client: getGoogleClient(t),
receiveChan: make(map[string]chan *pubsub.Message),
}
err := g.Close()
require.NoError(t, err)
// client empty
g = &googleClient{receiveChan: make(map[string]chan *pubsub.Message)}
err = g.Close()
require.NoError(t, err)
}
func TestGoogleClient_CreateTopic_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := NewMockClient(ctrl)
g := &googleClient{client: mockClient, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
tests := []struct {
name string
topicName string
mockBehavior func()
expectedErr error
}{
{
name: "CreateTopic_Success",
topicName: "test-topic",
mockBehavior: func() {
mockClient.EXPECT().CreateTopic(t.Context(), "test-topic").Return(&gcPubSub.Topic{}, nil)
},
expectedErr: nil,
},
{
name: "CreateTopic_AlreadyExists",
topicName: "test-topic",
mockBehavior: func() {
mockClient.EXPECT().CreateTopic(t.Context(), "test-topic").Return(&gcPubSub.Topic{}, errTopicExists)
},
expectedErr: errTopicExists,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockBehavior()
err := g.CreateTopic(t.Context(), tt.topicName)
require.ErrorIs(t, err, tt.expectedErr, "expected no error, but got one")
})
}
}
func TestGoogleClient_CreateTopic_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := NewMockClient(ctrl)
g := &googleClient{client: mockClient, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
mockClient.EXPECT().CreateTopic(t.Context(), "test-topic").
Return(&gcPubSub.Topic{}, errTestSentinel)
err := g.CreateTopic(t.Context(), "test-topic")
require.ErrorIs(t, err, errTestSentinel, "expected test-error but got different error")
}
func TestGoogleClient_CreateTopic_EmptyClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
g := &googleClient{client: nil, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
err := g.CreateTopic(t.Context(), "test-topic")
require.ErrorIs(t, err, errClientNotConnected, "expected client-error but got different error")
}
func TestGoogleClient_DeleteTopic(t *testing.T) {
ctx := t.Context()
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{client: client, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
// Test successful topic creation
t.Run("DeleteTopic_Success", func(t *testing.T) {
err := g.CreateTopic(ctx, "test-topic")
require.NoError(t, err)
err = g.DeleteTopic(ctx, "test-topic")
require.NoError(t, err, "expected topic deletion to succeed, but got error")
})
// Test topic deletion with topic not found
t.Run("DeleteTopic_NotFound", func(t *testing.T) {
err := g.DeleteTopic(ctx, "test-topic")
require.ErrorContains(t, err, "NotFound", "expected NotFound error for non existing topic deletion")
})
}
func TestGoogleClient_DeleteTopic_EmptyClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
g := &googleClient{client: nil, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
err := g.DeleteTopic(t.Context(), "test-topic")
require.ErrorIs(t, err, errClientNotConnected, "expected client-error but got different error")
}
func TestGoogleClient_Query(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
topic := "test-topic-query"
message := []byte("test message")
g := &googleClient{
client: client,
logger: logger,
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
topicObj, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
subName := "sub-query-" + topic
_, err = client.CreateSubscription(t.Context(), subName, gcPubSub.SubscriptionConfig{
Topic: topicObj,
})
require.NoError(t, err)
result := topicObj.Publish(t.Context(), &gcPubSub.Message{Data: message})
_, err = result.Get(t.Context())
require.NoError(t, err)
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
queryResult, err := g.Query(ctx, topic)
require.NoError(t, err)
assert.Equal(t, message, queryResult)
err = topicObj.Delete(t.Context())
require.NoError(t, err)
}
func TestIsConnected_WhenClientNotNil(t *testing.T) {
g := &googleClient{client: getGoogleClient(t)}
require.True(t, g.isConnected())
}
func TestClose_ClientNil(t *testing.T) {
g := &googleClient{
receiveChan: map[string]chan *pubsub.Message{
"test-topic": make(chan *pubsub.Message),
},
}
err := g.Close()
require.NoError(t, err)
}
func TestClose_MultipleReceiveChans_ClientNil(t *testing.T) {
g := &googleClient{
receiveChan: map[string]chan *pubsub.Message{
"topic1": make(chan *pubsub.Message),
"topic2": make(chan *pubsub.Message),
},
// client is nil
}
err := g.Close()
require.NoError(t, err)
}
func TestSubscribe_ClientNil(t *testing.T) {
g := &googleClient{}
msg, err := g.Subscribe(t.Context(), "test-topic")
require.Nil(t, msg)
require.NoError(t, err)
}
func TestGetTopic_ClientNil(t *testing.T) {
g := &googleClient{}
_, err := g.getTopic(t.Context(), "any-topic")
require.Equal(t, errClientNotConnected, err)
}
func TestIsConnected_WhenClientNil(t *testing.T) {
g := &googleClient{}
require.False(t, g.isConnected())
}
func TestGoogleClient_getTopic_CreateFailure(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
// Delete the server to simulate failure in CreateTopic
client.Close()
g := &googleClient{client: client, Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
}}
_, err := g.getTopic(t.Context(), "test-topic")
require.Error(t, err)
}
func TestGoogleClient_collectMessages_LimitReached(t *testing.T) {
logger := logging.NewMockLogger(logging.DEBUG)
g := &googleClient{
logger: logger,
}
msgChan := make(chan []byte, 3)
msgChan <- []byte("message1")
msgChan <- []byte("message2")
close(msgChan)
ctx := t.Context()
result := g.collectMessages(ctx, msgChan, 2)
expected := []byte("message1\nmessage2")
assert.Equal(t, expected, result)
}
func TestGoogleClient_getQuerySubscription_CreateFails(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
}
topic, err := client.CreateTopic(t.Context(), "test-topic-bad")
require.NoError(t, err)
// simulate failure by closing client
client.Close()
sub, err := g.getQuerySubscription(t.Context(), topic)
require.Error(t, err)
assert.Nil(t, sub)
}
func TestGoogleClient_Health_Success(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test-project", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
// Create some test topics and subscriptions
_, err := client.CreateTopic(t.Context(), "test-topic-health")
require.NoError(t, err)
health := g.Health()
assert.Equal(t, "UP", health.Status)
assert.Equal(t, "test-project", health.Details["projectID"])
assert.Equal(t, "GOOGLE", health.Details["backend"])
assert.NotNil(t, health.Details["writers"])
assert.NotNil(t, health.Details["readers"])
}
func TestGoogleClient_Health_WithError(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
Config: Config{ProjectID: "test-project", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
// Close client to cause errors in health check
client.Close()
// This will cause getWriterDetails and getReaderDetails to fail
health := g.Health()
// Health should still return, but status might be DOWN
assert.Equal(t, "DOWN", health.Status)
assert.Equal(t, "test-project", health.Details["projectID"])
assert.Equal(t, "GOOGLE", health.Details["backend"])
}
func TestGoogleClient_getWriterDetails(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test-project", SubscriptionName: "sub"},
}
// Create some test topics
_, err := client.CreateTopic(t.Context(), "writer-test-topic1")
require.NoError(t, err)
_, err = client.CreateTopic(t.Context(), "writer-test-topic2")
require.NoError(t, err)
status, details := g.getWriterDetails()
assert.Equal(t, "UP", status)
assert.NotNil(t, details)
assert.GreaterOrEqual(t, len(details), 2)
}
func TestGoogleClient_getReaderDetails(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test-project", SubscriptionName: "sub"},
}
// Create a topic and subscription
topic, err := client.CreateTopic(t.Context(), "reader-test-topic")
require.NoError(t, err)
_, err = client.CreateSubscription(t.Context(), "test-subscription", gcPubSub.SubscriptionConfig{
Topic: topic,
})
require.NoError(t, err)
status, details := g.getReaderDetails()
assert.Equal(t, "UP", status)
assert.NotNil(t, details)
assert.GreaterOrEqual(t, len(details), 1)
}
func TestGoogleClient_Subscribe_Success(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
topic := "test-subscribe-topic"
message := []byte("subscribe test message")
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
}
// Expect metrics calls
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
// Create topic and publish a message
topicObj, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
result := topicObj.Publish(t.Context(), &gcPubSub.Message{Data: message})
_, err = result.Get(t.Context())
require.NoError(t, err)
// Subscribe to the topic
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
// Start subscription in goroutine
var msg *pubsub.Message
go func() {
msg, err = g.Subscribe(ctx, topic)
}()
// Give it time to set up subscription and receive
time.Sleep(500 * time.Millisecond)
// Publish another message
result = topicObj.Publish(t.Context(), &gcPubSub.Message{Data: message})
_, err = result.Get(t.Context())
require.NoError(t, err)
// Wait for message
time.Sleep(1 * time.Second)
require.NoError(t, err)
require.NotNil(t, msg)
assert.Equal(t, message, msg.Value)
assert.Equal(t, topic, msg.Topic)
}
func TestGoogleClient_Subscribe_ContextCanceled(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
topic := "test-subscribe-timeout"
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
}
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
// Create topic to avoid errors
_, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Create a context with very short timeout
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
defer cancel()
// Wait for timeout
time.Sleep(50 * time.Millisecond)
msg, _ := g.Subscribe(ctx, topic)
// Should return nil when context is done
assert.Nil(t, msg)
// Error may or may not be nil depending on when context was canceled
}
func TestGoogleClient_Subscribe_NotConnected(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
// Close client to simulate not connected
client.Close()
msg, err := g.Subscribe(t.Context(), "test-topic")
assert.Nil(t, msg)
assert.ErrorIs(t, err, errClientNotConnected)
}
func TestGoogleClient_Query_EmptyTopic(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
_, err := g.Query(t.Context(), "")
assert.ErrorIs(t, err, errTopicName)
}
func TestGoogleClient_Query_NotConnected(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
client.Close()
_, err := g.Query(t.Context(), "test-topic")
assert.ErrorIs(t, err, errClientNotConnected)
}
func TestGoogleClient_Query_WithLimit(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
topic := "test-topic-query-limit"
topicObj, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Publish multiple messages
for i := 0; i < 5; i++ {
result := topicObj.Publish(t.Context(), &gcPubSub.Message{Data: []byte("message")})
_, err = result.Get(t.Context())
require.NoError(t, err)
}
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
// Query with limit
_, err = g.Query(ctx, topic, 30*time.Second, 3)
require.NoError(t, err)
}
func TestGoogleClient_getSubscription_ExistsError(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
topic, err := client.CreateTopic(t.Context(), "test-topic-sub-err")
require.NoError(t, err)
// Close client to cause error
client.Close()
sub, err := g.getSubscription(t.Context(), topic)
assert.Nil(t, sub)
assert.Error(t, err)
}
func TestParseQueryArgs_WithLimit(t *testing.T) {
timeout, limit := parseQueryArgs(30*time.Second, 5)
assert.Equal(t, defaultQueryTimeout, timeout)
assert.Equal(t, 5, limit)
}
func TestParseQueryArgs_NoArgs(t *testing.T) {
timeout, limit := parseQueryArgs()
assert.Equal(t, defaultQueryTimeout, timeout)
assert.Equal(t, defaultMessageLimit, limit)
}
func TestParseQueryArgs_OnlyTimeout(t *testing.T) {
timeout, limit := parseQueryArgs(45 * time.Second)
assert.Equal(t, defaultQueryTimeout, timeout)
assert.Equal(t, defaultMessageLimit, limit)
}
func TestGoogleClient_collectMessages_ContextDone(t *testing.T) {
logger := logging.NewMockLogger(logging.DEBUG)
g := &googleClient{
logger: logger,
}
msgChan := make(chan []byte)
ctx, cancel := context.WithCancel(t.Context())
// Cancel immediately
cancel()
result := g.collectMessages(ctx, msgChan, 10)
assert.Empty(t, result)
}
func TestGoogleClient_collectMessages_UnlimitedMessages(t *testing.T) {
logger := logging.NewMockLogger(logging.DEBUG)
g := &googleClient{
logger: logger,
}
msgChan := make(chan []byte, 3)
msgChan <- []byte("message1")
msgChan <- []byte("message2")
msgChan <- []byte("message3")
close(msgChan)
ctx := t.Context()
// Test with limit <= 0 (unlimited)
result := g.collectMessages(ctx, msgChan, 0)
expected := []byte("message1\nmessage2\nmessage3")
assert.Equal(t, expected, result)
}
func TestGoogleClient_getQuerySubscription_AlreadyExists(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
}
topic, err := client.CreateTopic(t.Context(), "test-topic-exists")
require.NoError(t, err)
subName := g.SubscriptionName + "-query-" + topic.ID()
// Create subscription first
_, err = client.CreateSubscription(t.Context(), subName, gcPubSub.SubscriptionConfig{
Topic: topic,
})
require.NoError(t, err)
// Now get it - should return existing one
sub, err := g.getQuerySubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, subName, sub.ID())
}
func TestGoogleClient_Subscribe_AlreadyStarted(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
topic := "test-subscribe-already-started"
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
}
// Create topic
topicObj, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Mark subscription as already started
g.subStarted[topic] = struct{}{}
g.receiveChan[topic] = make(chan *pubsub.Message, 1)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
// Publish a message
message := []byte("test message for already started")
result := topicObj.Publish(t.Context(), &gcPubSub.Message{Data: message})
_, err = result.Get(t.Context())
require.NoError(t, err)
// Manually put message in the channel
m := pubsub.NewMessage(t.Context())
m.Value = message
m.Topic = topic
g.receiveChan[topic] <- m
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
msg, err := g.Subscribe(ctx, topic)
require.NoError(t, err)
require.NotNil(t, msg)
assert.Equal(t, message, msg.Value)
}
func TestGoogleClient_Query_ContextTimeout(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
topic := "test-topic-query-timeout"
_, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Very short timeout - messages won't arrive in time
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond)
defer cancel()
time.Sleep(50 * time.Millisecond) // Wait for context to timeout
result, err := g.Query(ctx, topic)
// Should return empty result or error due to timeout
assert.True(t, err != nil || result != nil, "expected either an error or a non-nil result due to timeout")
assert.NotEqual(t, err == nil, result == nil, "expected error and result not to share the same nil state")
}
func TestGoogleClient_Query_GetTopicError(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
// Close client to cause getTopic to fail
client.Close()
_, err := g.Query(t.Context(), "test-topic")
assert.Error(t, err)
}
func TestGoogleClient_getSubscription_AlreadyExists(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
topic, err := client.CreateTopic(t.Context(), "test-topic-sub-exists")
require.NoError(t, err)
// Create subscription first
subName := g.SubscriptionName + "-" + topic.ID()
_, err = client.CreateSubscription(t.Context(), subName, gcPubSub.SubscriptionConfig{
Topic: topic,
})
require.NoError(t, err)
// Now call getSubscription - should return existing one
sub, err := g.getSubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub)
assert.Equal(t, subName, sub.ID())
}
func TestGoogleClient_DeleteTopic_AlreadyExists(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{client: client, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
// Create and then delete
err := g.CreateTopic(t.Context(), "test-delete-topic")
require.NoError(t, err)
err = g.DeleteTopic(t.Context(), "test-delete-topic")
require.NoError(t, err)
// Try deleting again - should handle "not found" gracefully
err = g.DeleteTopic(t.Context(), "test-delete-topic")
// Error should contain "NotFound" or be nil if implementation handles it
assert.True(t, err == nil || strings.Contains(err.Error(), "NotFound"), "expected no error or NotFound error when deleting missing topic")
}
func TestGoogleClient_CreateTopic_AlreadyExists(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockClient := NewMockClient(ctrl)
g := &googleClient{client: mockClient, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
// Mock CreateTopic to return "already exists" error
mockClient.EXPECT().CreateTopic(t.Context(), "existing-topic").
Return(&gcPubSub.Topic{}, errTopicExistsWrapped)
err := g.CreateTopic(t.Context(), "existing-topic")
// Should not return error if topic already exists
assert.NoError(t, err)
}
func TestGoogleClient_getQuerySubscription_ExistsCheck(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
topic, err := client.CreateTopic(t.Context(), "test-topic-query-exists")
require.NoError(t, err)
// First call - subscription doesn't exist, should create
sub1, err := g.getQuerySubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub1)
// Second call - subscription exists, should return existing
sub2, err := g.getQuerySubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub2)
assert.Equal(t, sub1.ID(), sub2.ID())
}
func TestGoogleClient_Subscribe_GetTopicError(t *testing.T) {
client := getGoogleClient(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
}
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Close client to cause getTopic to fail
client.Close()
topic := "test-subscribe-error"
msg, err := g.Subscribe(t.Context(), topic)
assert.Nil(t, msg)
assert.Error(t, err)
}
func TestGoogleClient_Subscribe_GetSubscriptionError(t *testing.T) {
client := getGoogleClient(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
topic := "test-subscribe-sub-error"
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
}
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
"topic", topic, "subscription_name", g.Config.SubscriptionName).AnyTimes()
// Create topic first
_, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Close client after topic creation to cause getSubscription to fail
client.Close()
msg, err := g.Subscribe(t.Context(), topic)
assert.Nil(t, msg)
assert.Error(t, err)
}
func TestConnect_Success(t *testing.T) {
// This test uses the real pstest server
client := getGoogleClient(t)
defer client.Close()
// Verify connection was successful by checking we can list topics
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
it := client.Topics(ctx)
_, err := it.Next()
// Should either return a topic or iterator.Done (no topics)
assert.True(t, err == nil || errors.Is(err, iterator.Done))
}
func TestConnect_NoTopics(t *testing.T) {
config := Config{ProjectID: "test-project", SubscriptionName: "test-sub"}
// Use a fresh pstest server with no topics
srv := pstest.NewServer()
defer srv.Close()
conn, err := grpc.NewClient(srv.Addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
require.NoError(t, err)
client, err := gcPubSub.NewClient(t.Context(), config.ProjectID, option.WithGRPCConn(conn))
require.NoError(t, err)
defer client.Close()
// Verify the client works with no topics
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
it := client.Topics(ctx)
_, err = it.Next()
// Should return iterator.Done when there are no topics
assert.True(t, errors.Is(err, iterator.Done) || err == nil)
}
func TestGetQuerySubscription_NewSubscription(t *testing.T) {
client := getGoogleClient(t)
defer client.Close()
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
topic, err := client.CreateTopic(t.Context(), "new-query-topic")
require.NoError(t, err)
// First time calling - should create new subscription
sub, err := g.getQuerySubscription(t.Context(), topic)
require.NoError(t, err)
assert.NotNil(t, sub)
assert.Contains(t, sub.ID(), "query")
}
func TestGetSubscription_CreateError(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
Config: Config{ProjectID: "test", SubscriptionName: "sub"},
logger: logging.NewMockLogger(logging.DEBUG),
}
topic, err := client.CreateTopic(t.Context(), "test-sub-create-err")
require.NoError(t, err)
// Close client to cause creation to fail
client.Close()
sub, err := g.getSubscription(t.Context(), topic)
assert.Nil(t, sub)
assert.Error(t, err)
}
func TestDeleteTopic_WithNotFoundString(t *testing.T) {
// Test with a real client
realClient := getGoogleClient(t)
defer realClient.Close()
gReal := &googleClient{client: realClient, Config: Config{ProjectID: "test", SubscriptionName: "sub"}}
// Try to delete a non-existent topic - should handle gracefully
err := gReal.DeleteTopic(t.Context(), "definitely-does-not-exist")
// Should either return nil (handled) or an error containing "not found"
lowerErr := strings.ToLower(fmt.Sprint(err))
assert.True(t, err == nil || strings.Contains(lowerErr, "not"),
"expected no error or a not-found error when deleting missing topic")
}
func TestPublish_GetTopicError(t *testing.T) {
client := getGoogleClient(t)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count",
"topic", "test-pub-error").AnyTimes()
// Close client to cause getTopic to fail
client.Close()
err := g.Publish(t.Context(), "test-pub-error", []byte("message"))
assert.Error(t, err)
}
func TestQuery_GetSubscriptionError(t *testing.T) {
client := getGoogleClient(t)
g := &googleClient{
client: client,
logger: logging.NewMockLogger(logging.DEBUG),
Config: Config{
ProjectID: "test",
SubscriptionName: "sub",
},
}
topic := "test-query-sub-err"
// Create topic
_, err := client.CreateTopic(t.Context(), topic)
require.NoError(t, err)
// Close client to cause getQuerySubscription to fail
client.Close()
_, err = g.Query(t.Context(), topic)
assert.Error(t, err)
}
func TestNew_WithRetryConnect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
// Use invalid config to trigger retry logic
config := Config{ProjectID: "invalid-project-for-retry", SubscriptionName: "test"}
client := New(config, logger, mockMetrics)
// Should return a client even if connection fails
assert.NotNil(t, client)
assert.Nil(t, client.client) // Client should be nil initially
// Clean up
_ = client.Close()
}
func TestCollectMessages_SingleMessage(t *testing.T) {
logger := logging.NewMockLogger(logging.DEBUG)
g := &googleClient{
logger: logger,
}
msgChan := make(chan []byte, 1)
msgChan <- []byte("single")
close(msgChan)
result := g.collectMessages(t.Context(), msgChan, 10)
assert.Equal(t, []byte("single"), result)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/health.go
================================================
package google
import (
"context"
"errors"
"time"
"google.golang.org/api/iterator"
"gofr.dev/pkg/gofr/datasource"
)
func (g *googleClient) Health() (health datasource.Health) {
health.Details = make(map[string]any)
var writerStatus, readerStatus string
health.Status = datasource.StatusUp
health.Details["projectID"] = g.Config.ProjectID
health.Details["backend"] = "GOOGLE"
writerStatus, health.Details["writers"] = g.getWriterDetails()
readerStatus, health.Details["readers"] = g.getReaderDetails()
if readerStatus == datasource.StatusDown || writerStatus == datasource.StatusDown {
health.Status = datasource.StatusDown
}
return health
}
//nolint:dupl // getWriterDetails provides the publishing details for current google publishers.
func (g *googleClient) getWriterDetails() (status string, details map[string]any) {
const contextTimeoutDuration = 50
status = datasource.StatusUp
ctx, cancel := context.WithTimeout(context.Background(), contextTimeoutDuration*time.Millisecond)
defer cancel()
it := g.client.Topics(ctx)
details = make(map[string]any)
for {
topic, err := it.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
status = datasource.StatusDown
break
}
if topic != nil {
details[topic.ID()] = topic
}
}
return status, details
}
//nolint:dupl // getReaderDetails provides the subscription details for current google subscriptions.
func (g *googleClient) getReaderDetails() (status string, details map[string]any) {
const contextTimeoutDuration = 50
status = datasource.StatusUp
ctx, cancel := context.WithTimeout(context.Background(), contextTimeoutDuration*time.Millisecond)
defer cancel()
subIt := g.client.Subscriptions(ctx)
details = make(map[string]any)
for {
subscription, err := subIt.Next()
if errors.Is(err, iterator.Done) {
break
}
if err != nil {
status = datasource.StatusDown
break
}
if subscription != nil {
details[subscription.ID()] = subscription
}
}
return status, details
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/helper.go
================================================
package google
import (
"bytes"
"context"
"errors"
"time"
gcPubSub "cloud.google.com/go/pubsub"
"google.golang.org/api/iterator"
)
func (g *googleClient) isConnected() bool {
if g.client == nil {
return false
}
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryInterval)
defer cancel()
it := g.client.Topics(ctx)
_, err := it.Next()
return err == nil || errors.Is(err, iterator.Done)
}
func validateConfigs(conf *Config) error {
if conf.ProjectID == "" {
return errProjectIDNotProvided
}
if conf.SubscriptionName == "" {
return errSubscriptionNotProvided
}
return nil
}
func parseQueryArgs(args ...any) (timeout time.Duration, limit int) {
timeout = defaultQueryTimeout
limit = defaultMessageLimit
if len(args) > 1 {
if val, ok := args[1].(int); ok {
limit = val
}
}
return timeout, limit
}
func (g *googleClient) getQuerySubscription(ctx context.Context, topic *gcPubSub.Topic) (*gcPubSub.Subscription, error) {
subName := g.SubscriptionName + "-query-" + topic.ID()
subscription := g.client.Subscription(subName)
exists, err := subscription.Exists(ctx)
if err != nil {
return nil, err
}
if !exists {
subscription, err = g.client.CreateSubscription(ctx, subName, gcPubSub.SubscriptionConfig{
Topic: topic,
})
if err != nil {
return nil, err
}
}
return subscription, nil
}
func (g *googleClient) collectMessages(ctx context.Context, msgChan <-chan []byte, limit int) []byte {
var result bytes.Buffer
collected := 0
for limit <= 0 || collected < limit {
select {
case msg, ok := <-msgChan:
if !ok {
g.logger.Debugf("Query: message channel closed, collected %d messages", collected)
return result.Bytes()
}
if result.Len() > 0 {
result.WriteByte('\n')
}
result.Write(msg)
collected++
g.logger.Debugf("Query: collected message %d", collected)
case <-ctx.Done():
return result.Bytes()
}
}
return result.Bytes()
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/interfaces.go
================================================
package google
import (
"context"
"cloud.google.com/go/pubsub"
)
type Client interface {
Writer
Reader
Close() error
Topics(ctx context.Context) *pubsub.TopicIterator
Subscriptions(ctx context.Context) *pubsub.SubscriptionIterator
}
type Writer interface {
Subscription(id string) *pubsub.Subscription
CreateSubscription(ctx context.Context, id string, cfg pubsub.SubscriptionConfig) (*pubsub.Subscription, error)
}
type Reader interface {
Topic(id string) *pubsub.Topic
CreateTopic(ctx context.Context, topicID string) (*pubsub.Topic, error)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/message.go
================================================
package google
import (
gcPubSub "cloud.google.com/go/pubsub"
)
type googleMessage struct {
msg *gcPubSub.Message
}
func newGoogleMessage(msg *gcPubSub.Message) *googleMessage {
return &googleMessage{msg: msg}
}
func (gm *googleMessage) Commit() {
gm.msg.Ack()
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/message_test.go
================================================
package google
import (
"testing"
gcPubSub "cloud.google.com/go/pubsub"
"github.com/stretchr/testify/assert"
)
func TestNew(t *testing.T) {
msg := new(gcPubSub.Message)
out := newGoogleMessage(msg)
assert.Equal(t, msg, out.msg)
}
func TestGoogleMessage_Commit(_ *testing.T) {
msg := newGoogleMessage(&gcPubSub.Message{})
msg.Commit()
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/metrics.go
================================================
package google
import "context"
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces.go
//
// Generated by this command:
//
// mockgen -source=interfaces.go -package=google -destination=mock_interfaces.go
//
// Package google is a generated GoMock package.
package google
import (
context "context"
reflect "reflect"
pubsub "cloud.google.com/go/pubsub"
gomock "go.uber.org/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockClient) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockClientMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockClient)(nil).Close))
}
// CreateSubscription mocks base method.
func (m *MockClient) CreateSubscription(ctx context.Context, id string, cfg pubsub.SubscriptionConfig) (*pubsub.Subscription, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSubscription", ctx, id, cfg)
ret0, _ := ret[0].(*pubsub.Subscription)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSubscription indicates an expected call of CreateSubscription.
func (mr *MockClientMockRecorder) CreateSubscription(ctx, id, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockClient)(nil).CreateSubscription), ctx, id, cfg)
}
// CreateTopic mocks base method.
func (m *MockClient) CreateTopic(ctx context.Context, topicID string) (*pubsub.Topic, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTopic", ctx, topicID)
ret0, _ := ret[0].(*pubsub.Topic)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateTopic indicates an expected call of CreateTopic.
func (mr *MockClientMockRecorder) CreateTopic(ctx, topicID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTopic", reflect.TypeOf((*MockClient)(nil).CreateTopic), ctx, topicID)
}
// Subscription mocks base method.
func (m *MockClient) Subscription(id string) *pubsub.Subscription {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscription", id)
ret0, _ := ret[0].(*pubsub.Subscription)
return ret0
}
// Subscription indicates an expected call of Subscription.
func (mr *MockClientMockRecorder) Subscription(id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscription", reflect.TypeOf((*MockClient)(nil).Subscription), id)
}
// Subscriptions mocks base method.
func (m *MockClient) Subscriptions(ctx context.Context) *pubsub.SubscriptionIterator {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscriptions", ctx)
ret0, _ := ret[0].(*pubsub.SubscriptionIterator)
return ret0
}
// Subscriptions indicates an expected call of Subscriptions.
func (mr *MockClientMockRecorder) Subscriptions(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscriptions", reflect.TypeOf((*MockClient)(nil).Subscriptions), ctx)
}
// Topic mocks base method.
func (m *MockClient) Topic(id string) *pubsub.Topic {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Topic", id)
ret0, _ := ret[0].(*pubsub.Topic)
return ret0
}
// Topic indicates an expected call of Topic.
func (mr *MockClientMockRecorder) Topic(id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topic", reflect.TypeOf((*MockClient)(nil).Topic), id)
}
// Topics mocks base method.
func (m *MockClient) Topics(ctx context.Context) *pubsub.TopicIterator {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Topics", ctx)
ret0, _ := ret[0].(*pubsub.TopicIterator)
return ret0
}
// Topics indicates an expected call of Topics.
func (mr *MockClientMockRecorder) Topics(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topics", reflect.TypeOf((*MockClient)(nil).Topics), ctx)
}
// MockWriter is a mock of Writer interface.
type MockWriter struct {
ctrl *gomock.Controller
recorder *MockWriterMockRecorder
}
// MockWriterMockRecorder is the mock recorder for MockWriter.
type MockWriterMockRecorder struct {
mock *MockWriter
}
// NewMockWriter creates a new mock instance.
func NewMockWriter(ctrl *gomock.Controller) *MockWriter {
mock := &MockWriter{ctrl: ctrl}
mock.recorder = &MockWriterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
return m.recorder
}
// CreateSubscription mocks base method.
func (m *MockWriter) CreateSubscription(ctx context.Context, id string, cfg pubsub.SubscriptionConfig) (*pubsub.Subscription, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateSubscription", ctx, id, cfg)
ret0, _ := ret[0].(*pubsub.Subscription)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateSubscription indicates an expected call of CreateSubscription.
func (mr *MockWriterMockRecorder) CreateSubscription(ctx, id, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockWriter)(nil).CreateSubscription), ctx, id, cfg)
}
// Subscription mocks base method.
func (m *MockWriter) Subscription(id string) *pubsub.Subscription {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscription", id)
ret0, _ := ret[0].(*pubsub.Subscription)
return ret0
}
// Subscription indicates an expected call of Subscription.
func (mr *MockWriterMockRecorder) Subscription(id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscription", reflect.TypeOf((*MockWriter)(nil).Subscription), id)
}
// MockReader is a mock of Reader interface.
type MockReader struct {
ctrl *gomock.Controller
recorder *MockReaderMockRecorder
}
// MockReaderMockRecorder is the mock recorder for MockReader.
type MockReaderMockRecorder struct {
mock *MockReader
}
// NewMockReader creates a new mock instance.
func NewMockReader(ctrl *gomock.Controller) *MockReader {
mock := &MockReader{ctrl: ctrl}
mock.recorder = &MockReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockReader) EXPECT() *MockReaderMockRecorder {
return m.recorder
}
// CreateTopic mocks base method.
func (m *MockReader) CreateTopic(ctx context.Context, topicID string) (*pubsub.Topic, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTopic", ctx, topicID)
ret0, _ := ret[0].(*pubsub.Topic)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateTopic indicates an expected call of CreateTopic.
func (mr *MockReaderMockRecorder) CreateTopic(ctx, topicID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTopic", reflect.TypeOf((*MockReader)(nil).CreateTopic), ctx, topicID)
}
// Topic mocks base method.
func (m *MockReader) Topic(id string) *pubsub.Topic {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Topic", id)
ret0, _ := ret[0].(*pubsub.Topic)
return ret0
}
// Topic indicates an expected call of Topic.
func (mr *MockReaderMockRecorder) Topic(id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Topic", reflect.TypeOf((*MockReader)(nil).Topic), id)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=google
//
// Package google is a generated GoMock package.
package google
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/tracing.go
================================================
package google
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
const tracerName = "gofr-gcp-pubsub"
// attributeCarrier implements propagation.TextMapCarrier for Google PubSub message attributes.
type attributeCarrier map[string]string
// Ensure attributeCarrier implements the interface at compile time.
var _ propagation.TextMapCarrier = attributeCarrier(nil)
// Get returns the value for a given key from the PubSub attributes.
func (c attributeCarrier) Get(key string) string {
return c[key]
}
// Set sets a key-value pair in the PubSub attributes.
func (c attributeCarrier) Set(key, value string) {
c[key] = value
}
// Keys returns all keys in the PubSub attributes.
func (c attributeCarrier) Keys() []string {
keys := make([]string, 0, len(c))
for k := range c {
keys = append(keys, k)
}
return keys
}
// injectTraceContext injects the current trace context into PubSub message attributes.
// This allows the consumer to extract the trace context and create span links.
func injectTraceContext(ctx context.Context, attrs map[string]string) map[string]string {
if attrs == nil {
attrs = make(map[string]string)
}
carrier := attributeCarrier(attrs)
otel.GetTextMapPropagator().Inject(ctx, carrier)
return attrs
}
// extractTraceLinks extracts the trace context from PubSub message attributes
// and returns span links to the producer span.
// If no trace context is found, returns nil (creating an orphan span).
func extractTraceLinks(attrs map[string]string) []trace.Link {
if len(attrs) == 0 {
return nil
}
carrier := attributeCarrier(attrs)
// Extract the context from attributes
extractedCtx := otel.GetTextMapPropagator().Extract(context.Background(), carrier)
// Get span context from extracted context
spanCtx := trace.SpanContextFromContext(extractedCtx)
// If valid span context exists, create a link to it
if spanCtx.IsValid() {
return []trace.Link{
{
SpanContext: spanCtx,
},
}
}
return nil
}
// startPublishSpan creates a new span for publishing with trace context injection.
// Returns the updated context, the span, and message attributes with injected trace context.
func startPublishSpan(ctx context.Context, topic string) (context.Context, trace.Span, map[string]string) {
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindProducer),
trace.WithAttributes(
attribute.String("messaging.system", "gcp_pubsub"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "publish"),
),
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "gcp-publish", opts...)
// Inject trace context into message attributes
attrs := injectTraceContext(ctx, nil)
return ctx, span, attrs
}
// extractMessageAttrs extracts string map attributes from message metadata.
// Returns nil if metadata is nil or not of type map[string]string.
func extractMessageAttrs(metaData any) map[string]string {
if metaData == nil {
return nil
}
if attrs, ok := metaData.(map[string]string); ok {
return attrs
}
return nil
}
// startSubscribeSpan creates a new span for subscribing with links to the producer span.
// If trace context exists in message attributes, creates a span linked to the producer.
// Otherwise, creates an orphan span (new trace).
func startSubscribeSpan(ctx context.Context, topic string, msgAttrs map[string]string) (context.Context, trace.Span) {
links := extractTraceLinks(msgAttrs)
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("messaging.system", "gcp_pubsub"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "receive"),
),
}
if len(links) > 0 {
opts = append(opts, trace.WithLinks(links...))
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "gcp-subscribe", opts...)
return ctx, span
}
================================================
FILE: pkg/gofr/datasource/pubsub/google/tracing_test.go
================================================
package google
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
func TestAttributeCarrier_GetSetKeys(t *testing.T) {
carrier := make(attributeCarrier)
// Test Set
carrier.Set("traceparent", "00-1234567890abcdef-fedcba0987654321-01")
carrier.Set("tracestate", "foo=bar")
// Test Get
assert.Equal(t, "00-1234567890abcdef-fedcba0987654321-01", carrier.Get("traceparent"))
assert.Equal(t, "foo=bar", carrier.Get("tracestate"))
assert.Empty(t, carrier.Get("nonexistent"))
// Test Keys
keys := carrier.Keys()
assert.Contains(t, keys, "traceparent")
assert.Contains(t, keys, "tracestate")
// Test Set updates existing key
carrier.Set("traceparent", "00-updated-value")
assert.Equal(t, "00-updated-value", carrier.Get("traceparent"))
}
func TestInjectTraceContext(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
attrs := injectTraceContext(ctx, nil)
require.NotNil(t, attrs)
traceparent, ok := attrs["traceparent"]
require.True(t, ok, "traceparent attribute should be injected")
assert.Contains(t, traceparent, span.SpanContext().TraceID().String())
}
func TestInjectTraceContext_PreservesExistingAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
existing := map[string]string{
"custom-attr": "custom-value",
}
attrs := injectTraceContext(ctx, existing)
assert.Equal(t, "custom-value", attrs["custom-attr"])
_, ok := attrs["traceparent"]
assert.True(t, ok, "traceparent should be injected alongside existing attributes")
}
func TestExtractTraceLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, producerSpan := tp.Tracer("test").Start(context.Background(), "producer-span")
attrs := injectTraceContext(ctx, nil)
producerSpan.End()
links := extractTraceLinks(attrs)
require.Len(t, links, 1, "should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), links[0].SpanContext.SpanID())
}
func TestExtractTraceLinks_NoAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
links := extractTraceLinks(nil)
assert.Nil(t, links, "should return nil for nil attributes")
}
func TestExtractTraceLinks_EmptyAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
links := extractTraceLinks(make(map[string]string))
assert.Nil(t, links, "should return nil for empty attributes")
}
func TestStartPublishSpan(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span, attrs := startPublishSpan(context.Background(), "test-topic")
defer span.End()
require.NotNil(t, span)
assert.True(t, span.SpanContext().IsValid())
require.NotNil(t, ctx)
_, hasTraceparent := attrs["traceparent"]
assert.True(t, hasTraceparent, "attributes should contain traceparent")
}
func TestStartSubscribeSpan_WithLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
_, producerSpan, attrs := startPublishSpan(context.Background(), "test-topic")
producerSpan.End()
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-topic", attrs)
subscribeSpan.End()
spans := exporter.GetSpans()
require.GreaterOrEqual(t, len(spans), 2)
var subSpan *tracetest.SpanStub
for i := range spans {
if spans[i].Name == "gcp-subscribe" {
subSpan = &spans[i]
break
}
}
require.NotNil(t, subSpan, "subscribe span should exist")
require.Len(t, subSpan.Links, 1, "subscribe span should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), subSpan.Links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), subSpan.Links[0].SpanContext.SpanID())
}
func TestStartSubscribeSpan_NoLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-topic", nil)
subscribeSpan.End()
spans := exporter.GetSpans()
require.Len(t, spans, 1)
assert.Empty(t, spans[0].Links, "orphan span should have no links")
}
================================================
FILE: pkg/gofr/datasource/pubsub/interface.go
================================================
// Package pubsub provides a foundation for implementing pub/sub clients for various message brokers such as google pub-sub,
// kafka and MQTT. It defines interfaces for publishing and subscribing to messages, managing topics, and handling messages.
package pubsub
import (
"context"
"gofr.dev/pkg/gofr/datasource"
)
type Publisher interface {
Publish(ctx context.Context, topic string, message []byte) error
}
type Subscriber interface {
Subscribe(ctx context.Context, topic string) (*Message, error)
}
type Client interface {
Publisher
Subscriber
Health() datasource.Health
CreateTopic(context context.Context, name string) error
DeleteTopic(context context.Context, name string) error
Query(ctx context.Context, query string, args ...any) ([]byte, error)
Close() error
}
type Committer interface {
Commit()
}
type Logger interface {
Debugf(format string, args ...any)
Debug(args ...any)
Logf(format string, args ...any)
Log(args ...any)
Errorf(format string, args ...any)
Error(args ...any)
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/conn.go
================================================
package kafka
import (
"context"
"errors"
"net"
"strconv"
"sync"
"github.com/segmentio/kafka-go"
)
//nolint:unused // We need this wrap around for testing purposes.
type Conn struct {
conns []*kafka.Conn
}
// initialize creates and configures all Kafka client components.
func (k *kafkaClient) initialize(ctx context.Context) error {
dialer, err := setupDialer(&k.config)
if err != nil {
return err
}
conns, err := connectToBrokers(ctx, k.config.Brokers, dialer, k.logger)
if err != nil {
return err
}
multi := &multiConn{
conns: conns,
dialer: dialer,
}
writer := createKafkaWriter(&k.config, dialer, k.logger)
reader := make(map[string]Reader)
k.logger.Logf("connected to %d Kafka brokers", len(conns))
k.dialer = dialer
k.conn = multi
k.writer = writer
k.reader = reader
return nil
}
func (k *kafkaClient) getNewReader(topic string) Reader {
reader := kafka.NewReader(kafka.ReaderConfig{
GroupID: k.config.ConsumerGroupID,
Brokers: k.config.Brokers,
Topic: topic,
MinBytes: defaultMinBytes,
MaxBytes: defaultMaxBytes,
Dialer: k.dialer,
StartOffset: int64(k.config.OffSet),
})
return reader
}
func (k *kafkaClient) DeleteTopic(_ context.Context, name string) error {
return k.conn.DeleteTopics(name)
}
func (k *kafkaClient) Controller() (broker kafka.Broker, err error) {
return k.conn.Controller()
}
func (k *kafkaClient) CreateTopic(_ context.Context, name string) error {
topics := kafka.TopicConfig{Topic: name, NumPartitions: 1, ReplicationFactor: 1}
return k.conn.CreateTopics(topics)
}
type multiConn struct {
conns []Connection
dialer *kafka.Dialer
mu sync.RWMutex
}
func (m *multiConn) Controller() (kafka.Broker, error) {
if len(m.conns) == 0 {
return kafka.Broker{}, errNoActiveConnections
}
// Try all connections until we find one that works
for _, conn := range m.conns {
if conn == nil {
continue
}
controller, err := conn.Controller()
if err == nil {
return controller, nil
}
}
return kafka.Broker{}, errNoActiveConnections
}
func (m *multiConn) CreateTopics(topics ...kafka.TopicConfig) error {
controller, err := m.Controller()
if err != nil {
return err
}
controllerAddr := net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port))
controllerResolvedAddr, err := net.ResolveTCPAddr("tcp", controllerAddr)
if err != nil {
return err
}
m.mu.RLock()
defer m.mu.RUnlock()
for _, conn := range m.conns {
if conn == nil {
continue
}
if tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
if tcpAddr.IP.Equal(controllerResolvedAddr.IP) && tcpAddr.Port == controllerResolvedAddr.Port {
return conn.CreateTopics(topics...)
}
}
}
// If not found, create a new connection
conn, err := m.dialer.DialContext(context.Background(), "tcp", controllerAddr)
if err != nil {
return err
}
m.conns = append(m.conns, conn)
return conn.CreateTopics(topics...)
}
func (m *multiConn) DeleteTopics(topics ...string) error {
controller, err := m.Controller()
if err != nil {
return err
}
controllerAddr := net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port))
controllerResolvedAddr, err := net.ResolveTCPAddr("tcp", controllerAddr)
if err != nil {
return err
}
for _, conn := range m.conns {
if conn == nil {
continue
}
if tcpAddr, ok := conn.RemoteAddr().(*net.TCPAddr); ok {
// Match IP (after resolution) and Port
if tcpAddr.IP.Equal(controllerResolvedAddr.IP) && tcpAddr.Port == controllerResolvedAddr.Port {
return conn.DeleteTopics(topics...)
}
}
}
// If not found, create a new connection
conn, err := m.dialer.DialContext(context.Background(), "tcp", controllerAddr)
if err != nil {
return err
}
m.conns = append(m.conns, conn)
return conn.DeleteTopics(topics...)
}
func (m *multiConn) Close() error {
var err error
for _, conn := range m.conns {
if conn != nil {
err = errors.Join(err, conn.Close())
}
}
return err
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/errors.go
================================================
package kafka
import "errors"
var (
ErrConsumerGroupNotProvided = errors.New("consumer group id not provided")
errFailedToConnectBrokers = errors.New("failed to connect to any kafka brokers")
errBrokerNotProvided = errors.New("kafka broker address not provided")
errPublisherNotConfigured = errors.New("can't publish message. Publisher not configured or topic is empty")
errBatchSize = errors.New("KAFKA_BATCH_SIZE must be greater than 0")
errBatchBytes = errors.New("KAFKA_BATCH_BYTES must be greater than 0")
errBatchTimeout = errors.New("KAFKA_BATCH_TIMEOUT must be greater than 0")
errClientNotConnected = errors.New("kafka client not connected")
errUnsupportedSASLMechanism = errors.New("unsupported SASL mechanism")
errSASLCredentialsMissing = errors.New("SASL credentials missing")
errUnsupportedSecurityProtocol = errors.New("unsupported security protocol")
errNoActiveConnections = errors.New("no active connections to brokers")
errCACertFileRead = errors.New("failed to read CA certificate file")
errClientCertLoad = errors.New("failed to load client certificate")
errNotController = errors.New("not a controller")
errUnreachable = errors.New("unreachable")
)
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/health.go
================================================
package kafka
import (
"encoding/json"
"fmt"
"net"
"strconv"
"gofr.dev/pkg/gofr/datasource"
)
func (k *kafkaClient) Health() datasource.Health {
health := datasource.Health{
Status: datasource.StatusDown,
Details: make(map[string]any),
}
if k.conn == nil {
health.Details["error"] = "invalid connection type"
return health
}
k.conn.mu.RLock()
defer k.conn.mu.RUnlock()
brokerStatus, allDown := k.evaluateBrokerHealth()
if !allDown {
health.Status = datasource.StatusUp
}
health.Details["brokers"] = brokerStatus
health.Details["backend"] = "KAFKA"
health.Details["writer"] = k.getWriterStatsAsMap()
health.Details["readers"] = k.getReaderStatsAsMap()
return health
}
func (k *kafkaClient) evaluateBrokerHealth() ([]map[string]any, bool) {
var (
statusList = make([]map[string]any, 0)
controllerAddr string
allDown = true
)
for _, conn := range k.conn.conns {
if conn == nil {
continue
}
status := checkBroker(conn, &controllerAddr)
if status["status"] == brokerStatusUp {
allDown = false
}
statusList = append(statusList, status)
}
return statusList, allDown
}
func checkBroker(conn Connection, controllerAddr *string) map[string]any {
brokerAddr := conn.RemoteAddr().String()
status := map[string]any{
"broker": brokerAddr,
"status": "DOWN",
"isController": false,
"error": nil,
}
_, err := conn.ReadPartitions()
if err != nil {
status["error"] = err.Error()
return status
}
status["status"] = brokerStatusUp
if *controllerAddr == "" {
controller, err := conn.Controller()
if err != nil {
status["error"] = fmt.Sprintf("controller lookup failed: %v", err)
} else if controller.Host != "" {
*controllerAddr = net.JoinHostPort(controller.Host, strconv.Itoa(controller.Port))
}
}
status["isController"] = brokerAddr == *controllerAddr
return status
}
func (k *kafkaClient) getReaderStatsAsMap() []any {
readerStats := make([]any, 0)
for _, reader := range k.reader {
var readerStat map[string]any
if err := convertStructToMap(reader.Stats(), &readerStat); err != nil {
k.logger.Errorf("kafka Reader Stats processing failed: %v", err)
continue // Log the error but continue processing other readers
}
readerStats = append(readerStats, readerStat)
}
return readerStats
}
func (k *kafkaClient) getWriterStatsAsMap() map[string]any {
writerStats := make(map[string]any)
if err := convertStructToMap(k.writer.Stats(), &writerStats); err != nil {
k.logger.Errorf("kafka Writer Stats processing failed: %v", err)
return nil
}
return writerStats
}
// convertStructToMap tries to convert any struct to a map representation by first marshaling it to JSON, then unmarshalling into a map.
func convertStructToMap(input, output any) error {
body, err := json.Marshal(input)
if err != nil {
return err
}
err = json.Unmarshal(body, &output)
if err != nil {
return err
}
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/health_test.go
================================================
package kafka
import (
"context"
"net"
"testing"
"github.com/segmentio/kafka-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"gofr.dev/pkg/gofr/datasource"
)
// MockConn simulates a Kafka connection.
type MockConn struct {
mock.Mock
addr string
isHealthy bool
isControl bool
}
func (m *MockConn) RemoteAddr() net.Addr {
return mockAddr{m.addr}
}
func (m *MockConn) ReadPartitions(...string) ([]kafka.Partition, error) {
if m.isHealthy {
return []kafka.Partition{{}}, nil
}
return nil, errUnreachable
}
func (m *MockConn) Controller() (kafka.Broker, error) {
if m.isControl {
host, _, _ := net.SplitHostPort(m.addr)
port := 9092
return kafka.Broker{Host: host, Port: port}, nil
}
return kafka.Broker{}, errNotController
}
// Add minimal required method stubs.
func (*MockConn) Close() error { return nil }
func (*MockConn) CreateTopics(...kafka.TopicConfig) error { return nil }
func (*MockConn) DeleteTopics(...string) error { return nil }
type mockAddr struct{ addr string }
func (mockAddr) Network() string { return "tcp" }
func (m mockAddr) String() string { return m.addr }
func TestKafkaHealth_AllBrokersUp(t *testing.T) {
client := &kafkaClient{
conn: &multiConn{
conns: []Connection{
&MockConn{addr: "127.0.0.1:9092", isHealthy: true, isControl: true},
&MockConn{addr: "127.0.0.2:9092", isHealthy: true, isControl: false},
},
},
reader: make(map[string]Reader),
writer: &mockWriter{},
logger: &mockLogger{},
}
health := client.Health()
assert.Equal(t, datasource.StatusUp, health.Status)
assert.Len(t, health.Details["brokers"], 2)
assert.Contains(t, health.Details["brokers"], map[string]any{
"broker": "127.0.0.1:9092",
"status": "UP",
"isController": true,
"error": nil,
})
}
func TestKafkaHealth_SomeBrokersUpSomeDown(t *testing.T) {
client := &kafkaClient{
conn: &multiConn{
conns: []Connection{
&MockConn{addr: "127.0.0.1:9092", isHealthy: true, isControl: false},
&MockConn{addr: "127.0.0.2:9092", isHealthy: false},
&MockConn{addr: "127.0.0.3:9092", isHealthy: true, isControl: true},
},
},
reader: make(map[string]Reader),
writer: &mockWriter{},
logger: &mockLogger{},
}
health := client.Health()
assert.Equal(t, datasource.StatusUp, health.Status) // Because at least one broker is down
brokers := health.Details["brokers"].([]map[string]any)
assert.Len(t, brokers, 3)
statusMap := map[string]string{}
for _, broker := range brokers {
addr := broker["broker"].(string)
status := broker["status"].(string)
statusMap[addr] = status
}
assert.Equal(t, "UP", statusMap["127.0.0.1:9092"])
assert.Equal(t, "DOWN", statusMap["127.0.0.2:9092"])
assert.Equal(t, "UP", statusMap["127.0.0.3:9092"])
}
func TestKafkaHealth_AllBrokersDown(t *testing.T) {
client := &kafkaClient{
conn: &multiConn{
conns: []Connection{
&MockConn{addr: "127.0.0.1:9092", isHealthy: false},
},
},
reader: make(map[string]Reader),
writer: &mockWriter{},
logger: &mockLogger{},
}
health := client.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Len(t, health.Details["brokers"], 1)
brokerInfo := health.Details["brokers"].([]map[string]any)[0]
assert.Equal(t, "DOWN", brokerInfo["status"])
assert.NotNil(t, brokerInfo["error"])
}
func TestKafkaHealth_InvalidConnType(t *testing.T) {
client := &kafkaClient{
conn: nil,
reader: make(map[string]Reader),
writer: &mockWriter{},
logger: &mockLogger{},
}
health := client.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Equal(t, "invalid connection type", health.Details["error"])
}
// --- Mock implementations for Writer/Reader/Logger/Stats
type mockWriter struct{}
func (*mockWriter) Stats() kafka.WriterStats {
return kafka.WriterStats{
Dials: 1,
Writes: 1,
Messages: 1,
Bytes: 1024,
Errors: 0,
}
}
func (*mockWriter) WriteMessages(context.Context, ...kafka.Message) error { return nil }
func (*mockWriter) Close() error { return nil }
type mockLogger struct{}
func (*mockLogger) Errorf(string, ...any) {}
func (*mockLogger) Debugf(string, ...any) {}
func (*mockLogger) Logf(string, ...any) {}
func (*mockLogger) Log(...any) {}
func (*mockLogger) Error(...any) {}
func (*mockLogger) Debug(...any) {}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/helper.go
================================================
package kafka
import (
"context"
"errors"
"fmt"
"io"
"time"
"github.com/segmentio/kafka-go"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
func validateConfigs(conf *Config) error {
if err := validateRequiredFields(conf); err != nil {
return err
}
setDefaultSecurityProtocol(conf)
if err := validateSASLConfigs(conf); err != nil {
return err
}
if err := validateTLSConfigs(conf); err != nil {
return err
}
if err := validateSecurityProtocol(conf); err != nil {
return err
}
return nil
}
func validateRequiredFields(conf *Config) error {
if len(conf.Brokers) == 0 {
return errBrokerNotProvided
}
if conf.BatchSize <= 0 {
return fmt.Errorf("batch size must be greater than 0: %w", errBatchSize)
}
if conf.BatchBytes <= 0 {
return fmt.Errorf("batch bytes must be greater than 0: %w", errBatchBytes)
}
if conf.BatchTimeout <= 0 {
return fmt.Errorf("batch timeout must be greater than 0: %w", errBatchTimeout)
}
return nil
}
// retryConnect handles the retry mechanism for connecting to the Kafka broker.
func (k *kafkaClient) retryConnect(ctx context.Context) {
for {
time.Sleep(defaultRetryTimeout)
err := k.initialize(ctx)
if err != nil {
var brokers any
if len(k.config.Brokers) > 1 {
brokers = k.config.Brokers
} else {
brokers = k.config.Brokers[0]
}
k.logger.Errorf("could not connect to Kafka at '%v', error: %v", brokers, err)
continue
}
return
}
}
func (k *kafkaClient) isConnected() bool {
if k.conn == nil {
return false
}
_, err := k.conn.Controller()
return err == nil
}
func setupDialer(conf *Config) (*kafka.Dialer, error) {
dialer := &kafka.Dialer{
Timeout: defaultRetryTimeout,
DualStack: true,
}
if conf.SecurityProtocol == protocolSASL || conf.SecurityProtocol == protocolSASLSSL {
mechanism, err := getSASLMechanism(conf.SASLMechanism, conf.SASLUser, conf.SASLPassword)
if err != nil {
return nil, err
}
dialer.SASLMechanism = mechanism
}
if conf.SecurityProtocol == "SSL" || conf.SecurityProtocol == "SASL_SSL" {
tlsConfig, err := createTLSConfig(&conf.TLS)
if err != nil {
return nil, err
}
dialer.TLS = tlsConfig
}
return dialer, nil
}
// connectToBrokers connects to Kafka brokers with context support.
func connectToBrokers(ctx context.Context, brokers []string, dialer *kafka.Dialer, logger pubsub.Logger) ([]Connection, error) {
conns := make([]Connection, 0)
if len(brokers) == 0 {
return nil, errBrokerNotProvided
}
for _, broker := range brokers {
conn, err := dialer.DialContext(ctx, "tcp", broker)
if err != nil {
logger.Errorf("failed to connect to broker %s: %v", broker, err)
continue
}
conns = append(conns, conn)
}
if len(conns) == 0 {
return nil, errFailedToConnectBrokers
}
return conns, nil
}
func createKafkaWriter(conf *Config, dialer *kafka.Dialer, logger pubsub.Logger) Writer {
return kafka.NewWriter(kafka.WriterConfig{
Brokers: conf.Brokers,
Dialer: dialer,
BatchSize: conf.BatchSize,
BatchBytes: conf.BatchBytes,
BatchTimeout: time.Duration(conf.BatchTimeout),
Logger: kafka.LoggerFunc(logger.Debugf),
})
}
func (*kafkaClient) parseQueryArgs(args ...any) (offSet int64, limit int) {
var offset int64
limit = 10
if len(args) > 0 {
if val, ok := args[0].(int64); ok {
offset = val
}
}
if len(args) > 1 {
if val, ok := args[1].(int); ok {
limit = val
}
}
return offset, limit
}
func (k *kafkaClient) createReader(topic string, offset int64) (*kafka.Reader, error) {
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: k.config.Brokers,
Topic: topic,
Partition: k.config.Partition,
MinBytes: 1,
MaxBytes: defaultMaxBytes,
StartOffset: kafka.FirstOffset,
})
if err := reader.SetOffset(offset); err != nil {
reader.Close()
return nil, fmt.Errorf("failed to set offset: %w", err)
}
return reader, nil
}
func (*kafkaClient) getReadContext(ctx context.Context) context.Context {
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
readCtx, cancel := context.WithTimeout(ctx, defaultReadTimeout)
_ = cancel // We can't defer here, but timeout will handle cleanup
return readCtx
}
return ctx
}
func (k *kafkaClient) readMessages(ctx context.Context, reader *kafka.Reader, limit int) ([]byte, error) {
var result []byte
for i := 0; i < limit; i++ {
msg, err := reader.ReadMessage(ctx)
if err != nil {
if k.isExpectedError(err) {
break
}
return nil, fmt.Errorf("failed to read message: %w", err)
}
if len(result) > 0 {
result = append(result, '\n')
}
result = append(result, msg.Value...)
}
return result, nil
}
func (*kafkaClient) isExpectedError(err error) bool {
return errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.EOF)
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/interfaces.go
================================================
package kafka
import (
"context"
"net"
"github.com/segmentio/kafka-go"
)
//go:generate go run go.uber.org/mock/mockgen -source=interfaces.go -destination=mock_interfaces.go -package=kafka
type Reader interface {
ReadMessage(ctx context.Context) (kafka.Message, error)
FetchMessage(ctx context.Context) (kafka.Message, error)
CommitMessages(ctx context.Context, msgs ...kafka.Message) error
Stats() kafka.ReaderStats
Close() error
}
type Writer interface {
WriteMessages(ctx context.Context, msg ...kafka.Message) error
Close() error
Stats() kafka.WriterStats
}
type Connection interface {
Controller() (broker kafka.Broker, err error)
CreateTopics(topics ...kafka.TopicConfig) error
DeleteTopics(topics ...string) error
RemoteAddr() net.Addr
ReadPartitions(topics ...string) (partitions []kafka.Partition, err error)
Close() error
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/kafka.go
================================================
// Package kafka provides a client for interacting with Apache Kafka message queues.This package facilitates interaction
// with Apache Kafka, allowing publishing and subscribing to topics, managing consumer groups, and handling messages.
package kafka
import (
"context"
"errors"
"sync"
"time"
"github.com/segmentio/kafka-go"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
const (
DefaultBatchSize = 100
DefaultBatchBytes = 1048576
DefaultBatchTimeout = 1000
defaultMaxBytes = 10e6 // 10MB
defaultMinBytes = 10e3
defaultRetryTimeout = 10 * time.Second
defaultReadTimeout = 30 * time.Second
protocolPlainText = "PLAINTEXT"
protocolSASL = "SASL_PLAINTEXT"
protocolSSL = "SSL"
protocolSASLSSL = "SASL_SSL"
messageMultipleBrokers = "MULTIPLE_BROKERS"
brokerStatusUp = "UP"
)
var errEmptyTopicName = errors.New("topic name cannot be empty")
type Config struct {
Brokers []string
Partition int
ConsumerGroupID string
OffSet int
BatchSize int
BatchBytes int
BatchTimeout int
RetryTimeout time.Duration
SASLMechanism string
SASLUser string
SASLPassword string
SecurityProtocol string
TLS TLSConfig
}
type kafkaClient struct {
dialer *kafka.Dialer
conn *multiConn
writer Writer
reader map[string]Reader
mu *sync.RWMutex
logger pubsub.Logger
config Config
metrics Metrics
}
func New(conf *Config, logger pubsub.Logger, metrics Metrics) *kafkaClient { //nolint:revive // New allows
// returning unexported types as intended.
err := validateConfigs(conf)
if err != nil {
logger.Errorf("could not initialize kafka, error: %v", err)
return nil
}
if len(conf.Brokers) == 1 {
logger.Debugf("connecting to Kafka broker: '%s'", conf.Brokers[0])
} else {
logger.Debugf("connecting to Kafka brokers: %v", conf.Brokers)
}
client := &kafkaClient{
logger: logger,
config: *conf,
metrics: metrics,
mu: &sync.RWMutex{},
}
ctx := context.Background()
err = client.initialize(ctx)
if err != nil {
logger.Errorf("failed to connect to kafka at %v, error: %v", conf.Brokers, err)
go client.retryConnect(ctx)
return client
}
return client
}
func (k *kafkaClient) Publish(ctx context.Context, topic string, message []byte) error {
ctx, span, headers := startPublishSpan(ctx, topic)
defer span.End()
k.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
if k.writer == nil || topic == "" {
return errPublisherNotConfigured
}
start := time.Now()
err := k.writer.WriteMessages(ctx,
kafka.Message{
Topic: topic,
Value: message,
Headers: headers,
Time: time.Now(),
},
)
end := time.Since(start)
if err != nil {
k.logger.Errorf("failed to publish message to kafka broker, error: %v", err)
return err
}
var hostName string
if len(k.config.Brokers) > 1 {
hostName = messageMultipleBrokers
} else {
hostName = k.config.Brokers[0]
}
k.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: hostName,
PubSubBackend: "KAFKA",
Time: end.Microseconds(),
})
k.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
func (k *kafkaClient) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if !k.isConnected() {
return nil, errClientNotConnected
}
if query == "" {
return nil, errEmptyTopicName
}
offset, limit := k.parseQueryArgs(args...)
reader, err := k.createReader(query, offset)
if err != nil {
return nil, err
}
defer reader.Close()
readCtx := k.getReadContext(ctx)
return k.readMessages(readCtx, reader, limit)
}
func (k *kafkaClient) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
if !k.isConnected() {
time.Sleep(defaultRetryTimeout)
return nil, errClientNotConnected
}
if k.config.ConsumerGroupID == "" {
k.logger.Error("cannot subscribe as consumer_id is not provided in configs")
return &pubsub.Message{}, ErrConsumerGroupNotProvided
}
// Span will be created after fetching message to access headers
var span trace.Span
k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic, "consumer_group", k.config.ConsumerGroupID)
var reader Reader
// Lock the reader map to ensure only one subscriber access the reader at a time
k.mu.Lock()
if k.reader == nil {
k.reader = make(map[string]Reader)
}
if k.reader[topic] == nil {
k.reader[topic] = k.getNewReader(topic)
}
reader = k.reader[topic]
// Release the lock on the reader map after update
k.mu.Unlock()
start := time.Now()
msg, err := reader.FetchMessage(ctx)
if err != nil {
k.logger.Errorf("failed to read message from kafka topic %s: %v", topic, err)
return nil, err
}
// Create span with links to producer span from message headers
ctx, span = startSubscribeSpan(ctx, topic, msg.Headers)
defer span.End()
m := pubsub.NewMessage(ctx)
m.Value = msg.Value
m.Topic = topic
m.Committer = newKafkaMessage(&msg, k.reader[topic], k.logger)
end := time.Since(start)
var hostName string
if len(k.config.Brokers) > 1 {
hostName = "multiple brokers"
} else {
hostName = k.config.Brokers[0]
}
k.logger.Debug(&pubsub.Log{
Mode: "SUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(msg.Value),
Topic: topic,
Host: hostName,
PubSubBackend: "KAFKA",
Time: end.Microseconds(),
})
k.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic, "consumer_group", k.config.ConsumerGroupID)
return m, err
}
func (k *kafkaClient) Close() (err error) {
for _, r := range k.reader {
err = errors.Join(err, r.Close())
}
if k.writer != nil {
err = errors.Join(err, k.writer.Close())
}
if k.conn != nil {
err = errors.Join(k.conn.Close())
}
return err
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/kafka_sasl.go
================================================
package kafka
import (
"fmt"
"strings"
"github.com/segmentio/kafka-go/sasl"
"github.com/segmentio/kafka-go/sasl/plain"
"github.com/segmentio/kafka-go/sasl/scram"
)
func setDefaultSecurityProtocol(conf *Config) {
if conf.SecurityProtocol == "" {
conf.SecurityProtocol = protocolPlainText
}
}
func validateSecurityProtocol(conf *Config) error {
protocol := strings.ToUpper(conf.SecurityProtocol)
switch protocol {
case protocolPlainText, protocolSASL, protocolSASLSSL, protocolSSL:
return nil
default:
return fmt.Errorf("unsupported security protocol: %s: %w", protocol, errUnsupportedSecurityProtocol)
}
}
func getSASLMechanism(mechanism, username, password string) (sasl.Mechanism, error) {
switch strings.ToUpper(mechanism) {
case "PLAIN":
return plain.Mechanism{
Username: username,
Password: password,
}, nil
case "SCRAM-SHA-256":
mechanism, _ := scram.Mechanism(scram.SHA256, username, password)
return mechanism, nil
case "SCRAM-SHA-512":
mechanism, _ := scram.Mechanism(scram.SHA512, username, password)
return mechanism, nil
default:
return nil, fmt.Errorf("%w: %s", errUnsupportedSASLMechanism, mechanism)
}
}
func validateSASLConfigs(conf *Config) error {
protocol := strings.ToUpper(conf.SecurityProtocol)
if protocol == protocolSASL || protocol == protocolSASLSSL {
if conf.SASLMechanism == "" || conf.SASLUser == "" || conf.SASLPassword == "" {
return fmt.Errorf("SASL credentials missing: %w", errSASLCredentialsMissing)
}
}
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/kafka_sasl_test.go
================================================
package kafka
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSASLMechanism_Success(t *testing.T) {
tests := []struct {
name string
mechanism string
username string
password string
expectedName string
}{
{
name: "PLAIN uppercase",
mechanism: "PLAIN",
username: "user",
password: "pass",
expectedName: "PLAIN",
},
{
name: "plain lowercase",
mechanism: "plain",
username: "user",
password: "pass",
expectedName: "PLAIN",
},
{
name: "SCRAM-SHA-256",
mechanism: "SCRAM-SHA-256",
username: "user",
password: "pass",
expectedName: "SCRAM-SHA-256",
},
{
name: "SCRAM-SHA-512",
mechanism: "SCRAM-SHA-512",
username: "user",
password: "pass",
expectedName: "SCRAM-SHA-512",
},
}
for _, tc := range tests {
mechanism, err := getSASLMechanism(tc.mechanism, tc.username, tc.password)
require.NoError(t, err, "unexpected error: %v", err)
assert.Equal(t, tc.expectedName, mechanism.Name(),
"expected mechanism name %q, got %q", tc.expectedName, mechanism.Name())
}
}
func TestGetSASLMechanism_Failure(t *testing.T) {
_, err := getSASLMechanism("FOO", "user", "pass")
require.Error(t, err, "expected an error for unsupported mechanism but got none")
assert.Contains(t, err.Error(), "unsupported SASL mechanism",
"unexpected error message: %v", err)
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/kafka_test.go
================================================
package kafka
import (
"context"
"errors"
"net"
"sync"
"testing"
"time"
"github.com/segmentio/kafka-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestValidateConfigs_ValidCases(t *testing.T) {
testCases := []struct {
name string
config Config
expected error
}{
{
name: "Valid Config",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SASLMechanism: "PLAIN",
SASLUser: "user",
SASLPassword: "password",
SecurityProtocol: "SASL_PLAINTEXT",
},
expected: nil,
},
{
name: "Valid PLAINTEXT Protocol",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: protocolPlainText,
},
expected: nil,
},
{
name: "Valid SSL Protocol with TLS Configs",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: "SSL",
TLS: TLSConfig{
CACertFile: "ca.pem",
CertFile: "cert.pem",
KeyFile: "key.pem",
},
},
expected: nil,
},
{
name: "Valid SASL_SSL Protocol with TLS and SASL Configs",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: "SASL_SSL",
SASLMechanism: "PLAIN",
SASLUser: "user",
SASLPassword: "password",
TLS: TLSConfig{
CACertFile: "ca.pem",
CertFile: "cert.pem",
KeyFile: "key.pem",
},
},
expected: nil,
},
{
name: "Valid SSL Protocol with InsecureSkipVerify",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: "SSL",
TLS: TLSConfig{
InsecureSkipVerify: true,
},
},
expected: nil,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateConfigs(&tc.config)
if !errors.Is(err, tc.expected) {
t.Errorf("Expected error %v, but got %v", tc.expected, err)
}
})
}
}
func TestValidateConfigs_InvalidCases(t *testing.T) {
testCases := []struct {
name string
config Config
expected error
}{
{
name: "Empty Broker",
config: Config{
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
},
expected: errBrokerNotProvided,
},
{
name: "Zero BatchSize",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 0,
BatchBytes: 1,
BatchTimeout: 1,
},
expected: errBatchSize,
},
{
name: "Zero BatchBytes",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 0,
BatchTimeout: 1,
},
expected: errBatchBytes,
},
{
name: "Zero BatchTimeout",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 0,
},
expected: errBatchTimeout,
},
{
name: "SASL_PLAINTEXT with Missing SASLMechanism",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: "SASL_PLAINTEXT",
SASLUser: "user",
SASLPassword: "password",
},
expected: errSASLCredentialsMissing,
},
{
name: "Unsupported Security Protocol",
config: Config{
Brokers: []string{"kafkabroker"},
BatchSize: 1,
BatchBytes: 1,
BatchTimeout: 1,
SecurityProtocol: "Invalid",
},
expected: errUnsupportedSecurityProtocol,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := validateConfigs(&tc.config)
if !errors.Is(err, tc.expected) {
t.Errorf("Expected error %v, but got %v", tc.expected, err)
}
})
}
}
func TestKafkaClient_PublishError(t *testing.T) {
var (
err error
errPublish = testutil.CustomError{ErrorMessage: "publishing error"}
)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockWriter := NewMockWriter(ctrl)
mockMetrics := NewMockMetrics(ctrl)
k := &kafkaClient{writer: mockWriter, metrics: mockMetrics}
ctx := t.Context()
testCases := []struct {
desc string
client *kafkaClient
mockCalls *gomock.Call
topic string
msg []byte
expErr error
expLog string
}{
{
desc: "error writer is nil",
client: &kafkaClient{metrics: mockMetrics},
topic: "test",
expErr: errPublisherNotConfigured,
},
{
desc: "error topic is not provided",
client: k,
expErr: errPublisherNotConfigured,
},
{
desc: "error while publishing message",
client: k,
topic: "test",
mockCalls: mockWriter.EXPECT().WriteMessages(gomock.Any(), gomock.Any()).Return(errPublish),
expErr: errPublish,
expLog: "failed to publish message to kafka broker",
},
}
for _, tc := range testCases {
testFunc := func() {
logger := logging.NewMockLogger(logging.DEBUG)
k.logger = logger
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "topic", tc.topic)
err = tc.client.Publish(ctx, tc.topic, tc.msg)
}
logs := testutil.StderrOutputForFunc(testFunc)
assert.Equal(t, tc.expErr, err)
assert.Contains(t, logs, tc.expLog)
}
}
func TestKafkaClient_Publish(t *testing.T) {
var err error
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockWriter := NewMockWriter(ctrl)
mockMetrics := NewMockMetrics(ctrl)
logs := testutil.StdoutOutputForFunc(func() {
ctx := t.Context()
logger := logging.NewMockLogger(logging.DEBUG)
k := &kafkaClient{
writer: mockWriter,
logger: logger,
metrics: mockMetrics,
config: Config{
Brokers: []string{"localhost:9092"}, // Make sure Broker is not empty
},
}
mockWriter.EXPECT().WriteMessages(gomock.Any(), gomock.Any()).Return(nil)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "topic", "test")
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_success_count", "topic", "test")
err = k.Publish(ctx, "test", []byte(`hello`))
})
require.NoError(t, err)
assert.Contains(t, logs, "KAFKA")
assert.Contains(t, logs, "PUB")
assert.Contains(t, logs, "hello")
assert.Contains(t, logs, "test")
}
func TestKafkaClient_SubscribeSuccess(t *testing.T) {
var (
msg *pubsub.Message
err error
)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
mockReader := NewMockReader(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockConnection := NewMockConnection(ctrl)
k := &kafkaClient{
dialer: &kafka.Dialer{},
writer: nil,
reader: map[string]Reader{
"test": mockReader,
},
conn: &multiConn{
conns: []Connection{
mockConnection,
},
},
logger: nil,
config: Config{
ConsumerGroupID: "consumer",
Brokers: []string{"kafkabroker"},
OffSet: -1,
},
mu: &sync.RWMutex{},
metrics: mockMetrics,
}
expMessage := pubsub.Message{
Value: []byte(`hello`),
Topic: "test",
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
mockReader.EXPECT().FetchMessage(gomock.Any()).
Return(kafka.Message{Value: []byte(`hello`), Topic: "test"}, nil)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test",
"consumer_group", gomock.Any())
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", "test",
"consumer_group", gomock.Any())
logs := testutil.StdoutOutputForFunc(func() {
logger := logging.NewMockLogger(logging.DEBUG)
k.logger = logger
msg, err = k.Subscribe(ctx, "test")
})
require.NoError(t, err)
assert.NotNil(t, msg.Context())
assert.Equal(t, expMessage.Value, msg.Value)
assert.Equal(t, expMessage.Topic, msg.Topic)
assert.Contains(t, logs, "KAFKA")
assert.Contains(t, logs, "hello")
assert.Contains(t, logs, "kafkabroker")
assert.Contains(t, logs, "test")
}
func TestKafkaClient_Subscribe_ErrConsumerGroupID(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnection := NewMockConnection(ctrl)
m := &multiConn{
conns: []Connection{
mockConnection,
},
}
k := &kafkaClient{
dialer: &kafka.Dialer{},
config: Config{
Brokers: []string{"kafkabroker"},
OffSet: -1,
},
conn: m,
logger: logging.NewMockLogger(logging.INFO),
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
msg, err := k.Subscribe(t.Context(), "test")
assert.NotNil(t, msg)
assert.Equal(t, ErrConsumerGroupNotProvided, err)
}
func TestKafkaClient_SubscribeError(t *testing.T) {
var (
msg *pubsub.Message
err error
errSub = testutil.CustomError{ErrorMessage: "error while subscribing"}
)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
mockReader := NewMockReader(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockConnection := NewMockConnection(ctrl)
m := &multiConn{
conns: []Connection{
mockConnection,
},
}
k := &kafkaClient{
dialer: &kafka.Dialer{},
writer: nil,
reader: map[string]Reader{
"test": mockReader,
},
conn: m,
logger: logging.NewMockLogger(logging.INFO),
config: Config{
ConsumerGroupID: "consumer",
Brokers: []string{"kafkabroker"},
OffSet: -1,
},
mu: &sync.RWMutex{},
metrics: mockMetrics,
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
mockReader.EXPECT().FetchMessage(gomock.Any()).
Return(kafka.Message{}, errSub)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count",
"topic", "test", "consumer_group", k.config.ConsumerGroupID)
logs := testutil.StderrOutputForFunc(func() {
logger := logging.NewMockLogger(logging.DEBUG)
k.logger = logger
msg, err = k.Subscribe(ctx, "test")
})
require.Error(t, err)
assert.Equal(t, errSub, err)
assert.Nil(t, msg)
assert.Contains(t, logs, "failed to read message from kafka topic test: error while subscribing")
}
func TestKafkaClient_Close(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockWriter := NewMockWriter(ctrl)
mockReader := NewMockReader(ctrl)
mockConn := NewMockConnection(ctrl)
k := kafkaClient{reader: map[string]Reader{"test-topic": mockReader}, writer: mockWriter, conn: &multiConn{
conns: []Connection{
mockConn,
},
}}
mockWriter.EXPECT().Close().Return(nil)
mockReader.EXPECT().Close().Return(nil)
mockConn.EXPECT().Close().Return(nil)
err := k.Close()
require.NoError(t, err)
}
func TestKafkaClient_CloseError(t *testing.T) {
var (
err error
errClose = testutil.CustomError{ErrorMessage: "close error"}
)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockWriter := NewMockWriter(ctrl)
k := kafkaClient{writer: mockWriter}
mockWriter.EXPECT().Close().Return(errClose)
logger := logging.NewMockLogger(logging.ERROR)
k.logger = logger
err = k.Close()
require.Error(t, err)
assert.ErrorIs(t, err, errClose)
}
func TestKafkaClient_getNewReader(t *testing.T) {
k := &kafkaClient{
dialer: &kafka.Dialer{},
config: Config{
Brokers: []string{"kafka-broker"},
ConsumerGroupID: "consumer",
OffSet: -1,
},
}
reader := k.getNewReader("test")
assert.NotNil(t, reader)
}
func TestNewKafkaClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
testCases := []struct {
desc string
config Config
expectNil bool
}{
{
desc: "validation of configs fail (Empty Broker)",
config: Config{
Brokers: []string{""},
},
expectNil: true,
},
{
desc: "validation of configs fail (Zero Batch Bytes)",
config: Config{
Brokers: []string{"kafka-broker"},
BatchBytes: 0,
},
expectNil: true,
},
{
desc: "validation of configs fail (Zero Batch Size)",
config: Config{
Brokers: []string{"kafka-broker"},
BatchBytes: 1,
BatchSize: 0,
},
expectNil: true,
},
{
desc: "validation of configs fail (Zero Batch Timeout)",
config: Config{
Brokers: []string{"kafka-broker"},
BatchBytes: 1,
BatchSize: 1,
BatchTimeout: 0,
},
expectNil: true,
},
{
desc: "successful initialization",
config: Config{
Brokers: []string{"kafka-broker"},
ConsumerGroupID: "consumer",
BatchBytes: 1,
BatchSize: 1,
BatchTimeout: 1,
SecurityProtocol: "SASL_PLAINTEXT",
SASLMechanism: "PLAIN",
SASLUser: "user",
SASLPassword: "password",
},
expectNil: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
k := New(&tc.config, logging.NewMockLogger(logging.ERROR), NewMockMetrics(ctrl))
if tc.expectNil {
assert.Nil(t, k)
} else {
assert.NotNil(t, k)
}
})
}
}
func TestKafkaClient_Controller(t *testing.T) {
ctrl := gomock.NewController(t)
mockClient := NewMockConnection(ctrl)
client := kafkaClient{
conn: &multiConn{
conns: []Connection{
mockClient,
},
},
}
mockClient.EXPECT().Controller().Return(kafka.Broker{}, nil)
broker, err := client.Controller()
assert.NotNil(t, broker)
require.NoError(t, err)
}
func TestKafkaClient_DeleteTopic(t *testing.T) {
ctrl := gomock.NewController(t)
mockClient := NewMockConnection(ctrl)
client := kafkaClient{
conn: &multiConn{
conns: []Connection{
mockClient,
},
dialer: &kafka.Dialer{}, // Needed if fallback dialing is triggered
},
}
mockClient.EXPECT().Controller().Return(kafka.Broker{
Host: "localhost",
Port: 9092,
}, nil).AnyTimes()
mockClient.EXPECT().RemoteAddr().Return(&net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 9092,
}).AnyTimes()
mockClient.EXPECT().DeleteTopics("test").Return(nil)
err := client.DeleteTopic(t.Context(), "test")
require.NoError(t, err)
}
func TestKafkaClient_CreateTopic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnection(ctrl)
// IP: 127.0.0.1 Port: 9092 -> controller's resolved address
controllerHost := "localhost"
controllerPort := 9092
client := kafkaClient{
conn: &multiConn{
conns: []Connection{
mockConn,
},
dialer: &kafka.Dialer{}, // Only used if fallback occurs
},
}
t.Run("successfully creates topic", func(t *testing.T) {
mockConn.EXPECT().Controller().Return(kafka.Broker{
Host: controllerHost,
Port: controllerPort,
}, nil)
// RemoteAddr should return IP resolved version of controller
mockConn.EXPECT().RemoteAddr().Return(&net.TCPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 9092,
})
mockConn.EXPECT().CreateTopics([]kafka.TopicConfig{
{
Topic: "test",
NumPartitions: 1,
ReplicationFactor: 1,
},
}).Return(nil)
err := client.CreateTopic(t.Context(), "test")
require.NoError(t, err)
})
t.Run("controller returns error", func(t *testing.T) {
mockConn.EXPECT().Controller().Return(kafka.Broker{}, errNoActiveConnections)
err := client.CreateTopic(t.Context(), "test")
require.EqualError(t, err, errNoActiveConnections.Error())
})
}
func TestKafkaClient_Subscribe_NotConnected(t *testing.T) {
var (
msg *pubsub.Message
err error
)
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctx := t.Context()
mockConnection := NewMockConnection(ctrl)
k := &kafkaClient{
dialer: &kafka.Dialer{},
conn: &multiConn{
conns: []Connection{
mockConnection,
},
},
logger: logging.NewMockLogger(logging.DEBUG),
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, errClientNotConnected)
msg, err = k.Subscribe(ctx, "test")
require.Error(t, err)
assert.Nil(t, msg)
assert.Equal(t, errClientNotConnected, err)
}
func TestKafkaClient_Query_Failures(t *testing.T) {
testCases := []struct {
name string
setupClient func() *kafkaClient
topic string
args []any
expectedErr string
}{
{
name: "Client not connected",
setupClient: func() *kafkaClient {
ctrl := gomock.NewController(t)
mockConnection := NewMockConnection(ctrl)
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, errClientNotConnected)
return &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConnection},
},
}
},
topic: "test-topic",
expectedErr: errClientNotConnected.Error(),
},
{
name: "Empty topic name",
setupClient: func() *kafkaClient {
ctrl := gomock.NewController(t)
mockConnection := NewMockConnection(ctrl)
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
return &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConnection},
},
}
},
topic: "",
expectedErr: "topic name cannot be empty",
},
{
name: "ReadMessage fails with non-EOF error",
setupClient: func() *kafkaClient {
ctrl := gomock.NewController(t)
mockConnection := NewMockConnection(ctrl)
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
return &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConnection},
},
config: Config{
Brokers: []string{"localhost:9092"},
Partition: 0,
},
}
},
topic: "test-topic",
expectedErr: "failed to read message",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := tc.setupClient()
result, err := client.Query(t.Context(), tc.topic, tc.args...)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErr)
assert.Nil(t, result)
})
}
}
func TestKafkaClient_Query_ArgumentParsing(t *testing.T) {
testCases := []struct {
name string
args []any
expectedOffset int64
expectedLimit int
}{
{
name: "No arguments - defaults",
args: []any{},
expectedOffset: 0,
expectedLimit: 10,
},
{
name: "Only offset provided",
args: []any{int64(100)},
expectedOffset: 100,
expectedLimit: 10,
},
{
name: "Offset and limit provided",
args: []any{int64(50), 5},
expectedOffset: 50,
expectedLimit: 5,
},
{
name: "Invalid offset type - ignored",
args: []any{"invalid", 5},
expectedOffset: 0,
expectedLimit: 5,
},
{
name: "Invalid limit type - ignored",
args: []any{int64(25), "invalid"},
expectedOffset: 25,
expectedLimit: 10,
},
{
name: "Both invalid types - defaults",
args: []any{"invalid1", "invalid2"},
expectedOffset: 0,
expectedLimit: 10,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnection := NewMockConnection(ctrl)
k := &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConnection},
},
config: Config{
Brokers: []string{"localhost:9092"},
Partition: 0,
},
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
_, err := k.Query(t.Context(), "test-topic", tc.args...)
assert.Error(t, err)
})
}
}
func TestKafkaClient_Query_ContextHandling(t *testing.T) {
testCases := []struct {
name string
setupCtx func(t *testing.T) (context.Context, context.CancelFunc)
description string
}{
{
name: "Context with existing deadline",
setupCtx: func(t *testing.T) (context.Context, context.CancelFunc) {
t.Helper()
return context.WithTimeout(t.Context(), 3*time.Second)
},
description: "Should use existing context deadline",
},
{
name: "Context without deadline",
setupCtx: func(t *testing.T) (context.Context, context.CancelFunc) {
t.Helper()
return context.WithCancel(t.Context())
},
description: "Should add 30 second timeout",
},
{
name: "Canceled context",
setupCtx: func(t *testing.T) (context.Context, context.CancelFunc) {
t.Helper()
ctx, cancel := context.WithCancel(t.Context())
cancel()
return ctx, func() {}
},
description: "Should handle canceled context",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnection := NewMockConnection(ctrl)
k := &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConnection},
},
config: Config{
Brokers: []string{"localhost:9092"},
Partition: 0,
},
}
mockConnection.EXPECT().Controller().Return(kafka.Broker{}, nil)
ctx, cancel := tc.setupCtx(t)
defer cancel()
_, err := k.Query(ctx, "test-topic")
assert.Error(t, err)
})
}
}
func TestKafkaClient_Subscribe_RaceDetector(t *testing.T) {
// This test is specifically designed to trigger race conditions
// Run with: go test -race
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnection(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockConn.EXPECT().Controller().Return(kafka.Broker{}, nil).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
client := &kafkaClient{
conn: &multiConn{
conns: []Connection{mockConn},
},
config: Config{ConsumerGroupID: "test-group", Brokers: []string{"localhost:9092"}},
logger: logging.NewMockLogger(logging.DEBUG),
metrics: mockMetrics,
reader: make(map[string]Reader),
mu: &sync.RWMutex{},
}
// Create mock reader
mockReader := NewMockReader(ctrl)
mockReader.EXPECT().FetchMessage(gomock.Any()).DoAndReturn(
func(_ context.Context) (kafka.Message, error) {
// Add small delay to increase chance of race
time.Sleep(time.Microsecond)
return kafka.Message{
Topic: "race-test-topic",
Value: []byte("test"),
}, nil
},
).AnyTimes()
client.reader["race-test-topic"] = mockReader
ctx := context.Background()
numGoroutines := 100
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Rapid concurrent access
for i := 0; i < numGoroutines; i++ {
go func() {
defer wg.Done()
_, _ = client.Subscribe(ctx, "race-test-topic")
}()
}
wg.Wait()
// If we reach here without race detector complaints, the fix works
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/message.go
================================================
package kafka
import (
"context"
"github.com/segmentio/kafka-go"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
type kafkaMessage struct {
msg *kafka.Message
reader Reader
logger pubsub.Logger
}
func newKafkaMessage(msg *kafka.Message, reader Reader, logger pubsub.Logger) *kafkaMessage {
return &kafkaMessage{
msg: msg,
reader: reader,
logger: logger,
}
}
func (kmsg *kafkaMessage) Commit() {
if kmsg.reader != nil {
err := kmsg.reader.CommitMessages(context.Background(), *kmsg.msg)
if err != nil {
kmsg.logger.Errorf("unable to commit message on kafka")
}
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/message_test.go
================================================
package kafka
import (
"testing"
"github.com/segmentio/kafka-go"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestNewMessage(t *testing.T) {
msg := new(kafka.Message)
reader := new(kafka.Reader)
k := newKafkaMessage(msg, reader, nil)
assert.NotNil(t, k)
assert.Equal(t, msg, k.msg)
assert.Equal(t, reader, k.reader)
}
func TestKafkaMessage_Commit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockReader := NewMockReader(ctrl)
msg := &kafka.Message{Topic: "test", Value: []byte("hello")}
logger := logging.NewMockLogger(logging.ERROR)
k := newKafkaMessage(msg, mockReader, logger)
mockReader.EXPECT().CommitMessages(gomock.Any(), *msg).Return(nil)
k.Commit()
}
func TestKafkaMessage_CommitError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockReader := NewMockReader(ctrl)
out := testutil.StderrOutputForFunc(func() {
msg := &kafka.Message{Topic: "test", Value: []byte("hello")}
logger := logging.NewMockLogger(logging.ERROR)
k := newKafkaMessage(msg, mockReader, logger)
mockReader.EXPECT().CommitMessages(gomock.Any(), *msg).
Return(testutil.CustomError{ErrorMessage: "error"})
k.Commit()
})
assert.Contains(t, out, "unable to commit message on kafka")
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/metrics.go
================================================
package kafka
import "context"
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interfaces.go
//
// Generated by this command:
//
// mockgen -source=interfaces.go -destination=mock_interfaces.go -package=kafka
//
// Package kafka is a generated GoMock package.
package kafka
import (
context "context"
net "net"
reflect "reflect"
kafka "github.com/segmentio/kafka-go"
gomock "go.uber.org/mock/gomock"
)
// MockReader is a mock of Reader interface.
type MockReader struct {
ctrl *gomock.Controller
recorder *MockReaderMockRecorder
isgomock struct{}
}
// MockReaderMockRecorder is the mock recorder for MockReader.
type MockReaderMockRecorder struct {
mock *MockReader
}
// NewMockReader creates a new mock instance.
func NewMockReader(ctrl *gomock.Controller) *MockReader {
mock := &MockReader{ctrl: ctrl}
mock.recorder = &MockReaderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockReader) EXPECT() *MockReaderMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockReader) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockReaderMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockReader)(nil).Close))
}
// CommitMessages mocks base method.
func (m *MockReader) CommitMessages(ctx context.Context, msgs ...kafka.Message) error {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range msgs {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CommitMessages", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// CommitMessages indicates an expected call of CommitMessages.
func (mr *MockReaderMockRecorder) CommitMessages(ctx any, msgs ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, msgs...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitMessages", reflect.TypeOf((*MockReader)(nil).CommitMessages), varargs...)
}
// FetchMessage mocks base method.
func (m *MockReader) FetchMessage(ctx context.Context) (kafka.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchMessage", ctx)
ret0, _ := ret[0].(kafka.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchMessage indicates an expected call of FetchMessage.
func (mr *MockReaderMockRecorder) FetchMessage(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchMessage", reflect.TypeOf((*MockReader)(nil).FetchMessage), ctx)
}
// ReadMessage mocks base method.
func (m *MockReader) ReadMessage(ctx context.Context) (kafka.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReadMessage", ctx)
ret0, _ := ret[0].(kafka.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadMessage indicates an expected call of ReadMessage.
func (mr *MockReaderMockRecorder) ReadMessage(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadMessage", reflect.TypeOf((*MockReader)(nil).ReadMessage), ctx)
}
// Stats mocks base method.
func (m *MockReader) Stats() kafka.ReaderStats {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stats")
ret0, _ := ret[0].(kafka.ReaderStats)
return ret0
}
// Stats indicates an expected call of Stats.
func (mr *MockReaderMockRecorder) Stats() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockReader)(nil).Stats))
}
// MockWriter is a mock of Writer interface.
type MockWriter struct {
ctrl *gomock.Controller
recorder *MockWriterMockRecorder
isgomock struct{}
}
// MockWriterMockRecorder is the mock recorder for MockWriter.
type MockWriterMockRecorder struct {
mock *MockWriter
}
// NewMockWriter creates a new mock instance.
func NewMockWriter(ctrl *gomock.Controller) *MockWriter {
mock := &MockWriter{ctrl: ctrl}
mock.recorder = &MockWriterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockWriter) EXPECT() *MockWriterMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockWriter) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockWriterMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockWriter)(nil).Close))
}
// Stats mocks base method.
func (m *MockWriter) Stats() kafka.WriterStats {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stats")
ret0, _ := ret[0].(kafka.WriterStats)
return ret0
}
// Stats indicates an expected call of Stats.
func (mr *MockWriterMockRecorder) Stats() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stats", reflect.TypeOf((*MockWriter)(nil).Stats))
}
// WriteMessages mocks base method.
func (m *MockWriter) WriteMessages(ctx context.Context, msg ...kafka.Message) error {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range msg {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "WriteMessages", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// WriteMessages indicates an expected call of WriteMessages.
func (mr *MockWriterMockRecorder) WriteMessages(ctx any, msg ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, msg...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteMessages", reflect.TypeOf((*MockWriter)(nil).WriteMessages), varargs...)
}
// MockConnection is a mock of Connection interface.
type MockConnection struct {
ctrl *gomock.Controller
recorder *MockConnectionMockRecorder
isgomock struct{}
}
// MockConnectionMockRecorder is the mock recorder for MockConnection.
type MockConnectionMockRecorder struct {
mock *MockConnection
}
// NewMockConnection creates a new mock instance.
func NewMockConnection(ctrl *gomock.Controller) *MockConnection {
mock := &MockConnection{ctrl: ctrl}
mock.recorder = &MockConnectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConnection) EXPECT() *MockConnectionMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockConnection) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockConnectionMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConnection)(nil).Close))
}
// Controller mocks base method.
func (m *MockConnection) Controller() (kafka.Broker, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Controller")
ret0, _ := ret[0].(kafka.Broker)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Controller indicates an expected call of Controller.
func (mr *MockConnectionMockRecorder) Controller() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Controller", reflect.TypeOf((*MockConnection)(nil).Controller))
}
// CreateTopics mocks base method.
func (m *MockConnection) CreateTopics(topics ...kafka.TopicConfig) error {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range topics {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "CreateTopics", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// CreateTopics indicates an expected call of CreateTopics.
func (mr *MockConnectionMockRecorder) CreateTopics(topics ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTopics", reflect.TypeOf((*MockConnection)(nil).CreateTopics), topics...)
}
// DeleteTopics mocks base method.
func (m *MockConnection) DeleteTopics(topics ...string) error {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range topics {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "DeleteTopics", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteTopics indicates an expected call of DeleteTopics.
func (mr *MockConnectionMockRecorder) DeleteTopics(topics ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTopics", reflect.TypeOf((*MockConnection)(nil).DeleteTopics), topics...)
}
// ReadPartitions mocks base method.
func (m *MockConnection) ReadPartitions(topics ...string) ([]kafka.Partition, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range topics {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ReadPartitions", varargs...)
ret0, _ := ret[0].([]kafka.Partition)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ReadPartitions indicates an expected call of ReadPartitions.
func (mr *MockConnectionMockRecorder) ReadPartitions(topics ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadPartitions", reflect.TypeOf((*MockConnection)(nil).ReadPartitions), topics...)
}
// RemoteAddr mocks base method.
func (m *MockConnection) RemoteAddr() net.Addr {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoteAddr")
ret0, _ := ret[0].(net.Addr)
return ret0
}
// RemoteAddr indicates an expected call of RemoteAddr.
func (mr *MockConnectionMockRecorder) RemoteAddr() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockConnection)(nil).RemoteAddr))
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=kafka
//
// Package kafka is a generated GoMock package.
package kafka
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/tls.go
================================================
package kafka
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"
)
type TLSConfig struct {
CertFile string
KeyFile string
CACertFile string
InsecureSkipVerify bool
}
func createTLSConfig(tlsConf *TLSConfig) (*tls.Config, error) {
tlsConfig := &tls.Config{
InsecureSkipVerify: tlsConf.InsecureSkipVerify, //nolint:gosec //Populate the value as per user input
}
if tlsConf.CACertFile != "" {
caCert, err := os.ReadFile(tlsConf.CACertFile)
if err != nil {
return nil, fmt.Errorf("%w: %w", errCACertFileRead, err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tlsConfig.RootCAs = caCertPool
}
if tlsConf.CertFile != "" && tlsConf.KeyFile != "" {
cert, err := tls.LoadX509KeyPair(tlsConf.CertFile, tlsConf.KeyFile)
if err != nil {
return nil, fmt.Errorf("%w: %w", errClientCertLoad, err)
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
return tlsConfig, nil
}
func validateTLSConfigs(conf *Config) error {
protocol := strings.ToUpper(conf.SecurityProtocol)
if protocol == protocolSSL || protocol == protocolSASLSSL {
if conf.TLS.CACertFile == "" && !conf.TLS.InsecureSkipVerify && conf.TLS.CertFile == "" {
return fmt.Errorf("for %s, provide either CA cert, client certs, or enable insecure mode: %w",
protocol, errUnsupportedSecurityProtocol)
}
}
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/tls_test.go
================================================
package kafka
import (
"os"
"path/filepath"
"testing"
)
func createTempFile(content string) (string, error) {
tmpFile, err := os.CreateTemp("", "test_cert_*.pem")
if err != nil {
return "", err
}
defer tmpFile.Close()
if _, err := tmpFile.WriteString(content); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
func Test_CreateTLSConfig_Success(t *testing.T) {
caFile, clientCertFile, clientKeyFile := createTestFiles(t)
defer os.Remove(caFile)
defer os.Remove(clientCertFile)
defer os.Remove(clientKeyFile)
tests := []struct {
name string
tlsConf TLSConfig
wantCert bool
}{
{"Only CA Cert", TLSConfig{CACertFile: caFile}, false},
{"CA Cert + Client Cert/Key", TLSConfig{CACertFile: caFile, CertFile: clientCertFile, KeyFile: clientKeyFile}, true},
{"Only Client Cert/Key", TLSConfig{CertFile: clientCertFile, KeyFile: clientKeyFile}, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
validateTLSConfig(t, &tt.tlsConf, tt.wantCert)
})
}
}
func createTestFiles(t *testing.T) (caFile, clientCertFile, clientKeyFile string) {
t.Helper()
// Valid CA Certificate
caCertContent := `-----BEGIN CERTIFICATE-----
MIICojCCAYoCCQDUMM9AFXkWaDANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhL
YWZrYS1DQTAeFw0yNTAzMTcwODU5NDFaFw0yNjAzMTcwODU5NDFaMBMxETAPBgNV
BAMMCEthZmthLUNBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzyxH
lWoFiwBnB7bJjS8AL6liIpWLuxxNugPYWUn/uodwKF4wM1lXlY70D85cPewOtYm7
gw/NsBYgz10jtla/7IFHkOoCTx5L9NkAI79i2jl12cG3oCKCgGocYafGQZBODuEI
UjnXOlvkYbaqSj+FE8wyAAxMqROLN48Iw8W0UkwfAIKMEbf75o1/828wysMbPxzh
nD2G/g/sF8zmi++kPjYkeKMhOVIl+EWf6nrpzO9GoTwKyIntOu4xPZM1A0icCgL2
3JNbMiuR4tLFvL1BHzwknF/8nucWu6g4S9XS/Ql57mo7FS1LtNvksPLwEKw2Ll/u
QV7pnVgBgszH+z5ubwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA4RqW6N5BO4gkS
j7qRiVEx8NoYx9kMvfmuN7ldjqA00gb8DI4tAIUZ+G8exfIVLLJL86L2Gis8y7yC
W9RCuQnuWc5Co+wTshGEBOn7WA1LIWGYnZDnxrhZ2Hv5HQVw48OCp0R1/YCD7Xt2
OLXk3tA2sG5rVmjOOhN+wYefwTioedmnZbiIn50MBTHQ0cad0Rfcl4iEDV0o1idi
HQ7R+FdTHWSfjfrYmbBDB6ALNwWXgEMdHTv3SShJgxcdbtfgLJwtIFjIzg9MRRx+
izrooAIzjs9GHmZVaoQsbyG27OPH6v4SqMwvPQttfmTKFY9aNGlmr7hfVUXdnGUd
+FR0EKOA
-----END CERTIFICATE-----`
// Valid Client Certificate and Key
clientCertContent := `-----BEGIN CERTIFICATE-----
MIICoDCCAYgCCQD04XTuhKncLDANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDDAhL
YWZrYS1DQTAeFw0yNTAzMTcwOTAwMjZaFw0yNjAzMTcwOTAwMjZaMBExDzANBgNV
BAMMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALAFJrUQ
qboZ5WFTkCbrUFw3NnbshVZZecYNzxEUXc94k4ZA58PrEkVmt78VBJyrFlRxTWCu
NT98VFdNZLR58yd4R48Q5wdrfBnkhw2byc1n19cgW+KOkyEGDhkycwc48GxTe6BB
EP/ntTPfBevtuLVYrJIiFyWiy+opBDv+by+A9XsASkbWxui99/HKWDswuiAwDsp1
q589lGIL3c4Uj9L6sGxA8ekMJAFM/Rq1SMxcV172E68HifR6/nJcfZU3/JKnVjN1
WX1lOrc9ENiY8pzoWU63ZONv6KSWXPxB1sW/HR1QFpFAiuwDh2xFcZ2zghMEuxmK
rlZT7tDuw6DOyL8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAgm+2Gr2LD7eL7UGH
R8lwaKxyA0gvRA0II9T3s4ReaCzXmDCgQl9YFs2QyyxuAPRhzVIcrQCo9IsN6+eN
vkoUsePb9m7onMNoKzJ63EhbsDnkj3eirG33lLaB5cG/TJm/8z+cm80HJ0viWtea
/vgG7Gzdbkkr5Swl4F9IHI/DpztMf2u05U68DhBxzs2eI/qY5majMfajzD13jAwe
u1D4NuBnhNYxoMReK62alDiFIIfFy1+/pxux+mQqKo18VpMKo0YwXxrfGOi9+5AD
IFi4oUxW0wLNUmTJFSIrFRE3eYWy56XiI8jPs7U94It8YwjhDSeHwslMKbGwogqI
Om59HA==
-----END CERTIFICATE-----`
//nolint:gosec // This is a test certificate
clientKeyContent := `-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEAsAUmtRCpuhnlYVOQJutQXDc2duyFVll5xg3PERRdz3iThkDn
w+sSRWa3vxUEnKsWVHFNYK41P3xUV01ktHnzJ3hHjxDnB2t8GeSHDZvJzWfX1yBb
4o6TIQYOGTJzBzjwbFN7oEEQ/+e1M98F6+24tViskiIXJaLL6ikEO/5vL4D1ewBK
RtbG6L338cpYOzC6IDAOynWrnz2UYgvdzhSP0vqwbEDx6QwkAUz9GrVIzFxXXvYT
rweJ9Hr+clx9lTf8kqdWM3VZfWU6tz0Q2JjynOhZTrdk42/opJZc/EHWxb8dHVAW
kUCK7AOHbEVxnbOCEwS7GYquVlPu0O7DoM7IvwIDAQABAoIBADtM0PiJL5UZ6lQ6
scLa3gzjMP8pudYYeNUHi+4mHWCrL5A4R5ySkmo9K8Q9UXtyjChQr4/VwOytd0Ce
O0IuH4P5mqoROLQgOwQCIJmuFXOU+3tnVG1kOR8UCiXlACm7vgvQqEKaCR8dscdS
6IzOXr8Bq8njoEa2rNorjVik5FJtIbbBEJJI9nISG7901pXk+kvEXujX4Tt1xqsF
W5L6Q+2rM2gCedQr+11aml265KdJq8sYGFf8/PNm9AG5V7xRKJWyFzohGuuMimRM
WSb0uEIdkfNfaxfYc/2g5u8g9mPyzAdAjsd9tl3nxdHdCou1B6XIeDRYJupZ7z18
JZAcMmECgYEA2LlmNlQX9OWoc8+lww/43LgU6KVpIBUtxFSydqSdYJVFk33tKtyq
mD1IlSLJ9X9kQ9QWVKxtKGudY+AnEsEMua8nm3ypO7LSMDdKrrnBs+H1wbPyixnW
AsB66v3Vmn6mMf1D1PE8GY/JlprZgllq2fJ/D+jVx3BCjYYXP9VdV28CgYEAz+tY
b7dosqXeG2c6pZ/1gYtqde/prclDuIdfMYVYE7RO9mwknK9wnOYYMKJwnSsnsefv
QTrGkbfBflGKcpzf/Ux+MyJkfmQYRtczPS0XmtovkW/UUwTYCTsXk+V+zd+uiX87
2jMURXGg6qpcT9Gm0zQgoeS45iyK2eVWTXhte7ECgYAPyKjmCgfYoSU8kgHri+0+
/fUf4HQgjwpPQy/gLir8DsMLc99jAME35zazDd6Rj56Yxgh+UDR+/h9vV7LgzciE
eXoz+8dDfsmKE2zP/t1ZoXpJijZ+5PnOJ4CMPsJgxxqJh316M7uBzRQMcOiocqSy
jNOuL/Hp3YYrUnm8/2gV5wKBgDhann6xJHR/VoLw6MlpYJ57DiDnJNwQmAVU061V
afj1Pw21Y/r/5jLwfo/4BzPiNYEXzxZL+vQV7SDysua7tE4wRGhRoxFKyfWxcFbd
eO9kwc3WlKLnxjJCTPKuGj9sqB7mWG+ctprX4HiaMikENwY5s7qNhrwESKIkcc7P
nEURAoGARj9QGcxbs5jBAxqRCHp+hzDEArIKqe2aDawAsx09Zv6kd97HiU/DFnJy
s18fZGQi04zDLMx72bYHG/9SdtcKLdwKgQcpHLLwDvAtXXyRE7YTvy6ziChf25lX
Q1k1WBr5rFlCp0GK2DbAkuCrLj0GghVAFYhxN19XRT/Dax1vgFo=
-----END RSA PRIVATE KEY-----`
var err error
// Create temporary files
caFile, err = createTempFile(caCertContent)
if err != nil {
t.Fatalf("Failed to create temp CA cert file: %v", err)
}
clientCertFile, err = createTempFile(clientCertContent)
if err != nil {
t.Fatalf("Failed to create temp client cert file: %v", err)
}
clientKeyFile, err = createTempFile(clientKeyContent)
if err != nil {
t.Fatalf("Failed to create temp client key file: %v", err)
}
return caFile, clientCertFile, clientKeyFile
}
func validateTLSConfig(t *testing.T, tlsConf *TLSConfig, wantCert bool) {
t.Helper()
cfg, err := createTLSConfig(tlsConf)
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if wantCert && len(cfg.Certificates) == 0 {
t.Errorf("Expected TLS certificate, but got none")
}
if !wantCert && len(cfg.Certificates) > 0 {
t.Errorf("Expected no TLS certificate, but found one")
}
}
func TestCreateTLSConfig_Errors(t *testing.T) {
invalidFile := filepath.Join(os.TempDir(), "nonexistent_file.pem")
tests := []struct {
name string
tlsConf TLSConfig
}{
{"Invalid CA Cert File", TLSConfig{CACertFile: invalidFile}},
{"Invalid Client Cert File", TLSConfig{CertFile: invalidFile, KeyFile: invalidFile}},
{"Invalid Client Key File", TLSConfig{CertFile: invalidFile, KeyFile: invalidFile}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := createTLSConfig(&tt.tlsConf)
if err == nil {
t.Errorf("Expected an error but got none")
}
})
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/tracing.go
================================================
package kafka
import (
"context"
"github.com/segmentio/kafka-go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const tracerName = "gofr-kafka"
// headerCarrier implements propagation.TextMapCarrier for Kafka headers.
type headerCarrier []kafka.Header
// Get returns the value for a given key from the Kafka headers.
func (c headerCarrier) Get(key string) string {
for _, h := range c {
if h.Key == key {
return string(h.Value)
}
}
return ""
}
// Set sets a key-value pair in the Kafka headers.
func (c *headerCarrier) Set(key, value string) {
// Check if key exists and update it
for i, h := range *c {
if h.Key == key {
(*c)[i].Value = []byte(value)
return
}
}
// Key doesn't exist, append new header
*c = append(*c, kafka.Header{Key: key, Value: []byte(value)})
}
// Keys returns all keys in the Kafka headers.
func (c headerCarrier) Keys() []string {
keys := make([]string, 0, len(c))
for _, h := range c {
keys = append(keys, h.Key)
}
return keys
}
// injectTraceContext injects the current trace context into Kafka message headers.
// This allows the consumer to extract the trace context and create span links.
func injectTraceContext(ctx context.Context, headers []kafka.Header) []kafka.Header {
carrier := headerCarrier(headers)
otel.GetTextMapPropagator().Inject(ctx, &carrier)
return carrier
}
// extractTraceLinks extracts the trace context from Kafka message headers
// and returns span links to the producer span.
// If no trace context is found, returns empty links (creating an orphan span).
func extractTraceLinks(headers []kafka.Header) []trace.Link {
carrier := headerCarrier(headers)
// Extract the context from headers
extractedCtx := otel.GetTextMapPropagator().Extract(context.Background(), &carrier)
// Get span context from extracted context
spanCtx := trace.SpanContextFromContext(extractedCtx)
// If valid span context exists, create a link to it
if spanCtx.IsValid() {
return []trace.Link{
{
SpanContext: spanCtx,
},
}
}
// No valid trace context found, return empty links (orphan span)
return nil
}
// startPublishSpan creates a new span for publishing with trace context injection.
// Returns the updated context for logging and the headers with injected trace context.
func startPublishSpan(ctx context.Context, topic string) (context.Context, trace.Span, []kafka.Header) {
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindProducer),
trace.WithAttributes(
attribute.String("messaging.system", "kafka"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "publish"),
),
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "kafka-publish", opts...)
// Inject trace context into headers
headers := injectTraceContext(ctx, nil)
return ctx, span, headers
}
// startSubscribeSpan creates a new span for subscribing with links to the producer span.
// If trace context exists in headers, creates a span linked to the producer.
// Otherwise, creates an orphan span (new trace).
func startSubscribeSpan(ctx context.Context, topic string, msgHeaders []kafka.Header) (context.Context, trace.Span) {
// Extract links from message headers
links := extractTraceLinks(msgHeaders)
// Create span with links if any exist
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("messaging.system", "kafka"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "receive"),
),
}
if len(links) > 0 {
opts = append(opts, trace.WithLinks(links...))
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "kafka-subscribe", opts...)
return ctx, span
}
================================================
FILE: pkg/gofr/datasource/pubsub/kafka/tracing_test.go
================================================
package kafka
import (
"context"
"testing"
"github.com/segmentio/kafka-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
func TestHeaderCarrier_GetSetKeys(t *testing.T) {
carrier := headerCarrier{}
// Test Set
carrier.Set("traceparent", "00-1234567890abcdef-fedcba0987654321-01")
carrier.Set("tracestate", "foo=bar")
// Test Get
assert.Equal(t, "00-1234567890abcdef-fedcba0987654321-01", carrier.Get("traceparent"))
assert.Equal(t, "foo=bar", carrier.Get("tracestate"))
assert.Empty(t, carrier.Get("nonexistent"))
// Test Keys
keys := carrier.Keys()
assert.Contains(t, keys, "traceparent")
assert.Contains(t, keys, "tracestate")
// Test Set updates existing key
carrier.Set("traceparent", "00-updated-value")
assert.Equal(t, "00-updated-value", carrier.Get("traceparent"))
}
func TestInjectTraceContext(t *testing.T) {
// Setup tracer with W3C propagator
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Create a span
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
// Inject trace context into headers
headers := injectTraceContext(ctx, nil)
// Verify traceparent header was injected
var traceparent string
for _, h := range headers {
if h.Key == "traceparent" {
traceparent = string(h.Value)
break
}
}
require.NotEmpty(t, traceparent, "traceparent header should be injected")
assert.Contains(t, traceparent, span.SpanContext().TraceID().String())
}
func TestExtractTraceLinks(t *testing.T) {
// Setup tracer with W3C propagator
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Create a producer span and inject context
ctx, producerSpan := tp.Tracer("test").Start(context.Background(), "producer-span")
headers := injectTraceContext(ctx, nil)
producerSpan.End()
// Extract links from headers
links := extractTraceLinks(headers)
// Verify link to producer span
require.Len(t, links, 1, "should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), links[0].SpanContext.SpanID())
}
func TestExtractTraceLinks_NoHeaders(t *testing.T) {
// Setup tracer
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Extract links from empty headers
links := extractTraceLinks(nil)
// Should return nil (orphan span)
assert.Nil(t, links, "should return nil for empty headers")
}
func TestStartPublishSpan(t *testing.T) {
// Setup tracer
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Start publish span
ctx, span, headers := startPublishSpan(context.Background(), "test-topic")
defer span.End()
// Verify span created
require.NotNil(t, span)
assert.True(t, span.SpanContext().IsValid())
// Verify context updated
require.NotNil(t, ctx)
// Verify headers contain trace context
var hasTraceparent bool
for _, h := range headers {
if h.Key == "traceparent" {
hasTraceparent = true
break
}
}
assert.True(t, hasTraceparent, "headers should contain traceparent")
}
func TestStartSubscribeSpan_WithLinks(t *testing.T) {
// Setup tracer
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Create producer span and get headers
_, producerSpan, headers := startPublishSpan(context.Background(), "test-topic")
producerSpan.End()
// Start subscribe span with headers
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-topic", headers)
subscribeSpan.End()
// Get recorded spans
spans := exporter.GetSpans()
require.GreaterOrEqual(t, len(spans), 2)
// Find subscribe span and verify links
var subSpan *tracetest.SpanStub
for i := range spans {
if spans[i].Name == "kafka-subscribe" {
subSpan = &spans[i]
break
}
}
require.NotNil(t, subSpan, "subscribe span should exist")
require.Len(t, subSpan.Links, 1, "subscribe span should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), subSpan.Links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), subSpan.Links[0].SpanContext.SpanID())
}
func TestStartSubscribeSpan_NoLinks(t *testing.T) {
// Setup tracer
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Start subscribe span without headers (orphan span)
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-topic", nil)
subscribeSpan.End()
// Get recorded spans
spans := exporter.GetSpans()
require.Len(t, spans, 1)
// Verify no links
assert.Empty(t, spans[0].Links, "orphan span should have no links")
}
func TestHeaderCarrier_ConvertFromKafkaHeaders(t *testing.T) {
// Test conversion from actual kafka.Header slice
kafkaHeaders := []kafka.Header{
{Key: "traceparent", Value: []byte("00-trace-span-01")},
{Key: "custom-header", Value: []byte("custom-value")},
}
carrier := headerCarrier(kafkaHeaders)
assert.Equal(t, "00-trace-span-01", carrier.Get("traceparent"))
assert.Equal(t, "custom-value", carrier.Get("custom-header"))
}
================================================
FILE: pkg/gofr/datasource/pubsub/log.go
================================================
package pubsub
import (
"fmt"
"io"
)
type Log struct {
Mode string `json:"mode"`
CorrelationID string `json:"correlationID"`
MessageValue string `json:"messageValue"`
Topic string `json:"topic"`
Host string `json:"host"`
PubSubBackend string `json:"pubSubBackend"`
Time int64 `json:"time"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %-4s %s \u001b[38;5;101m%s\u001b[0m\n",
l.CorrelationID, l.PubSubBackend, l.Time, l.Mode, l.Topic, l.MessageValue)
}
================================================
FILE: pkg/gofr/datasource/pubsub/message.go
================================================
package pubsub
import (
"context"
"encoding/json"
"errors"
"reflect"
"strconv"
)
var errNotPointer = errors.New("input should be a pointer to a variable")
type Message struct {
ctx context.Context
Topic string
Value []byte
MetaData any
Committer
}
func NewMessage(ctx context.Context) *Message {
if ctx == nil {
return &Message{ctx: context.Background()}
}
return &Message{ctx: ctx}
}
func (m *Message) Context() context.Context {
return m.ctx
}
func (m *Message) Param(p string) string {
if p == "topic" {
return m.Topic
}
return ""
}
func (m *Message) PathParam(p string) string {
return m.Param(p)
}
// Bind binds the message value to the input variable. The input should be a pointer to a variable.
func (m *Message) Bind(i any) error {
if reflect.ValueOf(i).Kind() != reflect.Ptr {
return errNotPointer
}
switch v := i.(type) {
case *string:
return m.bindString(v)
case *float64:
return m.bindFloat64(v)
case *int:
return m.bindInt(v)
case *bool:
return m.bindBool(v)
default:
return m.bindStruct(i)
}
}
func (m *Message) bindString(v *string) error {
*v = string(m.Value)
return nil
}
func (m *Message) bindFloat64(v *float64) error {
f, err := strconv.ParseFloat(string(m.Value), 64)
if err != nil {
return err
}
*v = f
return nil
}
func (m *Message) bindInt(v *int) error {
in, err := strconv.Atoi(string(m.Value))
if err != nil {
return err
}
*v = in
return nil
}
func (m *Message) bindBool(v *bool) error {
b, err := strconv.ParseBool(string(m.Value))
if err != nil {
return err
}
*v = b
return nil
}
func (m *Message) bindStruct(i any) error {
return json.Unmarshal(m.Value, i)
}
func (*Message) HostName() string {
return ""
}
func (*Message) Params(string) []string {
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/message_test.go
================================================
package pubsub
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMessage_Context(t *testing.T) {
ctx := t.Context()
m := NewMessage(ctx)
out := m.Context()
assert.Equal(t, ctx, out)
}
func TestMessage_Bind(t *testing.T) {
testCases := []struct {
desc string
input any
value []byte
expected any
hasError bool
}{
{
desc: "bind to string",
input: new(string),
value: []byte("test"),
expected: "test",
hasError: false,
},
{
desc: "bind to float64",
input: new(float64),
value: []byte("1.23"),
expected: 1.23,
hasError: false,
},
{
desc: "bind to int",
input: new(int),
value: []byte("123"),
expected: 123,
hasError: false,
},
{
desc: "bind to bool",
input: new(bool),
value: []byte("true"),
expected: true,
hasError: false,
},
{
desc: "bind to map[string]any",
input: &map[string]any{},
value: []byte(`{"key":"value"}`),
expected: &map[string]any{"key": "value"},
hasError: false,
},
{
desc: "bind to struct",
input: &struct{ Name string }{},
value: []byte(`{"Name":"test"}`),
expected: &struct{ Name string }{Name: "test"},
hasError: false,
},
{
desc: "bind to not pointer",
input: struct{ Name string }{},
value: []byte(`{"Name":"test"}`),
expected: &struct{ Name string }{},
hasError: true,
},
{
desc: "bind to struct with error",
input: &struct{ Name string }{},
value: []byte(`{"Name":}`),
expected: &struct{ Name string }{},
hasError: true,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
m := NewMessage(t.Context())
m.Value = tc.value
err := m.Bind(tc.input)
if tc.hasError {
require.Error(t, err)
} else {
require.NoError(t, err)
switch v := tc.input.(type) {
case *string:
assert.Equal(t, tc.expected, *v)
case *float64:
assert.InEpsilon(t, tc.expected, *v, 0.01)
case *int:
assert.Equal(t, tc.expected, *v)
case *bool:
assert.Equal(t, tc.expected, *v)
default:
assert.Equal(t, tc.expected, v)
}
}
})
}
}
func TestBindString(t *testing.T) {
m := &Message{Value: []byte("test")}
var s string
err := m.bindString(&s)
require.NoError(t, err)
assert.Equal(t, "test", s)
}
func TestBindFloat64(t *testing.T) {
m := &Message{Value: []byte("1.23")}
var f float64
err := m.bindFloat64(&f)
require.NoError(t, err)
assert.InEpsilon(t, 1.23, f, 0.01)
m = &Message{Value: []byte("not a float")}
var f2 float64
err = m.bindFloat64(&f2)
require.Error(t, err)
}
func TestBindInt(t *testing.T) {
m := &Message{Value: []byte("123")}
var i int
err := m.bindInt(&i)
require.NoError(t, err)
assert.Equal(t, 123, i)
m = &Message{Value: []byte("not an int")}
var i2 int
err = m.bindInt(&i2)
require.Error(t, err)
}
func TestBindBool(t *testing.T) {
m := &Message{Value: []byte("true")}
var b bool
err := m.bindBool(&b)
require.NoError(t, err)
assert.True(t, b)
m = &Message{Value: []byte("not a bool")}
var b2 bool
err = m.bindBool(&b2)
require.Error(t, err)
}
func TestBindStruct(t *testing.T) {
m := &Message{Value: []byte(`{"key":"value"}`)}
var i map[string]any
err := m.bindStruct(&i)
require.NoError(t, err)
assert.Equal(t, map[string]any{"key": "value"}, i)
m = &Message{Value: []byte(`{"key":}`)}
var i2 map[string]any
err = m.bindStruct(&i2)
require.Error(t, err)
}
func TestMessage_Param(t *testing.T) {
testCases := []struct {
desc string
input string
expectedOut string
}{
{desc: "topic is fetched", input: "topic", expectedOut: "test-topic"},
{desc: "any other param is fetched", input: "path", expectedOut: ""},
}
m := NewMessage(t.Context())
m.Topic = "test-topic"
for _, tc := range testCases {
out := m.Param(tc.input)
assert.Equal(t, tc.expectedOut, out)
}
}
func TestMessage_PathParam(t *testing.T) {
testCases := []struct {
desc string
input string
expectedOut string
}{
{desc: "topic is fetched", input: "topic", expectedOut: "test-topic"},
{desc: "other path param is fetched", input: "path", expectedOut: ""},
}
m := NewMessage(t.Context())
m.Topic = "test-topic"
for _, tc := range testCases {
out := m.PathParam(tc.input)
assert.Equal(t, tc.expectedOut, out)
}
}
func TestMessage_HostName(t *testing.T) {
m := &Message{}
out := m.HostName()
assert.Empty(t, out)
}
func TestMessage_QueryParam(t *testing.T) {
m := &Message{}
assert.Nil(t, m.Params("test"))
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/default_client.go
================================================
package mqtt
import (
"fmt"
"math"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/google/uuid"
)
const backoffMultiplier = 2
func getDefaultClient(config *Config, logger Logger, metrics Metrics) *MQTT {
var (
host = publicBroker
port = 1883
clientID = getClientID(config.ClientID)
)
if config.Username == "gofr-mqtt-test" {
host = "broker.hivemq.com"
}
opts := mqtt.NewClientOptions()
opts.AddBroker(fmt.Sprintf("tcp://%s:%d", host, port))
opts.SetClientID(clientID)
opts.SetAutoReconnect(true)
opts.SetKeepAlive(config.KeepAlive)
subscriptions := make(map[string]subscription)
mu := new(sync.RWMutex)
opts.SetOnConnectHandler(createReconnectHandler(mu, config, subscriptions, logger))
opts.SetConnectionLostHandler(createConnectionLostHandler(logger))
opts.SetReconnectingHandler(createReconnectingHandler(logger, config))
client := mqtt.NewClient(opts)
mqttClient := &MQTT{
Client: client,
config: config,
logger: logger,
subscriptions: subscriptions,
mu: mu,
metrics: metrics,
}
if token := client.Connect(); token.Wait() && token.Error() != nil {
logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error())
go retryDefaultConnect(client, config, logger, opts)
return mqttClient
}
config.Hostname = host
config.Port = port
config.ClientID = clientID
msg := make(map[string]subscription)
logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, clientID)
return &MQTT{Client: client, config: config, logger: logger, subscriptions: msg, mu: new(sync.RWMutex), metrics: metrics}
}
func getMQTTClientOptions(config *Config) *mqtt.ClientOptions {
options := mqtt.NewClientOptions()
options.AddBroker(fmt.Sprintf("%s://%s:%d", config.Protocol, config.Hostname, config.Port))
clientID := getClientID(config.ClientID)
options.SetClientID(clientID)
if config.Username != "" {
options.SetUsername(config.Username)
}
if config.Password != "" {
options.SetPassword(config.Password)
}
options.SetOrderMatters(config.Order)
options.SetResumeSubs(config.RetrieveRetained)
options.SetAutoReconnect(true)
options.SetKeepAlive(config.KeepAlive)
return options
}
func getClientID(clientID string) string {
if clientID != "" {
clientID = "-" + clientID
}
id, err := uuid.NewRandom()
if err != nil {
return "gofr-mqtt-default-client-id" + clientID
}
return id.String() + clientID
}
func retryDefaultConnect(client mqtt.Client, config *Config, logger Logger, options *mqtt.ClientOptions) {
backoff := defaultRetryTimeout
for {
token := client.Connect()
if token.Wait() && token.Error() == nil {
logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, options.ClientID)
return
}
logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error())
time.Sleep(backoff)
backoff = time.Duration(math.Min(float64(backoff*backoffMultiplier), float64(maxRetryTimeout)))
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/helper.go
================================================
package mqtt
import (
"bytes"
"context"
"errors"
"fmt"
"math"
"strconv"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// parseQueryArgs extracts collectTimeout and messageLimit from variadic arguments.
// This can be a package-level function as it doesn't depend on *MQTT state.
func parseQueryArgs(args ...any) (collectTimeout time.Duration, messageLimit int) {
collectTimeout = defaultQueryCollectTimeout
messageLimit = defaultQueryMessageLimit
if len(args) > 0 {
if val, ok := args[0].(time.Duration); ok {
collectTimeout = val
}
}
if len(args) > 1 {
if val, ok := args[1].(int); ok {
messageLimit = val
}
}
return collectTimeout, messageLimit
}
// createQueryMessageHandler creates the MQTT message handler for the Query method.
func (m *MQTT) createQueryMessageHandler(ctx context.Context, msgChan chan<- *pubsub.Message, topicForLogging string) mqtt.MessageHandler {
return func(_ mqtt.Client, msg mqtt.Message) {
// Use context.WithoutCancel to ensure the message processing isn't prematurely stopped
// if the handler's parent context (original Query ctx) is canceled while the message is in flight.
messageCtx := context.WithoutCancel(ctx)
message := pubsub.NewMessage(messageCtx)
message.Topic = msg.Topic()
message.Value = msg.Payload()
message.MetaData = map[string]string{
"qos": string(msg.Qos()),
"retained": strconv.FormatBool(msg.Retained()),
"messageID": strconv.Itoa(int(msg.MessageID())),
}
select {
case msgChan <- message:
default:
m.logger.Debugf("Query: msgChan full for topic %s, message dropped during collection", topicForLogging)
}
}
}
// subscribeToTopicForQuery handles the MQTT subscription logic for the Query method.
func (m *MQTT) subscribeToTopicForQuery(ctx context.Context, topicName string, timeout time.Duration, handler mqtt.MessageHandler) error {
token := m.Client.Subscribe(topicName, m.config.QoS, handler)
if !token.WaitTimeout(timeout) {
if ctxErr := ctx.Err(); ctxErr != nil {
return fmt.Errorf("context error during MQTT subscription to '%s': %w", topicName, ctxErr)
}
// If token has an error, it means WaitTimeout likely hit its own timeout AND there was an underlying subscription error.
if tokenErr := token.Error(); tokenErr != nil {
return fmt.Errorf("%w to topic '%s' (timed out with underlying error): %w", errSubscriptionFailed, topicName, tokenErr)
}
// Fallback: WaitTimeout returned false, context is fine, token.Error() is nil. This is the direct timeout.
return fmt.Errorf("%w for topic '%s'", errSubscriptionTimeout, topicName)
}
if tokenErr := token.Error(); tokenErr != nil {
return fmt.Errorf("%w to '%s': %w", errSubscriptionFailed, topicName, tokenErr)
}
return nil
}
// collectMessages handles the message collection loop for the Query method.
func (m *MQTT) collectMessages(queryCtx context.Context, msgChan <-chan *pubsub.Message,
messageLimit int, topicName string) (*bytes.Buffer, int, error) {
var resultBuffer bytes.Buffer
messagesCollected := 0
for {
// Early return if limit reached
if messageLimit > 0 && messagesCollected >= messageLimit {
return &resultBuffer, messagesCollected, nil
}
select {
case msg, ok := <-msgChan:
if !ok {
m.logger.Debugf("Query: msgChan closed unexpectedly while collecting for topic %s", topicName)
return &resultBuffer, messagesCollected, nil
}
m.addMessageToBuffer(&resultBuffer, msg)
messagesCollected++
case <-queryCtx.Done():
return m.handleContextDone(queryCtx, topicName, &resultBuffer, messagesCollected)
}
}
}
func (*MQTT) addMessageToBuffer(buffer *bytes.Buffer, msg *pubsub.Message) {
if buffer.Len() > 0 {
buffer.WriteByte('\n')
}
buffer.Write(msg.Value)
}
func (*MQTT) handleContextDone(queryCtx context.Context, topicName string, buffer *bytes.Buffer,
collected int) (*bytes.Buffer, int, error) {
if !errors.Is(queryCtx.Err(), context.DeadlineExceeded) {
err := fmt.Errorf("%w for topic '%s': %w", errQueryCancelled, topicName, queryCtx.Err())
return buffer, collected, err
}
return buffer, collected, nil
}
func (m *MQTT) createMqttHandler(_ context.Context, topic string, msgs chan *pubsub.Message) mqtt.MessageHandler {
return func(_ mqtt.Client, msg mqtt.Message) {
ctx := context.Background()
ctx, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "mqtt-subscribe")
defer span.End()
m.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic)
var messg = pubsub.NewMessage(context.WithoutCancel(ctx))
messg.Topic = msg.Topic()
messg.Value = msg.Payload()
messg.MetaData = map[string]string{
"qos": string(msg.Qos()),
"retained": strconv.FormatBool(msg.Retained()),
"messageID": strconv.Itoa(int(msg.MessageID())),
}
messg.Committer = &message{msg: msg}
// store the message in the channel
msgs <- messg
m.logger.Debug(&pubsub.Log{
Mode: "SUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(msg.Payload()),
Topic: msg.Topic(),
Host: m.config.Hostname,
PubSubBackend: "MQTT",
})
}
}
func getHandler(subscribeFunc SubscribeFunc) func(client mqtt.Client, msg mqtt.Message) {
return func(_ mqtt.Client, msg mqtt.Message) {
pubsubMsg := &pubsub.Message{
Topic: msg.Topic(),
Value: msg.Payload(),
MetaData: map[string]string{
"qos": string(msg.Qos()),
"retained": strconv.FormatBool(msg.Retained()),
"messageID": strconv.Itoa(int(msg.MessageID())),
},
}
// call the user defined function
_ = subscribeFunc(pubsubMsg)
}
}
func (m *MQTT) Unsubscribe(topic string) error {
m.mu.RLock()
defer m.mu.RUnlock()
token := m.Client.Unsubscribe(topic)
token.Wait()
if token.Error() != nil {
m.logger.Errorf("error while unsubscribing from topic '%s', error: %v", topic, token.Error())
return token.Error()
}
sub, ok := m.subscriptions[topic]
if ok {
close(sub.msgs)
delete(m.subscriptions, topic)
}
return nil
}
func (m *MQTT) Close() error {
timeout := m.config.CloseTimeout
return m.Disconnect(uint(math.Min(float64(timeout.Milliseconds()), float64(math.MaxUint32))))
}
func (m *MQTT) Disconnect(waitTime uint) error {
m.mu.RLock()
defer m.mu.RUnlock()
var err error
for topic := range m.subscriptions {
unsubscribeErr := m.Unsubscribe(topic)
if err != nil {
err = errors.Join(err, unsubscribeErr)
m.logger.Errorf("Error closing Subscription: %v", err)
}
}
m.Client.Disconnect(waitTime)
return err
}
func (m *MQTT) Ping() error {
connected := m.Client.IsConnected()
if !connected {
return errClientNotConnected
}
return nil
}
func retryConnect(client mqtt.Client, config *Config, logger Logger, options *mqtt.ClientOptions) {
for {
token := client.Connect()
if token.Wait() && token.Error() == nil {
logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, options.ClientID)
return
}
logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error())
time.Sleep(defaultRetryTimeout)
}
}
func createReconnectHandler(mu *sync.RWMutex, config *Config, subs map[string]subscription,
logger Logger) mqtt.OnConnectHandler {
return func(client mqtt.Client) {
// Re-subscribe to all topics after reconnecting
mu.RLock()
defer mu.RUnlock()
for topic, sub := range subs {
token := client.Subscribe(topic, config.QoS, sub.handler)
if token.Wait() && token.Error() != nil {
logger.Debugf("failed to resubscribe to topic %s: %v", topic, token.Error())
} else {
logger.Debugf("resubscribed to topic %s successfully", topic)
}
}
}
}
func createConnectionLostHandler(logger Logger) func(_ mqtt.Client, err error) {
return func(_ mqtt.Client, err error) {
logger.Errorf("mqtt connection lost, error: %v", err.Error())
}
}
func createReconnectingHandler(logger Logger, config *Config) func(mqtt.Client, *mqtt.ClientOptions) {
return func(_ mqtt.Client, _ *mqtt.ClientOptions) {
logger.Infof("reconnecting to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, config.ClientID)
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/interface.go
================================================
// Package mqtt provides a client for interacting with MQTT message brokers.This package facilitates interaction with
// MQTT brokers, allowing publishing and subscribing to topics, managing subscriptions, and handling messages.
package mqtt
import (
"context"
"gofr.dev/pkg/gofr/datasource"
)
//go:generate go run go.uber.org/mock/mockgen -destination=mock_client.go -package=mqtt github.com/eclipse/paho.mqtt.golang Client
//go:generate go run go.uber.org/mock/mockgen -destination=mock_token.go -package=mqtt github.com/eclipse/paho.mqtt.golang Token
//go:generate go run go.uber.org/mock/mockgen -source=interface.go -destination=mock_interfaces.go -package=mqtt
type Logger interface {
Infof(format string, args ...any)
Debug(args ...any)
Debugf(format string, args ...any)
Warnf(format string, args ...any)
Errorf(format string, args ...any)
}
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
type PubSub interface {
SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) error
Publish(ctx context.Context, topic string, message []byte) error
Unsubscribe(topic string) error
Disconnect(waitTime uint) error
Ping() error
Health() datasource.Health
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/message.go
================================================
package mqtt
import mqtt "github.com/eclipse/paho.mqtt.golang"
type message struct {
msg mqtt.Message
}
func (m *message) Commit() {
m.msg.Ack()
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/message_test.go
================================================
package mqtt
import (
"testing"
)
func TestMessage(_ *testing.T) {
m := message{msg: mockMessage{}}
m.Commit()
}
type mockMessage struct {
duplicate bool
qos int
retained bool
topic string
messageID int
payload string
}
func (m mockMessage) Duplicate() bool {
return m.duplicate
}
func (m mockMessage) Qos() byte {
return byte(m.qos)
}
func (m mockMessage) Retained() bool {
return m.retained
}
func (m mockMessage) Topic() string {
return m.topic
}
func (m mockMessage) MessageID() uint16 {
if m.messageID < 0 || m.messageID > int(^uint16(0)) {
return 0
}
return uint16(m.messageID)
}
func (m mockMessage) Payload() []byte {
return []byte(m.payload)
}
func (mockMessage) Ack() {
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/mock_client.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/eclipse/paho.mqtt.golang (interfaces: Client)
//
// Generated by this command:
//
// mockgen -destination=mock_client.go -package=mqtt github.com/eclipse/paho.mqtt.golang Client
//
// Package mqtt is a generated GoMock package.
package mqtt
import (
reflect "reflect"
mqtt "github.com/eclipse/paho.mqtt.golang"
gomock "go.uber.org/mock/gomock"
)
// MockClient is a mock of Client interface.
type MockClient struct {
ctrl *gomock.Controller
recorder *MockClientMockRecorder
}
// MockClientMockRecorder is the mock recorder for MockClient.
type MockClientMockRecorder struct {
mock *MockClient
}
// NewMockClient creates a new mock instance.
func NewMockClient(ctrl *gomock.Controller) *MockClient {
mock := &MockClient{ctrl: ctrl}
mock.recorder = &MockClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
// AddRoute mocks base method.
func (m *MockClient) AddRoute(arg0 string, arg1 mqtt.MessageHandler) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddRoute", arg0, arg1)
}
// AddRoute indicates an expected call of AddRoute.
func (mr *MockClientMockRecorder) AddRoute(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRoute", reflect.TypeOf((*MockClient)(nil).AddRoute), arg0, arg1)
}
// Connect mocks base method.
func (m *MockClient) Connect() mqtt.Token {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Connect")
ret0, _ := ret[0].(mqtt.Token)
return ret0
}
// Connect indicates an expected call of Connect.
func (mr *MockClientMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockClient)(nil).Connect))
}
// Disconnect mocks base method.
func (m *MockClient) Disconnect(arg0 uint) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Disconnect", arg0)
}
// Disconnect indicates an expected call of Disconnect.
func (mr *MockClientMockRecorder) Disconnect(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockClient)(nil).Disconnect), arg0)
}
// IsConnected mocks base method.
func (m *MockClient) IsConnected() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "isConnected")
ret0, _ := ret[0].(bool)
return ret0
}
// IsConnected indicates an expected call of IsConnected.
func (mr *MockClientMockRecorder) IsConnected() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isConnected", reflect.TypeOf((*MockClient)(nil).IsConnected))
}
// IsConnectionOpen mocks base method.
func (m *MockClient) IsConnectionOpen() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsConnectionOpen")
ret0, _ := ret[0].(bool)
return ret0
}
// IsConnectionOpen indicates an expected call of IsConnectionOpen.
func (mr *MockClientMockRecorder) IsConnectionOpen() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsConnectionOpen", reflect.TypeOf((*MockClient)(nil).IsConnectionOpen))
}
// OptionsReader mocks base method.
func (m *MockClient) OptionsReader() mqtt.ClientOptionsReader {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OptionsReader")
ret0, _ := ret[0].(mqtt.ClientOptionsReader)
return ret0
}
// OptionsReader indicates an expected call of OptionsReader.
func (mr *MockClientMockRecorder) OptionsReader() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OptionsReader", reflect.TypeOf((*MockClient)(nil).OptionsReader))
}
// Publish mocks base method.
func (m *MockClient) Publish(arg0 string, arg1 byte, arg2 bool, arg3 any) mqtt.Token {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", arg0, arg1, arg2, arg3)
ret0, _ := ret[0].(mqtt.Token)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockClientMockRecorder) Publish(arg0, arg1, arg2, arg3 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockClient)(nil).Publish), arg0, arg1, arg2, arg3)
}
// Subscribe mocks base method.
func (m *MockClient) Subscribe(arg0 string, arg1 byte, arg2 mqtt.MessageHandler) mqtt.Token {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscribe", arg0, arg1, arg2)
ret0, _ := ret[0].(mqtt.Token)
return ret0
}
// Subscribe indicates an expected call of Subscribe.
func (mr *MockClientMockRecorder) Subscribe(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockClient)(nil).Subscribe), arg0, arg1, arg2)
}
// SubscribeMultiple mocks base method.
func (m *MockClient) SubscribeMultiple(arg0 map[string]byte, arg1 mqtt.MessageHandler) mqtt.Token {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SubscribeMultiple", arg0, arg1)
ret0, _ := ret[0].(mqtt.Token)
return ret0
}
// SubscribeMultiple indicates an expected call of SubscribeMultiple.
func (mr *MockClientMockRecorder) SubscribeMultiple(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeMultiple", reflect.TypeOf((*MockClient)(nil).SubscribeMultiple), arg0, arg1)
}
// Unsubscribe mocks base method.
func (m *MockClient) Unsubscribe(arg0 ...string) mqtt.Token {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range arg0 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Unsubscribe", varargs...)
ret0, _ := ret[0].(mqtt.Token)
return ret0
}
// Unsubscribe indicates an expected call of Unsubscribe.
func (mr *MockClientMockRecorder) Unsubscribe(arg0 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockClient)(nil).Unsubscribe), arg0...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/mock_interfaces.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interfaces.go -package=mqtt
//
// Package mqtt is a generated GoMock package.
package mqtt
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
datasource "gofr.dev/pkg/gofr/datasource"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Warnf mocks base method.
func (m *MockLogger) Warnf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Warnf", varargs...)
}
// Warnf indicates an expected call of Warnf.
func (mr *MockLoggerMockRecorder) Warnf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Warnf", reflect.TypeOf((*MockLogger)(nil).Warnf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
// MockPubSub is a mock of PubSub interface.
type MockPubSub struct {
ctrl *gomock.Controller
recorder *MockPubSubMockRecorder
}
// MockPubSubMockRecorder is the mock recorder for MockPubSub.
type MockPubSubMockRecorder struct {
mock *MockPubSub
}
// NewMockPubSub creates a new mock instance.
func NewMockPubSub(ctrl *gomock.Controller) *MockPubSub {
mock := &MockPubSub{ctrl: ctrl}
mock.recorder = &MockPubSubMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPubSub) EXPECT() *MockPubSubMockRecorder {
return m.recorder
}
// Disconnect mocks base method.
func (m *MockPubSub) Disconnect(waitTime uint) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Disconnect", waitTime)
ret0, _ := ret[0].(error)
return ret0
}
// Disconnect indicates an expected call of Disconnect.
func (mr *MockPubSubMockRecorder) Disconnect(waitTime any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockPubSub)(nil).Disconnect), waitTime)
}
// Health mocks base method.
func (m *MockPubSub) Health() datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health")
ret0, _ := ret[0].(datasource.Health)
return ret0
}
// Health indicates an expected call of Health.
func (mr *MockPubSubMockRecorder) Health() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockPubSub)(nil).Health))
}
// Ping mocks base method.
func (m *MockPubSub) Ping() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ping")
ret0, _ := ret[0].(error)
return ret0
}
// Ping indicates an expected call of Ping.
func (mr *MockPubSubMockRecorder) Ping() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ping", reflect.TypeOf((*MockPubSub)(nil).Ping))
}
// Publish mocks base method.
func (m *MockPubSub) Publish(ctx context.Context, topic string, message []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, topic, message)
ret0, _ := ret[0].(error)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockPubSubMockRecorder) Publish(ctx, topic, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockPubSub)(nil).Publish), ctx, topic, message)
}
// SubscribeWithFunction mocks base method.
func (m *MockPubSub) SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SubscribeWithFunction", topic, subscribeFunc)
ret0, _ := ret[0].(error)
return ret0
}
// SubscribeWithFunction indicates an expected call of SubscribeWithFunction.
func (mr *MockPubSubMockRecorder) SubscribeWithFunction(topic, subscribeFunc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeWithFunction", reflect.TypeOf((*MockPubSub)(nil).SubscribeWithFunction), topic, subscribeFunc)
}
// Unsubscribe mocks base method.
func (m *MockPubSub) Unsubscribe(topic string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Unsubscribe", topic)
ret0, _ := ret[0].(error)
return ret0
}
// Unsubscribe indicates an expected call of Unsubscribe.
func (mr *MockPubSubMockRecorder) Unsubscribe(topic any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unsubscribe", reflect.TypeOf((*MockPubSub)(nil).Unsubscribe), topic)
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/mock_token.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/eclipse/paho.mqtt.golang (interfaces: Token)
//
// Generated by this command:
//
// mockgen -destination=mock_token.go -package=mqtt github.com/eclipse/paho.mqtt.golang Token
//
// Package mqtt is a generated GoMock package.
package mqtt
import (
reflect "reflect"
time "time"
gomock "go.uber.org/mock/gomock"
)
// MockToken is a mock of Token interface.
type MockToken struct {
ctrl *gomock.Controller
recorder *MockTokenMockRecorder
}
// MockTokenMockRecorder is the mock recorder for MockToken.
type MockTokenMockRecorder struct {
mock *MockToken
}
// NewMockToken creates a new mock instance.
func NewMockToken(ctrl *gomock.Controller) *MockToken {
mock := &MockToken{ctrl: ctrl}
mock.recorder = &MockTokenMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockToken) EXPECT() *MockTokenMockRecorder {
return m.recorder
}
// Done mocks base method.
func (m *MockToken) Done() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Done")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// Done indicates an expected call of Done.
func (mr *MockTokenMockRecorder) Done() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Done", reflect.TypeOf((*MockToken)(nil).Done))
}
// Error mocks base method.
func (m *MockToken) Error() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Error")
ret0, _ := ret[0].(error)
return ret0
}
// Error indicates an expected call of Error.
func (mr *MockTokenMockRecorder) Error() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockToken)(nil).Error))
}
// Wait mocks base method.
func (m *MockToken) Wait() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Wait")
ret0, _ := ret[0].(bool)
return ret0
}
// Wait indicates an expected call of Wait.
func (mr *MockTokenMockRecorder) Wait() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Wait", reflect.TypeOf((*MockToken)(nil).Wait))
}
// WaitTimeout mocks base method.
func (m *MockToken) WaitTimeout(arg0 time.Duration) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WaitTimeout", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// WaitTimeout indicates an expected call of WaitTimeout.
func (mr *MockTokenMockRecorder) WaitTimeout(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitTimeout", reflect.TypeOf((*MockToken)(nil).WaitTimeout), arg0)
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/mqtt.go
================================================
package mqtt
import (
"context"
"errors"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
const (
publicBroker = "broker.emqx.io"
messageBuffer = 10
defaultRetryTimeout = 10 * time.Second
maxRetryTimeout = 1 * time.Minute
defaultQueryMessageLimit = 10
defaultQueryCollectTimeout = 5 * time.Second
unsubscribeOpTimeout = 2 * time.Second
)
var (
errClientNotConnected = errors.New("mqtt client not connected")
errEmptyTopicName = errors.New("empty topic name")
errSubscriptionTimeout = errors.New("timed out waiting for MQTT subscription")
errSubscriptionFailed = errors.New("failed to subscribe to MQTT topic")
errQueryCancelled = errors.New("query canceled")
)
type SubscribeFunc func(*pubsub.Message) error
// MQTT is the struct that implements PublisherSubscriber interface to
// provide functionality for the MQTT as a pubsub.
type MQTT struct {
// contains filtered or unexported fields
mqtt.Client
logger Logger
metrics Metrics
config *Config
subscriptions map[string]subscription
mu *sync.RWMutex
}
type Config struct {
Protocol string
Hostname string
Port int
Username string
Password string
ClientID string
QoS byte
Order bool
RetrieveRetained bool
KeepAlive time.Duration
CloseTimeout time.Duration
}
type subscription struct {
msgs chan *pubsub.Message
handler func(_ mqtt.Client, msg mqtt.Message)
}
// New establishes a connection to MQTT Broker using the configs and return pubsub.MqttPublisherSubscriber
// with more MQTT focused functionalities related to subscribing(push), unsubscribing and disconnecting from broker.
func New(config *Config, logger Logger, metrics Metrics) *MQTT {
if config.Hostname == "" {
return getDefaultClient(config, logger, metrics)
}
options := getMQTTClientOptions(config)
subs := make(map[string]subscription)
mu := new(sync.RWMutex)
logger.Debugf("connecting to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, config.ClientID)
options.SetOnConnectHandler(createReconnectHandler(mu, config, subs, logger))
options.SetConnectionLostHandler(createConnectionLostHandler(logger))
options.SetReconnectingHandler(createReconnectingHandler(logger, config))
// create the client using the options above
client := mqtt.NewClient(options)
if token := client.Connect(); token.Wait() && token.Error() != nil {
logger.Errorf("could not connect to MQTT at '%v:%v', error: %v", config.Hostname, config.Port, token.Error())
go retryConnect(client, config, logger, options)
} else {
logger.Infof("connected to MQTT at '%v:%v' with clientID '%v'", config.Hostname, config.Port, options.ClientID)
}
return &MQTT{Client: client, config: config, logger: logger, subscriptions: subs, mu: mu, metrics: metrics}
}
func (m *MQTT) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
if !m.Client.IsConnected() {
time.Sleep(defaultRetryTimeout)
return nil, errClientNotConnected
}
m.mu.Lock()
// get the message channel for the given topic
subs, ok := m.subscriptions[topic]
if !ok {
subs.msgs = make(chan *pubsub.Message, messageBuffer)
subs.handler = m.createMqttHandler(ctx, topic, subs.msgs)
token := m.Client.Subscribe(topic, m.config.QoS, subs.handler)
if token.Wait() && token.Error() != nil {
m.mu.Unlock()
m.logger.Errorf("error getting a message from MQTT, error: %v", token.Error())
return nil, token.Error()
}
m.subscriptions[topic] = subs
}
m.mu.Unlock()
select {
// blocks if there are no messages in the channel
case msg := <-subs.msgs:
m.metrics.IncrementCounter(msg.Context(), "app_pubsub_subscribe_success_count", "topic", msg.Topic)
return msg, nil
case <-ctx.Done():
return nil, nil
}
}
// Query retrieves messages from a topic, waiting up to a specified duration and message limit.
func (m *MQTT) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if !m.Client.IsConnected() {
return nil, errClientNotConnected
}
if query == "" {
return nil, errEmptyTopicName
}
collectTimeout, messageLimit := parseQueryArgs(args...)
msgChan := make(chan *pubsub.Message, messageBuffer)
handler := m.createQueryMessageHandler(ctx, msgChan, query)
if err := m.subscribeToTopicForQuery(ctx, query, collectTimeout, handler); err != nil {
return nil, err
}
defer func() {
unsubToken := m.Client.Unsubscribe(query)
if !unsubToken.WaitTimeout(unsubscribeOpTimeout) {
m.logger.Warnf("Query: timed out unsubscribing from topic %s", query)
}
}()
queryCtx, cancel := context.WithTimeout(ctx, collectTimeout)
defer cancel()
resultBuffer, messagesCollected, collectionErr := m.collectMessages(queryCtx, msgChan, messageLimit, query)
if collectionErr != nil {
return nil, collectionErr
}
if resultBuffer.Len() == 0 && messagesCollected == 0 {
m.logger.Debugf("Query: no messages collected for topic %s within timeout/limit", query)
}
return resultBuffer.Bytes(), nil
}
func (m *MQTT) Publish(ctx context.Context, topic string, message []byte) error {
_, span := otel.GetTracerProvider().Tracer("gofr").Start(ctx, "mqtt-publish")
defer span.End()
m.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
s := time.Now()
token := m.Client.Publish(topic, m.config.QoS, m.config.RetrieveRetained, message)
// Check for errors during publishing (More on error reporting
// https://pkg.go.dev/github.com/eclipse/paho.mqtt.golang#readme-error-handling)
if token.Wait() && token.Error() != nil {
m.logger.Errorf("error while publishing message, error: %v", token.Error())
return token.Error()
}
t := time.Since(s)
m.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: m.config.Hostname,
PubSubBackend: "MQTT",
Time: t.Microseconds(),
})
m.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
func (m *MQTT) Health() datasource.Health {
res := datasource.Health{
Status: "DOWN",
Details: map[string]any{
"backend": "MQTT",
"host": m.config.Hostname,
},
}
if m.Client == nil {
m.logger.Errorf("%v", "datasource not initialized")
return res
}
err := m.Ping()
if err != nil {
m.logger.Errorf("%v", "health check failed")
return res
}
res.Status = "UP"
return res
}
func (m *MQTT) CreateTopic(_ context.Context, topic string) error {
token := m.Client.Publish(topic, m.config.QoS, false, []byte("topic creation"))
token.Wait()
if token.Error() != nil {
m.logger.Errorf("unable to create topic '%s', error: %v", topic, token.Error())
return token.Error()
}
return nil
}
// DeleteTopic is implemented to adhere to the PubSub Client interface
// Note: there is no concept of deletion.
func (*MQTT) DeleteTopic(_ context.Context, _ string) error {
return nil
}
// Extended Functionalities for MQTT
// SubscribeWithFunction subscribe with a subscribing function, called whenever broker publishes a message.
func (m *MQTT) SubscribeWithFunction(topic string, subscribeFunc SubscribeFunc) error {
token := m.Client.Subscribe(topic, 1, getHandler(subscribeFunc))
if token.Wait() && token.Error() != nil {
return token.Error()
}
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/mqtt/mqtt_test.go
================================================
package mqtt
import (
"context"
"errors"
"net/url"
"strconv"
"sync"
"testing"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
var (
errToken = errors.New("connection error with MQTT")
errTest = errors.New("test error")
)
var (
//nolint:gochecknoglobals //used for testing purposes only
mockConfigs = &Config{
Protocol: "tcp",
Hostname: "localhost",
Port: 1883,
Username: "admin",
Password: "admin",
ClientID: "abc2222",
QoS: 1,
Order: false,
RetrieveRetained: false,
KeepAlive: 0,
}
//nolint:gochecknoglobals //used for testing purposes only
msg = []byte(`hello world`)
)
func TestMQTT_New(t *testing.T) {
var client *MQTT
conf := Config{
Protocol: "tcp",
Hostname: "localhost",
Port: 1883,
QoS: 0,
Order: false,
RetrieveRetained: false,
}
out := testutil.StderrOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.ERROR)
client = New(&conf, mockLogger, nil)
})
assert.NotNil(t, client.Client)
assert.Contains(t, out, "could not connect to MQTT")
}
// TestMQTT_EmptyConfigs test the scenario where configs are not provided and
// a client tries to connect to the public broker.
func TestMQTT_EmptyConfigs(t *testing.T) {
var client *MQTT
out := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
client = New(&Config{Username: "gofr-mqtt-test"}, mockLogger, nil)
})
assert.NotNil(t, client)
assert.Contains(t, out, "connected to MQTT")
}
func TestMQTT_getMQTTClientOptions(t *testing.T) {
conf := Config{
Protocol: "tcp",
Hostname: "localhost",
Port: 1883,
QoS: 0,
Username: "user",
Password: "pass",
ClientID: "test",
Order: false,
}
expectedURL, _ := url.Parse("tcp://localhost:1883")
options := getMQTTClientOptions(&conf)
assert.Contains(t, options.ClientID, conf.ClientID)
assert.ElementsMatch(t, options.Servers, []*url.URL{expectedURL})
assert.Equal(t, conf.Username, options.Username)
assert.Equal(t, conf.Password, options.Password)
assert.Equal(t, conf.Order, options.Order)
}
func TestMQTT_Ping(t *testing.T) {
ctrl, mq, mockClient, _, _ := getMockMQTT(t, nil)
defer ctrl.Finish()
mockClient.EXPECT().IsConnected().Return(true)
// Success Case
err := mq.Ping()
require.NoError(t, err)
mockClient.EXPECT().Disconnect(uint(1))
// Disconnect the client
_ = mq.Disconnect(1)
mockClient.EXPECT().IsConnected().Return(false)
// Failure Case
err = mq.Ping()
require.Error(t, err)
assert.Equal(t, errClientNotConnected, err)
}
func TestMQTT_Disconnect(t *testing.T) {
ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
ctx := t.Context()
mockMetrics.EXPECT().
IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test")
mockClient.EXPECT().Disconnect(uint(1))
mockClient.EXPECT().Publish("test", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(3)
// Disconnect the broker and then try to publish
_ = client.Disconnect(1)
err := client.Publish(ctx, "test", msg)
require.Error(t, err)
require.ErrorIs(t, err, errToken)
}
func TestMQTT_DisconnectWithSubscriptions(t *testing.T) {
subs := make(map[string]subscription)
subs["test/topic"] = subscription{
msgs: make(chan *pubsub.Message),
handler: func(_ mqtt.Client, _ mqtt.Message) {},
}
ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
client.subscriptions = subs
mockClient.EXPECT().Disconnect(uint(1))
mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
_ = client.Disconnect(1)
// we assert that on unsubscribing the subscription gets deleted
_, ok := client.subscriptions["test/topic"]
assert.False(t, ok)
}
func TestMQTT_PublishSuccess(t *testing.T) {
out := testutil.StdoutOutputForFunc(func() {
ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
ctx := t.Context()
mockMetrics.EXPECT().
IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic")
mockMetrics.EXPECT().
IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", "test/topic")
mockClient.EXPECT().Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg).
Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
err := client.Publish(ctx, "test/topic", msg)
require.NoError(t, err)
})
assert.Contains(t, out, "PUB")
assert.Contains(t, out, "MQTT")
assert.Contains(t, out, "hello world")
assert.Contains(t, out, "test/topic")
}
func TestMQTT_PublishFailure(t *testing.T) {
ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
ctx := t.Context()
// case where the client has been disconnected, resulting in a Publishing failure
mockMetrics.EXPECT().
IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", "test/topic")
mockClient.EXPECT().Disconnect(uint(1))
// Disconnect the client
_ = client.Disconnect(1)
mockClient.EXPECT().Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, msg).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(3)
err := client.Publish(ctx, "test/topic", []byte(`hello world`))
require.Error(t, err)
require.ErrorIs(t, err, errToken)
}
func TestMQTT_SubscribeSuccess(t *testing.T) {
ctrl, client, mockClient, mockMetrics, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
ctx := t.Context()
mockMetrics.EXPECT().
IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", "test/topic")
mockClient.EXPECT().IsConnected().Return(true)
mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
go func() {
client.subscriptions["test/topic"].msgs <- &pubsub.Message{
Topic: "test/topic",
Value: msg,
MetaData: nil,
Committer: &message{msg: mockMessage{}},
}
}()
m, err := client.Subscribe(ctx, "test/topic")
assert.Equal(t, msg, m.Value)
require.NoError(t, err)
}
func TestMQTT_SubscribeFailure(t *testing.T) {
ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
ctx := t.Context()
mockClient.EXPECT().IsConnected().Return(true)
mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(3)
m, err := client.Subscribe(ctx, "test/topic")
assert.Nil(t, m)
require.Error(t, err)
require.ErrorIs(t, err, errToken)
}
func TestMQTT_SubscribeWithFunc(t *testing.T) {
ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
subscriptionFunc := func(msg *pubsub.Message) error {
assert.Equal(t, "test/topic", msg.Topic)
return nil
}
subscriptionFuncErr := func(*pubsub.Message) error {
return errTest
}
// Success case
mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
err := client.SubscribeWithFunction("test/topic", subscriptionFunc)
require.NoError(t, err)
// Error case where error is returned from subscription function
mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
err = client.SubscribeWithFunction("test/topic", subscriptionFuncErr)
require.NoError(t, err)
// Unsubscribe from the topic
mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
_ = client.Unsubscribe("test/topic")
// Error case where the client cannot connect
mockClient.EXPECT().Disconnect(uint(1))
_ = client.Disconnect(1)
mockClient.EXPECT().Subscribe("test/topic", mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(2)
err = client.SubscribeWithFunction("test/topic", subscriptionFunc)
require.Error(t, err)
require.ErrorIs(t, err, errToken)
}
func Test_getHandler(t *testing.T) {
subscriptionFunc := func(msg *pubsub.Message) error {
assert.Equal(t, []byte("hello from sub func"), msg.Value)
assert.Equal(t, map[string]string{"qos": string(byte(1)), "retained": "false", "messageID": "123"}, msg.MetaData)
assert.Equal(t, "test/topic", msg.Topic)
return nil
}
h := getHandler(subscriptionFunc)
h(nil, mockMessage{
duplicate: false,
qos: 1,
retained: false,
topic: "test/topic",
messageID: 123,
payload: "hello from sub func",
})
}
func TestMQTT_Unsubscribe(t *testing.T) {
out := testutil.StderrOutputForFunc(func() {
ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
// Success case
mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
err := client.Unsubscribe("test/topic")
require.NoError(t, err)
// Failure case
mockClient.EXPECT().Disconnect(uint(1))
_ = client.Disconnect(1)
mockClient.EXPECT().Unsubscribe("test/topic").Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(3)
err = client.Unsubscribe("test/topic")
require.Error(t, err)
})
assert.Contains(t, out, "error while unsubscribing from topic 'test/topic'")
}
func TestMQTT_CreateTopic(t *testing.T) {
out := testutil.StderrOutputForFunc(func() {
ctrl, client, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
// Success case
mockClient.EXPECT().
Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, []byte("topic creation")).
Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(nil)
err := client.CreateTopic(t.Context(), "test/topic")
require.NoError(t, err)
// Failure case
mockClient.EXPECT().Disconnect(uint(1))
client.Client.Disconnect(1)
mockClient.EXPECT().
Publish("test/topic", mockConfigs.QoS, mockConfigs.RetrieveRetained, []byte("topic creation")).
Return(mockToken)
mockToken.EXPECT().Wait().Return(true)
mockToken.EXPECT().Error().Return(errToken).Times(3)
err = client.CreateTopic(t.Context(), "test/topic")
require.Error(t, err)
})
assert.Contains(t, out, "unable to create topic 'test/topic'")
}
func TestMQTT_Health(t *testing.T) {
// The Client is not configured(nil)
out := testutil.StderrOutputForFunc(func() {
m := &MQTT{config: &Config{}, logger: logging.NewMockLogger(logging.ERROR)}
res := m.Health()
assert.Equal(t, datasource.Health{
Status: "DOWN",
Details: map[string]any{"backend": "MQTT", "host": ""},
}, res)
})
assert.Contains(t, out, "datasource not initialized")
// The client ping fails
out = testutil.StderrOutputForFunc(func() {
ctrl, client, mockClient, _, _ := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockClient.EXPECT().IsConnected().Return(false)
res := client.Health()
assert.Equal(t, datasource.Health{
Status: "DOWN",
Details: map[string]any{"backend": "MQTT", "host": "localhost"},
}, res)
})
assert.Contains(t, out, "health check failed")
// Success Case - Status UP
_ = testutil.StderrOutputForFunc(func() {
ctrl, client, mockClient, _, _ := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockClient.EXPECT().IsConnected().Return(true)
res := client.Health()
assert.Equal(t, datasource.Health{
Status: "UP",
Details: map[string]any{"backend": "MQTT", "host": "localhost"},
}, res)
})
}
func TestMQTT_DeleteTopic(t *testing.T) {
m := &MQTT{}
err := m.DeleteTopic(t.Context(), "test/topic")
require.NoError(t, err)
}
func TestReconnectingHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Infof("reconnecting to MQTT at '%v:%v' with clientID '%v'", "any", 1234, "gopher")
handler := createReconnectingHandler(mockLogger, &Config{
Hostname: "any",
Port: 1234,
ClientID: "gopher",
})
assert.NotNil(t, handler)
handler(NewMockClient(ctrl), &mqtt.ClientOptions{})
}
func TestConnectionLostHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Errorf("mqtt connection lost, error: %v", gomock.Any()).
DoAndReturn(func(_ string, args ...any) {
assert.Len(t, args, 1)
require.Error(t, mqtt.ErrNotConnected, args[0])
})
connectionLostHandler := createConnectionLostHandler(mockLogger)
connectionLostHandler(NewMockClient(ctrl), mqtt.ErrNotConnected)
}
func TestReconnectHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
qos := byte(1)
clientMock := NewMockClient(ctrl)
tokenMock := NewMockToken(ctrl)
clientMock.EXPECT().Subscribe("topic1", qos, gomock.Any()).Return(tokenMock)
tokenMock.EXPECT().Wait().Return(true)
tokenMock.EXPECT().Error().Return(nil)
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Debugf("resubscribed to topic %s successfully", "topic1")
msgsChan := make(chan *pubsub.Message)
defer close(msgsChan)
subs := map[string]subscription{
"topic1": {
msgs: msgsChan,
handler: func(_ mqtt.Client, _ mqtt.Message) {},
},
}
reconnectHandler := createReconnectHandler(&sync.RWMutex{}, &Config{
Hostname: "any",
Port: 1234,
ClientID: "gopher",
QoS: qos,
}, subs, mockLogger)
assert.NotNil(t, reconnectHandler)
reconnectHandler(clientMock)
}
func TestMQTT_createMqttHandler(t *testing.T) {
var (
out string
msgs = make(chan *pubsub.Message)
wg = sync.WaitGroup{}
)
wg.Add(1)
go func() {
defer wg.Done()
out = testutil.StdoutOutputForFunc(func() {
ctrl, client, _, mockMetrics, _ := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", "test/topic")
handler := client.createMqttHandler(t.Context(), "test/topic", msgs)
handler(nil, mockMessage{false, 1, false, "test/topic", 123, "hello world"})
})
}()
m := <-msgs
close(msgs)
assert.NotNil(t, m)
assert.Equal(t, m.Value, msg)
// wait for the goroutine test to finish writing log to out
wg.Wait()
assert.Contains(t, out, "SUB")
assert.Contains(t, out, "hello world")
assert.Contains(t, out, "test/topic")
assert.Contains(t, out, "MQTT")
}
func getMockMQTT(t *testing.T, conf *Config) (*gomock.Controller, *MQTT, *MockClient, *MockMetrics, *MockToken) {
t.Helper()
ctrl := gomock.NewController(t)
mockClient := NewMockClient(ctrl)
mockToken := NewMockToken(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockMetrics := NewMockMetrics(ctrl)
mq := &MQTT{mockClient, mockLogger, mockMetrics, conf, make(map[string]subscription), &sync.RWMutex{}}
return ctrl, mq, mockClient, mockMetrics, mockToken
}
func TestMQTT_Close(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
ctrl, client, mockMQTT, _, _ := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockMQTT.EXPECT().Disconnect(uint(0 * time.Millisecond))
// Close the client
err := client.Close()
require.NoError(t, err)
}
func Test_parseQueryArgs(t *testing.T) {
tests := []struct {
name string
args []any
expectedTimeout time.Duration
expectedLimit int
}{
{"no args", []any{}, defaultQueryCollectTimeout, defaultQueryMessageLimit},
{"only timeout arg", []any{10 * time.Second}, 10 * time.Second, defaultQueryMessageLimit},
{"timeout and limit args", []any{15 * time.Second, 5}, 15 * time.Second, 5},
{"invalid timeout type, valid limit", []any{"not a duration", 5}, defaultQueryCollectTimeout, 5},
{"valid timeout, invalid limit type", []any{10 * time.Second, "not an int"}, 10 * time.Second, defaultQueryMessageLimit},
{"only limit arg (nil for timeout)", []any{nil, 7}, defaultQueryCollectTimeout, 7},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
timeout, limit := parseQueryArgs(tt.args...)
assert.Equal(t, tt.expectedTimeout, timeout, "Timeout mismatch")
assert.Equal(t, tt.expectedLimit, limit, "Limit mismatch")
})
}
}
func TestMQTT_createQueryMessageHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mq := &MQTT{logger: mockLogger}
msgChan := make(chan *pubsub.Message, 1) // Buffer size of 1
topic := "test/handler/topic"
handler := mq.createQueryMessageHandler(t.Context(), msgChan, topic)
mockMsg1 := &mockMessage{topic: topic, payload: "message 1", messageID: 123, qos: 1, retained: false}
mockMsg2 := &mockMessage{topic: topic, payload: "message 2 (dropped)", messageID: 124, qos: 1, retained: false}
// Send first message
handler(nil, mockMsg1)
// Expect a log when the second message is dropped due to buffer overflow
mockLogger.EXPECT().Debugf("Query: msgChan full for topic %s, message dropped during collection", topic).Times(1)
// Send second message, which should be dropped
handler(nil, mockMsg2)
// Assert the received message (single assertion block)
receivedMsg := <-msgChan
assert.Equal(t, mockMsg1.Topic(), receivedMsg.Topic)
assert.Equal(t, mockMsg1.Payload(), receivedMsg.Value)
meta := receivedMsg.MetaData.(map[string]string)
assert.Equal(t, map[string]string{
"messageID": strconv.Itoa(int(mockMsg1.MessageID())),
"qos": string(mockMsg1.Qos()),
"retained": strconv.FormatBool(mockMsg1.Retained()),
}, meta, "Metadata mismatch")
// Ensure the channel is empty
assert.Empty(t, msgChan, "Channel should be empty after reading the first message")
}
func TestMQTT_subscribeToTopicForQuery_SuccessAndErrors(t *testing.T) {
topic := "test/subquery"
timeout := 50 * time.Millisecond
dummyHandler := func(_ mqtt.Client, _ mqtt.Message) {}
testCases := []struct {
name string
waitSuccess bool
tokenErr error
expectedErr error
}{
{"success", true, nil, nil},
{"timeout without token error", false, nil, errSubscriptionTimeout},
{"timeout with token error", false, errToken, errSubscriptionFailed},
{"completed but token error", true, errToken, errSubscriptionFailed},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl, mq, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockClient.EXPECT().Subscribe(topic, mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().WaitTimeout(timeout).Return(tc.waitSuccess)
mockToken.EXPECT().Error().Return(tc.tokenErr)
err := mq.subscribeToTopicForQuery(t.Context(), topic, timeout, dummyHandler)
assert.ErrorIs(t, err, tc.expectedErr)
})
}
}
func TestMQTT_subscribeToTopicForQuery_ContextError(t *testing.T) {
topicName := "test/subquery"
subscribeTimeout := 50 * time.Millisecond // Short timeout for tests
var dummyHandler mqtt.MessageHandler = func(_ mqtt.Client, _ mqtt.Message) {}
t.Run("error_context_canceled_during_wait", func(t *testing.T) {
ctrl, mq, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
cancelledCtx, cancel := context.WithCancel(t.Context())
cancel() // Cancel context immediately
mockClient.EXPECT().Subscribe(topicName, mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().WaitTimeout(subscribeTimeout).Return(false) // Assume WaitTimeout returns false due to context
err := mq.subscribeToTopicForQuery(cancelledCtx, topicName, subscribeTimeout, dummyHandler)
require.Error(t, err)
require.ErrorIs(t, err, context.Canceled) // Check that the specific context error is wrapped
assert.Contains(t, err.Error(), "context error during MQTT subscription to '"+topicName+"': context canceled")
})
t.Run("error_context_deadline_exceeded_during_wait", func(t *testing.T) {
ctrl, mq, mockClient, _, mockToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
deadlineCtx, cancel := context.WithTimeout(t.Context(), 1*time.Nanosecond) // Ensure deadline exceeded
defer cancel()
time.Sleep(5 * time.Millisecond) // Give time for deadline to pass
mockClient.EXPECT().Subscribe(topicName, mockConfigs.QoS, gomock.Any()).Return(mockToken)
mockToken.EXPECT().WaitTimeout(subscribeTimeout).Return(false)
err := mq.subscribeToTopicForQuery(deadlineCtx, topicName, subscribeTimeout, dummyHandler)
require.Error(t, err)
require.ErrorIs(t, err, context.DeadlineExceeded)
assert.Contains(t, err.Error(), "context error during MQTT subscription to '"+topicName+"': context deadline exceeded")
})
}
func TestMQTT_Query_SuccessCases(t *testing.T) {
topic := "test/query/success"
testCases := []struct {
name string
messageLimit int
timeout time.Duration
messages []string
expectedData string
}{
{"one_message_default_args", 1, 150 * time.Millisecond, []string{"hello query one"}, "hello query one"},
{"multiple_messages_limit_reached", 2, 150 * time.Millisecond, []string{"msgA", "msgB", "msgC"}, "msgA\nmsgB"},
{"timeout_reached_before_limit", 5, 30 * time.Millisecond, []string{"only"}, "only"},
{"no_messages_collected", 1, 30 * time.Millisecond, []string{}, ""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl, mq, mockClient, _, mockSubToken := getMockMQTT(t, mockConfigs)
defer ctrl.Finish()
mockClient.EXPECT().IsConnected().Return(true)
var capturedHandler mqtt.MessageHandler
mockClient.EXPECT().Subscribe(topic, mockConfigs.QoS, gomock.Any()).
DoAndReturn(func(_ string, _ byte, h mqtt.MessageHandler) mqtt.Token {
capturedHandler = h
return mockSubToken
})
mockSubToken.EXPECT().WaitTimeout(tc.timeout).Return(true)
mockSubToken.EXPECT().Error().Return(nil)
mockUnsubToken := NewMockToken(ctrl)
mockClient.EXPECT().Unsubscribe(topic).Return(mockUnsubToken)
mockUnsubToken.EXPECT().WaitTimeout(unsubscribeOpTimeout).Return(true)
// Simulate message publishing
go func() {
time.Sleep(10 * time.Millisecond)
for _, msg := range tc.messages {
capturedHandler(nil, &mockMessage{topic: topic, payload: msg, qos: int(mockConfigs.QoS)})
}
}()
result, err := mq.Query(t.Context(), topic, tc.timeout, tc.messageLimit)
require.NoError(t, err)
assert.Equal(t, tc.expectedData, string(result))
})
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/client.go
================================================
package nats
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
//go:generate mockgen -destination=mock_tracer.go -package=nats go.opentelemetry.io/otel/trace Tracer
const defaultRetryTimeout = 2 * time.Second
var (
errClientNotConnected = errors.New("nats client not connected")
errEmptySubject = errors.New("subject name cannot be empty")
)
const (
goFrNatsStreamName = "gofr_migrations"
defaultDeleteTimeout = 5 * time.Second
defaultQueryTimeout = 30 * time.Second
defaultMaxBytes = 100 * 1024 * 1024
defaultAckWait = 30 * time.Second
)
// Client represents a Client for NATS jStream operations.
type Client struct {
connManager ConnectionManagerInterface
subManager SubscriptionManagerInterface
subscriptions map[string]context.CancelFunc
subMutex sync.Mutex
streamManager StreamManagerInterface
Config *Config
logger pubsub.Logger
metrics Metrics
tracer trace.Tracer
natsConnector Connector
jetStreamCreator JetStreamCreator
}
type messageHandler func(context.Context, jetstream.Msg) error
// Connect establishes a connection to NATS and sets up jStream.
func (c *Client) Connect() error {
c.logger.Debugf("connecting to NATS server at %v", c.Config.Server)
if err := validateAndPrepare(c.Config, c.logger); err != nil {
return err
}
if err := c.establishConnection(); err != nil {
c.logger.Errorf("failed to connect to NATS server at %v: %v", c.Config.Server, err)
go c.retryConnect()
return err
}
return nil
}
// UseLogger sets the logger for the NATS client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(pubsub.Logger); ok {
c.logger = l
}
}
// UseTracer sets the tracer for the NATS client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// UseMetrics sets the metrics for the NATS client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// Publish publishes a message to a topic.
func (c *Client) Publish(ctx context.Context, subject string, message []byte) error {
if err := checkClient(c); err != nil {
return err
}
return c.connManager.Publish(ctx, subject, message, c.metrics)
}
// Subscribe subscribes to a topic and returns a single message.
func (c *Client) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
for {
if err := checkClient(c); err != nil {
time.Sleep(defaultRetryTimeout)
return nil, errClientNotConnected
}
js, err := c.connManager.jetStream()
if err == nil {
return c.subManager.Subscribe(ctx, topic, js, c.Config, c.logger, c.metrics)
}
c.logger.Debugf("Waiting for NATS connection to be established for topic %s", topic)
time.Sleep(defaultRetryTimeout)
}
}
func (c *Client) SubscribeWithHandler(ctx context.Context, subject string, handler messageHandler) error {
c.subMutex.Lock()
defer c.subMutex.Unlock()
// Cancel any existing subscription for this subject
c.cancelExistingSubscription(subject)
js, err := c.connManager.jetStream()
if err != nil {
return err
}
consumerName := fmt.Sprintf("%s_%s", c.Config.Consumer, strings.ReplaceAll(subject, ".", "_"))
cons, err := c.createOrUpdateConsumer(ctx, js, subject, consumerName)
if err != nil {
return err
}
// Create a new context for this subscription
subCtx, cancel := context.WithCancel(ctx)
c.subscriptions[subject] = cancel
go func() {
defer cancel() // Ensure the cancellation is handled properly
c.processMessages(subCtx, cons, subject, handler)
}()
return nil
}
func (c *Client) cancelExistingSubscription(subject string) {
if cancel, exists := c.subscriptions[subject]; exists {
cancel()
delete(c.subscriptions, subject)
}
}
// Close closes the Client.
func (c *Client) Close(ctx context.Context) error {
c.subManager.Close()
if c.connManager != nil {
c.connManager.Close(ctx)
}
return nil
}
// Query retrieves messages from a NATS stream/subject.
func (c *Client) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if err := checkClient(c); err != nil {
return nil, err
}
if query == "" {
return nil, errEmptySubject
}
// Parse optional arguments
timeout, limit := parseQueryArgs(args...)
// Create a query context with timeout
queryCtx, cancel := createQueryContext(ctx, timeout)
defer cancel()
js, err := c.connManager.jetStream()
if err != nil {
return nil, err
}
streamName := c.getStreamName(query)
consumerName := fmt.Sprintf("query_%s_%d", query, time.Now().UnixNano())
// Create a consumer
cons, err := c.createConsumer(queryCtx, js, streamName, query, consumerName)
if err != nil {
return nil, err
}
defer c.cleanupConsumer(js, streamName, cons)
// Collect messages
return collectMessages(queryCtx, cons, limit, c.Config, c.logger)
}
// CreateTopic creates a new topic (stream) in NATS jStream.
func (c *Client) CreateTopic(ctx context.Context, name string) error {
if err := checkClient(c); err != nil {
return err
}
// For migrations stream, use special configuration with max bytes
if name == goFrNatsStreamName {
return c.streamManager.CreateStream(ctx, &StreamConfig{
Stream: name,
Subjects: []string{name},
MaxBytes: defaultMaxBytes,
Storage: "file",
Retention: "limits",
MaxAge: 365 * 24 * time.Hour,
})
}
return c.streamManager.CreateStream(ctx, &StreamConfig{
Stream: name,
Subjects: []string{name},
})
}
// DeleteTopic deletes a topic (stream) in NATS jStream.
func (c *Client) DeleteTopic(ctx context.Context, name string) error {
if err := checkClient(c); err != nil {
return err
}
return c.streamManager.DeleteStream(ctx, name)
}
// CreateStream creates a new stream in NATS jStream.
func (c *Client) CreateStream(ctx context.Context, cfg *StreamConfig) error {
if err := checkClient(c); err != nil {
return err
}
return c.streamManager.CreateStream(ctx, cfg)
}
// DeleteStream deletes a stream in NATS jStream.
func (c *Client) DeleteStream(ctx context.Context, name string) error {
if err := checkClient(c); err != nil {
return err
}
return c.streamManager.DeleteStream(ctx, name)
}
// CreateOrUpdateStream creates or updates a stream in NATS jStream.
func (c *Client) CreateOrUpdateStream(ctx context.Context, cfg *jetstream.StreamConfig) (jetstream.Stream, error) {
if err := checkClient(c); err != nil {
return nil, err
}
return c.streamManager.CreateOrUpdateStream(ctx, cfg)
}
// GetJetStreamStatus returns the status of the jStream connection.
func GetJetStreamStatus(ctx context.Context, js jetstream.JetStream) (string, error) {
_, err := js.AccountInfo(ctx)
if err != nil {
return jetStreamStatusError, err
}
return jetStreamStatusOK, nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/client_helper.go
================================================
package nats
import (
"context"
"errors"
"strings"
"time"
"github.com/nats-io/nats.go/jetstream"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// establishConnection handles the actual connection process to NATS and sets up jStream.
func (c *Client) establishConnection() error {
connManager := NewConnectionManager(c.Config, c.logger, c.natsConnector, c.jetStreamCreator)
if err := connManager.Connect(); err != nil {
return err
}
c.connManager = connManager
js, err := c.connManager.jetStream()
if err != nil {
return err
}
c.streamManager = newStreamManager(js, c.logger)
c.subManager = newSubscriptionManager(batchSize)
c.logger.Logf("connected to NATS server '%s'", c.Config.Server)
return nil
}
func (c *Client) retryConnect() {
for {
c.logger.Debugf("connecting to NATS server at %v", c.Config.Server)
if err := c.establishConnection(); err != nil {
c.logger.Errorf("failed to connect to NATS server at %v: %v", c.Config.Server, err)
time.Sleep(defaultRetryTimeout)
continue
}
return
}
}
func validateAndPrepare(config *Config, logger pubsub.Logger) error {
if err := validateConfigs(config); err != nil {
logger.Errorf("could not initialize NATS jStream: %v", err)
return err
}
return nil
}
func (c *Client) createOrUpdateConsumer(
ctx context.Context, js jetstream.JetStream, subject, consumerName string) (jetstream.Consumer, error) {
cons, err := js.CreateOrUpdateConsumer(ctx, c.Config.Stream.Stream, jetstream.ConsumerConfig{
Durable: consumerName,
AckPolicy: jetstream.AckExplicitPolicy,
FilterSubject: subject,
MaxDeliver: c.Config.Stream.MaxDeliver,
DeliverPolicy: jetstream.DeliverNewPolicy,
})
if err != nil {
c.logger.Errorf("failed to create or update consumer: %v", err)
return nil, err
}
return cons, nil
}
func (c *Client) processMessages(ctx context.Context, cons jetstream.Consumer, subject string, handler messageHandler) {
for ctx.Err() == nil {
if err := c.fetchAndProcessMessages(ctx, cons, subject, handler); err != nil {
c.logger.Errorf("Error in message processing loop for subject %s: %v", subject, err)
}
}
}
func (c *Client) fetchAndProcessMessages(ctx context.Context, cons jetstream.Consumer, subject string, handler messageHandler) error {
msgs, err := cons.Fetch(1, jetstream.FetchMaxWait(c.Config.MaxWait))
if err != nil {
if !errors.Is(err, context.DeadlineExceeded) {
c.logger.Errorf("Error fetching messages for subject %s: %v", subject, err)
}
return err
}
return c.processFetchedMessages(ctx, msgs, handler, subject)
}
func (c *Client) processFetchedMessages(ctx context.Context, msgs jetstream.MessageBatch, handler messageHandler, subject string) error {
for msg := range msgs.Messages() {
if err := c.handleMessage(ctx, msg, handler); err != nil {
c.logger.Errorf("Error processing message: %v", err)
}
}
if err := msgs.Error(); err != nil {
c.logger.Errorf("Error in message batch for subject %s: %v", subject, err)
return err
}
return nil
}
func (c *Client) handleMessage(ctx context.Context, msg jetstream.Msg, handler messageHandler) error {
err := handler(ctx, msg)
if err == nil {
if ackErr := msg.Ack(); ackErr != nil {
c.logger.Errorf("Error sending ACK for message: %v", ackErr)
return ackErr
}
return nil
}
c.logger.Errorf("Error handling message: %v", err)
if nakErr := msg.Nak(); nakErr != nil {
c.logger.Debugf("Error sending NAK for message: %v", nakErr)
return nakErr
}
return err
}
// parseQueryArgs parses the query arguments.
func parseQueryArgs(args ...any) (timeout time.Duration, limit int) {
// Default values
timeout = defaultQueryTimeout
limit = 100
if len(args) > 0 {
// First argument can be a custom timeout
if val, ok := args[0].(time.Duration); ok && val > 0 {
timeout = val
}
}
if len(args) > 1 {
// Second argument is the message limit
if val, ok := args[1].(int); ok && val > 0 {
limit = val
}
}
return timeout, limit
}
// collectMessages fetches messages from the consumer and combines them.
func collectMessages(ctx context.Context, cons jetstream.Consumer, limit int,
config *Config, logger pubsub.Logger) ([]byte, error) {
var result []byte
messagesCollected := 0
for messagesCollected < limit {
// Fetch messages with a batch size based on remaining needed messages
fetchSize := minInt(batchSize, limit-messagesCollected)
if fetchSize <= 0 {
break
}
msgs, err := fetchBatch(cons, fetchSize, config.MaxWait, logger)
if err != nil {
return result, err
}
collected := processBatch(ctx, msgs, &result, &messagesCollected, limit, logger)
if !collected {
break
}
}
if strings.Contains(cons.CachedInfo().Config.FilterSubject, goFrNatsStreamName) && len(result) == 0 {
logger.Debugf("No migration records found in stream %s", config.Stream.Stream)
}
return result, nil
}
func fetchBatch(cons jetstream.Consumer, fetchSize int,
maxWait time.Duration, logger pubsub.Logger) (jetstream.MessageBatch, error) {
msgs, err := cons.Fetch(fetchSize, jetstream.FetchMaxWait(maxWait))
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
return nil, nil
}
logger.Errorf("Error fetching messages: %v", err)
return nil, err
}
return msgs, nil
}
func processBatch(ctx context.Context, msgs jetstream.MessageBatch,
result *[]byte, messagesCollected *int, limit int, logger pubsub.Logger) bool {
receivedAny := false
for msg := range msgs.Messages() {
receivedAny = true
// Add newline separator between messages
if len(*result) > 0 {
*result = append(*result, '\n')
}
// Append message data
*result = append(*result, msg.Data()...)
// Acknowledge the message
if err := msg.Ack(); err != nil {
logger.Debugf("Error acknowledging message: %v", err)
}
*messagesCollected++
if *messagesCollected >= limit {
break
}
}
if !receivedAny || errors.Is(ctx.Err(), context.DeadlineExceeded) || errors.Is(ctx.Err(), context.Canceled) {
return false
}
return true
}
// Helper function for Go versions < 1.21.
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func checkClient(c *Client) error {
if c == nil {
return errClientNotConnected
}
if c.connManager == nil {
return errClientNotConnected
}
if !c.connManager.isConnected() {
return errClientNotConnected
}
return nil
}
func createQueryContext(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if _, hasDeadline := ctx.Deadline(); hasDeadline {
return ctx, func() {}
}
return context.WithTimeout(ctx, timeout)
}
func (c *Client) getStreamName(query string) string {
if query == goFrNatsStreamName {
return goFrNatsStreamName
}
return c.Config.Stream.Stream
}
func (c *Client) createConsumer(ctx context.Context, js jetstream.JetStream, streamName,
query, consumerName string) (jetstream.Consumer, error) {
cons, err := js.CreateOrUpdateConsumer(ctx, streamName, jetstream.ConsumerConfig{
Durable: consumerName,
AckPolicy: jetstream.AckExplicitPolicy,
FilterSubject: query,
DeliverPolicy: jetstream.DeliverAllPolicy,
AckWait: c.Config.MaxWait,
})
if err != nil {
c.logger.Errorf("failed to create consumer for query: %v", err)
return nil, err
}
return cons, nil
}
func (c *Client) cleanupConsumer(js jetstream.JetStream, streamName string, cons jetstream.Consumer) {
deleteCtx, cancel := context.WithTimeout(context.Background(), defaultDeleteTimeout)
defer cancel()
info, err := cons.Info(deleteCtx)
if err == nil {
if err := js.DeleteConsumer(deleteCtx, streamName, info.Name); err != nil {
c.logger.Debugf("failed to delete temporary consumer: %v", err)
}
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/client_test.go
================================================
package nats
import (
"context"
"sync"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestNATSClient_Publish(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockMetrics := NewMockMetrics(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
conf := &Config{
Server: NATSServer,
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
}
client := &Client{
connManager: mockConnManager,
Config: conf,
logger: mockLogger,
metrics: mockMetrics,
}
ctx := t.Context()
subject := "test-subject"
message := []byte("test-message")
// Set up expected calls
gomock.InOrder(
mockConnManager.EXPECT().IsConnected().Return(true),
mockConnManager.EXPECT().Publish(ctx, subject, message, mockMetrics).Return(nil),
)
// Call Publish
err := client.Publish(ctx, subject, message)
require.NoError(t, err)
}
func TestNATSClient_PublishError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMetrics := NewMockMetrics(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
ctx := t.Context()
subject := "test"
message := []byte("test-message")
config := &Config{
Server: NATSServer,
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
}
client := &Client{
connManager: mockConnManager,
metrics: mockMetrics,
Config: config,
logger: logging.NewMockLogger(logging.DEBUG),
}
tests := []struct {
name string
client *Client
mockCall func(mockConn *MockConnectionManagerInterface)
expErr error
}{
{name: "nil client", client: nil, mockCall: func(_ *MockConnectionManagerInterface) {}, expErr: errClientNotConnected},
{name: "nil connManager", client: &Client{connManager: nil}, mockCall: func(_ *MockConnectionManagerInterface) {},
expErr: errClientNotConnected},
{name: "not connected to NATS server", client: &Client{connManager: mockConnManager},
mockCall: func(mockConn *MockConnectionManagerInterface) {
mockConn.EXPECT().IsConnected().Return(false)
}, expErr: errClientNotConnected},
{name: "err in publishing", client: client, mockCall: func(mockConn *MockConnectionManagerInterface) {
mockConn.EXPECT().IsConnected().Return(true)
mockConn.EXPECT().Publish(gomock.Any(), subject, message, mockMetrics).Return(errPublishError)
}, expErr: errPublishError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.mockCall(mockConnManager)
err := tt.client.Publish(ctx, subject, message)
require.Error(t, err)
assert.Equal(t, tt.expErr, err)
})
}
}
func TestNATSClient_SubscribeSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSubManager := NewMockSubscriptionManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockJetStream := NewMockJetStream(ctrl)
client := &Client{
connManager: mockConnManager,
subManager: mockSubManager,
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
MaxWait: time.Second,
},
metrics: mockMetrics,
logger: logging.NewMockLogger(logging.DEBUG),
}
ctx := t.Context()
expectedMsg := &pubsub.Message{
Topic: "test-subject",
Value: []byte("test message"),
}
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(mockJetStream, nil).AnyTimes()
mockSubManager.EXPECT().
Subscribe(ctx, "test-subject", mockJetStream, gomock.Any(), gomock.Any(), gomock.Any()).
Return(expectedMsg, nil)
msg, err := client.Subscribe(ctx, "test-subject")
require.NoError(t, err)
assert.Equal(t, expectedMsg, msg)
}
func TestNATSClient_SubscribeError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSubManager := NewMockSubscriptionManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockJetStream := NewMockJetStream(ctrl)
client := &Client{
connManager: mockConnManager,
subManager: mockSubManager,
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
},
metrics: mockMetrics,
logger: logging.NewMockLogger(logging.DEBUG),
}
ctx := t.Context()
expectedErr := errSubscriptionError
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(mockJetStream, nil).AnyTimes()
mockSubManager.EXPECT().
Subscribe(ctx, "test-subject", mockJetStream, gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, expectedErr)
msg, err := client.Subscribe(ctx, "test-subject")
require.Error(t, err)
assert.Nil(t, msg)
assert.Equal(t, expectedErr, err)
}
func TestNATSClient_Close(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockSubManager := NewMockSubscriptionManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
connManager: mockConnManager,
subManager: mockSubManager,
logger: logging.NewMockLogger(logging.DEBUG),
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
},
},
}
ctx := t.Context()
mockSubManager.EXPECT().Close()
mockConnManager.EXPECT().Close(ctx)
err := client.Close(ctx)
require.NoError(t, err)
}
func TestNew(t *testing.T) {
config := &Config{
Server: NATSServer,
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
MaxWait: 5 * time.Second,
}
mockLogger := logging.NewMockLogger(logging.DEBUG)
natsClient := New(config, mockLogger)
assert.NotNil(t, natsClient)
// Check PubSubWrapper struct
assert.NotNil(t, natsClient)
assert.NotNil(t, natsClient.Client)
// Check Client struct
assert.Equal(t, config, natsClient.Client.Config)
assert.NotNil(t, natsClient.Client.subManager)
// Check methods
assert.NotNil(t, natsClient.DeleteTopic)
assert.NotNil(t, natsClient.CreateTopic)
assert.NotNil(t, natsClient.Subscribe)
assert.NotNil(t, natsClient.Publish)
assert.NotNil(t, natsClient.Close)
// Check new methods
assert.NotNil(t, natsClient.UseLogger)
assert.NotNil(t, natsClient.UseMetrics)
assert.NotNil(t, natsClient.UseTracer)
assert.NotNil(t, natsClient.Connect)
// Check that Connect hasn't been called yet
assert.Nil(t, natsClient.Client.connManager)
}
func TestNATSClient_DeleteTopic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
streamManager: mockStreamManager,
connManager: mockConnManager,
logger: logging.NewMockLogger(logging.DEBUG),
Config: &Config{},
}
ctx := t.Context()
gomock.InOrder(
mockConnManager.EXPECT().IsConnected().Return(true),
mockStreamManager.EXPECT().DeleteStream(ctx, "test-topic").Return(nil),
)
err := client.DeleteTopic(ctx, "test-topic")
require.NoError(t, err)
}
func TestNATSClient_DeleteTopic_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
streamManager: mockStreamManager,
connManager: mockConnManager,
logger: logging.NewMockLogger(logging.DEBUG),
Config: &Config{},
}
ctx := t.Context()
expectedErr := errFailedToDeleteStream
gomock.InOrder(
mockConnManager.EXPECT().IsConnected().Return(true),
mockStreamManager.EXPECT().DeleteStream(ctx, "test-topic").Return(expectedErr),
)
err := client.DeleteTopic(ctx, "test-topic")
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func TestNATSClient_CreateTopic(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
streamManager: mockStreamManager,
connManager: mockConnManager,
logger: logging.NewMockLogger(logging.DEBUG),
Config: &Config{},
}
ctx := t.Context()
mockConnManager.EXPECT().IsConnected().Return(true)
mockStreamManager.EXPECT().
CreateStream(ctx, &StreamConfig{
Stream: "test-topic",
Subjects: []string{"test-topic"},
}).Return(nil)
err := client.CreateTopic(ctx, "test-topic")
require.NoError(t, err)
}
func TestClient_Connect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create mocks
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockNATSConnector := NewMockNATSConnector(ctrl)
mockJSCreator := NewMockJetStreamCreator(ctrl)
mockConn := NewMockConnInterface(ctrl)
mockJS := NewMockJetStream(ctrl)
// Set up client with mocks
client := &Client{
Config: &Config{
Server: "nats://localhost:4222",
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
},
logger: mockLogger,
natsConnector: mockNATSConnector,
jetStreamCreator: mockJSCreator,
}
// Set expectations
mockNATSConnector.EXPECT().Connect("nats://localhost:4222", gomock.Any()).
Return(mockConn, nil).Times(2)
mockJSCreator.EXPECT().New(mockConn).Return(mockJS, nil).Times(2)
_ = client.Connect()
time.Sleep(100 * time.Millisecond)
// Assert that the connection manager was set
assert.NotNil(t, client.connManager)
// Check for log output
out := testutil.StdoutOutputForFunc(func() {
client.logger = logging.NewMockLogger(logging.DEBUG)
_ = client.Connect()
time.Sleep(100 * time.Millisecond)
})
// Assert that the expected log message is produced
assert.Contains(t, out, "connecting to NATS server at nats://localhost:4222\n"+
"Successfully connected to NATS server at nats://localhost:4222\n")
}
func TestClient_ValidateAndPrepare(t *testing.T) {
client := &Client{
Config: &Config{},
logger: logging.NewMockLogger(logging.DEBUG),
}
err := validateAndPrepare(client.Config, client.logger)
require.Error(t, err)
client.Config = &Config{
Server: "nats://localhost:4222",
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
}
err = validateAndPrepare(client.Config, client.logger)
assert.NoError(t, err)
}
func TestClient_ValidateAndPrepareError(t *testing.T) {
client := &Client{
Config: &Config{},
logger: logging.NewMockLogger(logging.DEBUG),
}
err := validateAndPrepare(client.Config, client.logger)
require.Error(t, err)
client.Config = &Config{
Server: "",
Stream: StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
},
Consumer: "test-consumer",
}
err = validateAndPrepare(client.Config, client.logger)
assert.Error(t, err)
}
func TestClient_UseLogger(t *testing.T) {
client := &Client{}
mockLogger := logging.NewMockLogger(logging.DEBUG)
client.UseLogger(mockLogger)
assert.Equal(t, mockLogger, client.logger)
client.UseLogger("not a logger")
assert.Equal(t, mockLogger, client.logger) // Should not change
}
func TestClient_UseTracer(t *testing.T) {
client := &Client{}
mockTracer := noop.NewTracerProvider().Tracer("test")
client.UseTracer(mockTracer)
assert.Equal(t, mockTracer, client.tracer)
client.UseTracer("not a tracer")
assert.Equal(t, mockTracer, client.tracer) // Should not change
}
func TestClient_UseMetrics(t *testing.T) {
client := &Client{}
mockMetrics := NewMockMetrics(gomock.NewController(t))
client.UseMetrics(mockMetrics)
assert.Equal(t, mockMetrics, client.metrics)
client.UseMetrics("not metrics")
assert.Equal(t, mockMetrics, client.metrics) // Should not change
}
func TestClient_SubscribeWithHandler(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client, mocks := setupClientAndMocks(t, ctrl)
var wg sync.WaitGroup
wg.Add(2) // Two handlers
t.Run("First_Subscription", func(t *testing.T) {
testFirstSubscription(t, client, mocks, &wg)
})
t.Run("Second_Subscription", func(t *testing.T) {
testSecondSubscription(t, client, mocks, &wg)
})
waitForHandlersToComplete(t, &wg)
err := client.Close(t.Context())
require.NoError(t, err)
}
func setupClientAndMocks(t *testing.T, ctrl *gomock.Controller) (*Client, *testMocks) {
t.Helper()
mocks := &testMocks{
connManager: NewMockConnectionManagerInterface(ctrl),
jetStream: NewMockJetStream(ctrl),
consumer1: NewMockConsumer(ctrl),
messageBatch1: NewMockMessageBatch(ctrl),
msg1: NewMockMsg(ctrl),
consumer2: NewMockConsumer(ctrl),
messageBatch2: NewMockMessageBatch(ctrl),
msg2: NewMockMsg(ctrl),
subManager: NewMockSubscriptionManagerInterface(ctrl),
messageChan1: make(chan jetstream.Msg, 1),
messageChan2: make(chan jetstream.Msg, 1),
}
client := &Client{
connManager: mocks.connManager,
subManager: mocks.subManager,
Config: createTestConfig(),
logger: logging.NewMockLogger(logging.DEBUG),
subscriptions: make(map[string]context.CancelFunc),
}
setupCommonExpectations(mocks)
return client, mocks
}
func createTestConfig() *Config {
return &Config{
Consumer: "test-consumer",
Stream: StreamConfig{
Stream: "test-stream",
MaxDeliver: 3,
},
MaxWait: time.Second,
}
}
func setupCommonExpectations(mocks *testMocks) {
mocks.connManager.EXPECT().JetStream().Return(mocks.jetStream, nil).AnyTimes()
mocks.subManager.EXPECT().Close().Times(1)
mocks.connManager.EXPECT().Close(gomock.Any()).AnyTimes()
}
func testFirstSubscription(t *testing.T, client *Client, mocks *testMocks, wg *sync.WaitGroup) {
t.Helper()
setupFirstSubscriptionExpectations(mocks)
firstHandlerCalled := make(chan bool, 1)
firstHandler := createFirstHandler(t, firstHandlerCalled, wg)
err := client.SubscribeWithHandler(t.Context(), "test-subject", firstHandler)
require.NoError(t, err)
mocks.messageChan1 <- mocks.msg1
close(mocks.messageChan1)
assertHandlerCalled(t, firstHandlerCalled, "First handler")
}
func testSecondSubscription(t *testing.T, client *Client, mocks *testMocks, wg *sync.WaitGroup) {
t.Helper()
setupSecondSubscriptionExpectations(mocks)
errorHandlerCalled := make(chan bool, 1)
errorHandler := createErrorHandler(t, errorHandlerCalled, wg)
err := client.SubscribeWithHandler(t.Context(), "test-subject", errorHandler)
require.NoError(t, err)
mocks.messageChan2 <- mocks.msg2
close(mocks.messageChan2)
assertHandlerCalled(t, errorHandlerCalled, "Error handler")
}
func setupFirstSubscriptionExpectations(mocks *testMocks) {
mocks.jetStream.EXPECT().
CreateOrUpdateConsumer(gomock.Any(), gomock.Any(), gomock.Any()).
Return(mocks.consumer1, nil).
Times(1)
mocks.consumer1.EXPECT().
Fetch(gomock.Any(), gomock.Any()).
Return(mocks.messageBatch1, nil).
Times(2)
gomock.InOrder(
mocks.messageBatch1.EXPECT().
Messages().
Return(mocks.messageChan1).
Times(1),
mocks.messageBatch1.EXPECT().
Messages().
Return(nil).
Times(1),
)
mocks.messageBatch1.EXPECT().
Error().
Return(nil).
Times(1)
mocks.msg1.EXPECT().
Ack().
Return(nil).
Times(1)
}
func setupSecondSubscriptionExpectations(mocks *testMocks) {
mocks.jetStream.EXPECT().
CreateOrUpdateConsumer(gomock.Any(), gomock.Any(), gomock.Any()).
Return(mocks.consumer2, nil).
Times(1)
mocks.consumer2.EXPECT().
Fetch(gomock.Any(), gomock.Any()).
Return(mocks.messageBatch2, nil).
Times(2)
gomock.InOrder(
mocks.messageBatch2.EXPECT().
Messages().
Return(mocks.messageChan2).
Times(1),
mocks.messageBatch2.EXPECT().
Messages().
Return(nil).
Times(1),
)
mocks.messageBatch2.EXPECT().
Error().
Return(nil).
Times(1)
mocks.msg2.EXPECT().
Nak().
Return(nil).
Times(1)
}
func createFirstHandler(t *testing.T, handlerCalled chan<- bool, wg *sync.WaitGroup) func(context.Context, jetstream.Msg) error {
t.Helper()
return func(context.Context, jetstream.Msg) error {
t.Log("First handler called")
handlerCalled <- true
wg.Done()
return nil
}
}
func createErrorHandler(t *testing.T, handlerCalled chan<- bool, wg *sync.WaitGroup) func(context.Context, jetstream.Msg) error {
t.Helper()
return func(context.Context, jetstream.Msg) error {
t.Log("Error handler called")
handlerCalled <- true
t.Logf("Error handling message: %v", errHandlerError)
t.Logf("Error processing message: %v", errHandlerError)
wg.Done()
return errHandlerError
}
}
func assertHandlerCalled(t *testing.T, handlerCalled <-chan bool, handlerName string) {
t.Helper()
select {
case <-handlerCalled:
t.Logf("%s was called successfully", handlerName)
case <-time.After(time.Second * 5):
t.Fatalf("%s was not called within the expected time", handlerName)
}
}
func waitForHandlersToComplete(t *testing.T, wg *sync.WaitGroup) {
t.Helper()
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
// All handlers completed
case <-time.After(5 * time.Second):
t.Fatal("Handlers did not complete within the expected time")
}
}
type testMocks struct {
connManager *MockConnectionManagerInterface
jetStream *MockJetStream
consumer1 *MockConsumer
messageBatch1 *MockMessageBatch
messageChan1 chan jetstream.Msg
msg1 *MockMsg
consumer2 *MockConsumer
messageBatch2 *MockMessageBatch
messageChan2 chan jetstream.Msg
msg2 *MockMsg
subManager *MockSubscriptionManagerInterface
}
func TestClient_CreateStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
connManager: mockConnManager,
streamManager: mockStreamManager,
}
cfg := StreamConfig{
Stream: "test-stream",
Subjects: []string{"test-subject"},
}
mockConnManager.EXPECT().IsConnected().Return(true)
mockStreamManager.EXPECT().CreateStream(gomock.Any(), &cfg).Return(nil)
err := client.CreateStream(t.Context(), &cfg)
require.NoError(t, err)
}
func TestClient_DeleteStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
client := &Client{
connManager: mockConnManager,
streamManager: mockStreamManager,
}
mockConnManager.EXPECT().IsConnected().Return(true)
mockStreamManager.EXPECT().DeleteStream(gomock.Any(), "test-stream").Return(nil)
err := client.DeleteStream(t.Context(), "test-stream")
require.NoError(t, err)
}
func TestClient_CreateOrUpdateStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStreamManager := NewMockStreamManagerInterface(ctrl)
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockStream := NewMockStream(ctrl)
client := &Client{
connManager: mockConnManager,
streamManager: mockStreamManager,
}
cfg := jetstream.StreamConfig{
Name: "test-stream",
Subjects: []string{"test-subject"},
}
mockConnManager.EXPECT().IsConnected().Return(true)
mockStreamManager.EXPECT().CreateOrUpdateStream(gomock.Any(), &cfg).Return(mockStream, nil)
stream, err := client.CreateOrUpdateStream(t.Context(), &cfg)
require.NoError(t, err)
assert.Equal(t, mockStream, stream)
}
func Test_checkClient(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnectionManagerInterface(ctrl)
tests := []struct {
name string
client *Client
mockCall *gomock.Call
expErr error
}{
{name: "nil client", client: nil, expErr: errClientNotConnected},
{name: "nil connManager", client: &Client{connManager: nil}, expErr: errClientNotConnected},
{name: "not connected to NATS server", client: &Client{connManager: mockConn},
mockCall: mockConn.EXPECT().IsConnected().Return(false), expErr: errClientNotConnected},
{name: "valid client", client: &Client{connManager: mockConn},
mockCall: mockConn.EXPECT().IsConnected().Return(true), expErr: nil},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkClient(tt.client)
assert.Equalf(t, tt.expErr, err, "Test[%d] failed - %s", i, tt.name)
})
}
}
func TestClient_retryConnect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockNATSConnector := NewMockNATSConnector(ctrl)
mockJSCreator := NewMockJetStreamCreator(ctrl)
mockConn := NewMockConnInterface(ctrl)
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
subs := make(map[string]context.CancelFunc)
cfg := Config{Server: "nats://localhost:4222",
Stream: StreamConfig{Stream: "test_stream", Subjects: []string{"test_subject"}},
Consumer: "test_consumer",
}
tests := []struct {
name string
setupMocks func(*Client, *MockNATSConnector, *MockJetStreamCreator, *MockConnInterface, *MockJetStream)
connSuccess bool
}{
{
name: "successful connection on first attempt",
setupMocks: func(client *Client, mockNATSConnector *MockNATSConnector, mockJSCreator *MockJetStreamCreator,
mockConn *MockConnInterface, mockJS *MockJetStream) {
gomock.InOrder(
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).
Return(mockConn, nil).MaxTimes(1),
mockJSCreator.EXPECT().New(mockConn).Return(mockJS, nil).MaxTimes(1),
)
},
connSuccess: true,
},
{
name: "successful connection after retries",
setupMocks: func(client *Client, mockNATSConnector *MockNATSConnector, mockJSCreator *MockJetStreamCreator,
mockConn *MockConnInterface, mockJS *MockJetStream) {
gomock.InOrder(
// First attempt fails
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).
Return(nil, errConnectionError).MaxTimes(1),
// Second attempt succeeds
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).
Return(mockConn, nil).MaxTimes(1),
mockJSCreator.EXPECT().New(mockConn).
Return(mockJS, nil),
)
},
},
{
name: "JetStream creation fails after successful connection",
setupMocks: func(client *Client, mockNATSConnector *MockNATSConnector, mockJSCreator *MockJetStreamCreator,
mockConn *MockConnInterface, mockJS *MockJetStream) {
gomock.InOrder(
// Connection succeeds but JetStream creation fails
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).Return(mockConn, nil),
mockJSCreator.EXPECT().New(mockConn).Return(nil, errConnectionError),
mockConn.EXPECT().Close(),
// Retry succeeds
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).Return(mockConn, nil),
mockJSCreator.EXPECT().New(mockConn).Return(mockJS, nil),
)
},
},
}
for _, tt := range tests {
client := &Client{
Config: &cfg,
logger: logger,
natsConnector: mockNATSConnector,
jetStreamCreator: mockJSCreator,
subscriptions: subs,
}
tt.setupMocks(client, mockNATSConnector, mockJSCreator, mockConn, mockJS)
client.retryConnect()
time.Sleep(500 * time.Millisecond)
if tt.connSuccess {
assert.NotNil(t, client.connManager)
assert.NotNil(t, client.streamManager)
assert.NotNil(t, client.subManager)
}
}
}
func TestClient_GetJetStreamStatus(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
jStream := NewMockJetStream(ctrl)
tests := []struct {
name string
mockCall *gomock.Call
want string
wantErr error
}{
{name: "status OK", want: jetStreamStatusOK,
mockCall: jStream.EXPECT().AccountInfo(gomock.Any()).Return(nil, nil)},
{name: "error in jetstream", want: jetStreamStatusError, wantErr: errJetStream,
mockCall: jStream.EXPECT().AccountInfo(gomock.Any()).Return(nil, errJetStream)},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := t.Context()
got, err := GetJetStreamStatus(ctx, jStream)
assert.Equalf(t, tt.wantErr, err, "Test[%d] failed- %s", i, tt.name)
assert.Equalf(t, tt.want, got, "Test[%d] failed- %s", i, tt.name)
})
}
}
func TestClient_establishConnection(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockNATSConnector := NewMockNATSConnector(ctrl)
mockJSCreator := NewMockJetStreamCreator(ctrl)
mockConn := NewMockConnInterface(ctrl)
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
cfg := Config{
Server: "nats://localhost:4222",
Stream: StreamConfig{
Stream: "test_stream",
Subjects: []string{"test_subject"},
},
Consumer: "test_consumer",
}
tests := []struct {
name string
client *Client
setupMocks func(*Client, *MockNATSConnector, *MockJetStreamCreator, *MockConnInterface, *MockJetStream)
wantErr error
}{
{
name: "successful connection",
setupMocks: func(client *Client, mockNATSConnector *MockNATSConnector, _ *MockJetStreamCreator,
_ *MockConnInterface, mockJS *MockJetStream) {
gomock.InOrder(
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).
Return(mockConn, nil),
mockJSCreator.EXPECT().New(mockConn).Return(mockJS, nil),
)
},
wantErr: nil,
},
{
name: "connection failure",
setupMocks: func(client *Client, mockNATSConnector *MockNATSConnector, _ *MockJetStreamCreator,
_ *MockConnInterface, _ *MockJetStream) {
mockNATSConnector.EXPECT().Connect(client.Config.Server, gomock.Any()).
Return(nil, errConnectionError)
},
wantErr: errConnectionError,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &Client{
Config: &cfg,
logger: logger,
natsConnector: mockNATSConnector,
jetStreamCreator: mockJSCreator,
}
tt.setupMocks(client, mockNATSConnector, mockJSCreator, mockConn, mockJS)
err := client.establishConnection()
assert.Equalf(t, tt.wantErr, err, "Test[%d] failed - %s", i, tt.name)
})
}
}
func TestClient_Query_Success(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockJetStream := NewMockJetStream(ctrl)
mockConsumer := NewMockConsumer(ctrl)
mockMessageBatch := NewMockMessageBatch(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := &Client{
connManager: mockConnManager,
logger: mockLogger,
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
},
MaxWait: 5 * time.Second,
},
}
query := "test-subject"
expectedMessages := []byte("message1\nmessage2")
// Mock expectations
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(mockJetStream, nil)
mockJetStream.EXPECT().CreateOrUpdateConsumer(gomock.Any(),
"test-stream", gomock.Any()).Return(mockConsumer, nil)
mockConsumer.EXPECT().CachedInfo().AnyTimes().Return(&jetstream.ConsumerInfo{
Config: jetstream.ConsumerConfig{FilterSubject: "test-subject"}})
mockConsumer.EXPECT().Info(gomock.Any()).Return(&jetstream.ConsumerInfo{Name: "test-stream"}, nil)
mockJetStream.EXPECT().DeleteConsumer(gomock.Any(), "test-stream", gomock.Any()).Return(nil)
mockConsumer.EXPECT().Fetch(2, gomock.Any()).Return(mockMessageBatch, nil).Times(1)
// Mock message channel
messageChannel := make(chan jetstream.Msg, 2)
messageChannel <- newMockMessage("message1")
messageChannel <- newMockMessage("message2")
close(messageChannel)
mockMessageBatch.EXPECT().Messages().Return(messageChannel)
// Call the Query method
result, err := client.Query(t.Context(), query, 2*time.Second, 2)
// Assertions
require.NoError(t, err)
require.Equal(t, expectedMessages, result)
}
// Helper function to create a mock message.
func newMockMessage(data string) jetstream.Msg {
ctrl := gomock.NewController(nil)
mockMsg := NewMockMsg(ctrl)
mockMsg.EXPECT().Data().Return([]byte(data)).AnyTimes()
mockMsg.EXPECT().Ack().Return(nil).AnyTimes()
return mockMsg
}
func TestClient_Query_EmptyQuery(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockConnManager.EXPECT().IsConnected().Return(true).AnyTimes()
client := &Client{
connManager: mockConnManager,
logger: logging.NewMockLogger(logging.DEBUG),
}
query := ""
result, err := client.Query(t.Context(), query)
require.Error(t, err)
require.Nil(t, result)
require.Equal(t, errEmptySubject, err)
}
func TestClient_Query_ClientNotConnected(t *testing.T) {
client := &Client{
logger: logging.NewMockLogger(logging.DEBUG),
}
query := "test-subject"
result, err := client.Query(t.Context(), query)
require.Error(t, err)
require.Nil(t, result)
require.Equal(t, errClientNotConnected, err)
}
func TestClient_Query_JetStreamError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := &Client{
connManager: mockConnManager,
logger: mockLogger,
}
query := "test-subject"
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(nil, errJetStream)
result, err := client.Query(t.Context(), query)
require.Error(t, err)
require.Nil(t, result)
require.Contains(t, err.Error(), errJetStream.Error())
}
func TestClient_Query_ConsumerCreationError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockJetStream := NewMockJetStream(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := &Client{
connManager: mockConnManager,
logger: mockLogger,
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
},
},
}
query := "test-subject"
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(mockJetStream, nil)
mockJetStream.EXPECT().CreateOrUpdateConsumer(gomock.Any(), "test-stream", gomock.Any()).
Return(nil, errConsumerCreationError)
result, err := client.Query(t.Context(), query)
require.Error(t, err)
require.Nil(t, result)
require.Contains(t, err.Error(), errConsumerCreationError.Error())
}
func TestClient_Query_MessageFetchError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockJetStream := NewMockJetStream(ctrl)
mockConsumer := NewMockConsumer(ctrl)
mockMessageBatch := NewMockMessageBatch(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := &Client{
connManager: mockConnManager,
logger: mockLogger,
Config: &Config{
Stream: StreamConfig{
Stream: "test-stream",
},
MaxWait: 5 * time.Second,
},
}
query := "test-subject"
// Mock expectations
mockConnManager.EXPECT().IsConnected().Return(true)
mockConnManager.EXPECT().JetStream().Return(mockJetStream, nil)
mockJetStream.EXPECT().CreateOrUpdateConsumer(gomock.Any(),
"test-stream", gomock.Any()).Return(mockConsumer, nil)
mockConsumer.EXPECT().Fetch(gomock.Any(), gomock.Any()).Return(mockMessageBatch, errHandlerError)
mockJetStream.EXPECT().DeleteConsumer(gomock.Any(), "test-stream", gomock.Any()).Return(nil)
mockConsumer.EXPECT().Info(gomock.Any()).Return(&jetstream.ConsumerInfo{Name: "test-stream"}, nil)
// Call the Query method
result, err := client.Query(t.Context(), query)
// Assertions
require.Error(t, err)
require.Nil(t, result)
require.Contains(t, err.Error(), errHandlerError.Error())
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/committer.go
================================================
package nats
import (
"log"
"github.com/nats-io/nats.go/jetstream"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
// natsCommitter implements the pubsub.Committer interface for Client messages.
type natsCommitter struct {
msg jetstream.Msg
span trace.Span
}
// Commit commits the message and ends the subscribe span.
func (c *natsCommitter) Commit() {
defer c.span.End()
if err := c.msg.Ack(); err != nil {
c.span.RecordError(err)
c.span.SetStatus(codes.Error, "ack failed")
log.Println("Error committing message:", err)
// nak the message
if err := c.msg.Nak(); err != nil {
c.span.RecordError(err)
log.Println("Error naking message:", err)
}
return
}
}
// Nak naks the message and ends the subscribe span.
func (c *natsCommitter) Nak() error {
defer c.span.End()
err := c.msg.Nak()
if err != nil {
c.span.RecordError(err)
c.span.SetStatus(codes.Error, "nak failed")
}
return err
}
// Rollback rolls back the message and ends the subscribe span.
func (c *natsCommitter) Rollback() error {
defer c.span.End()
err := c.msg.Nak()
if err != nil {
c.span.RecordError(err)
c.span.SetStatus(codes.Error, "rollback failed")
}
return err
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/committer_test.go
================================================
package nats
import (
"context"
"testing"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel/trace/noop"
"go.uber.org/mock/gomock"
)
// createTestCommitter is a helper function for tests to create a natsCommitter.
func createTestCommitter(msg jetstream.Msg) *natsCommitter {
_, span := noop.NewTracerProvider().Tracer("test").Start(context.Background(), "test")
return &natsCommitter{msg: msg, span: span}
}
func TestNATSCommitter_Commit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
committer := createTestCommitter(mockMsg)
t.Run("Successful Commit", func(_ *testing.T) {
mockMsg.EXPECT().Ack().Return(nil)
committer.Commit()
})
t.Run("Failed Commit with Successful Nak", func(_ *testing.T) {
mockMsg.EXPECT().Ack().Return(assert.AnError)
mockMsg.EXPECT().Nak().Return(nil)
committer.Commit()
})
t.Run("Failed Commit with Failed Nak", func(_ *testing.T) {
mockMsg.EXPECT().Ack().Return(assert.AnError)
mockMsg.EXPECT().Nak().Return(assert.AnError)
committer.Commit()
})
}
func TestNATSCommitter_Nak(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
committer := createTestCommitter(mockMsg)
t.Run("Successful Nak", func(t *testing.T) {
mockMsg.EXPECT().Nak().Return(nil)
err := committer.Nak()
assert.NoError(t, err)
})
t.Run("Failed Nak", func(t *testing.T) {
mockMsg.EXPECT().Nak().Return(assert.AnError)
err := committer.Nak()
assert.Error(t, err)
})
}
func TestNATSCommitter_Rollback(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
committer := createTestCommitter(mockMsg)
t.Run("Successful Rollback", func(t *testing.T) {
mockMsg.EXPECT().Nak().Return(nil)
err := committer.Rollback()
assert.NoError(t, err)
})
t.Run("Failed Rollback", func(t *testing.T) {
mockMsg.EXPECT().Nak().Return(assert.AnError)
err := committer.Rollback()
assert.Error(t, err)
})
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/config.go
================================================
package nats
import (
"time"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
const batchSize = 100
// Config defines the Client configuration.
type Config struct {
Server string
CredsFile string
Stream StreamConfig
Consumer string
MaxWait time.Duration
MaxPullWait int
}
// StreamConfig holds stream settings for NATS jStream.
type StreamConfig struct {
Stream string
Subjects []string
MaxDeliver int
MaxWait time.Duration
MaxBytes int64
Storage string
Retention string
MaxAge time.Duration
}
// New creates a new Client.
func New(cfg *Config, logger pubsub.Logger) *PubSubWrapper {
if cfg == nil {
cfg = &Config{}
}
client := &Client{
Config: cfg,
subManager: newSubscriptionManager(batchSize),
logger: logger,
}
return &PubSubWrapper{Client: client}
}
// validateConfigs validates the configuration for NATS jStream.
func validateConfigs(conf *Config) error {
if conf.Server == "" {
return errServerNotProvided
}
if len(conf.Stream.Subjects) == 0 {
return errSubjectsNotProvided
}
if conf.Consumer == "" {
return errConsumerNotProvided
}
return nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/connection_manager.go
================================================
package nats
import (
"context"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
//go:generate mockgen -destination=mock_jetstream.go -package=nats github.com/nats-io/nats.go/jetstream jStream,Stream,Consumer,Msg,MessageBatch
type ConnectionManager struct {
conn ConnInterface
jStream jetstream.JetStream
config *Config
logger pubsub.Logger
natsConnector Connector
jetStreamCreator JetStreamCreator
}
func (cm *ConnectionManager) jetStream() (jetstream.JetStream, error) {
if cm.jStream == nil {
return nil, errJetStreamNotConfigured
}
return cm.jStream, nil
}
// natsConnWrapper wraps a nats.Conn to implement the ConnInterface.
type natsConnWrapper struct {
conn *nats.Conn
}
func (w *natsConnWrapper) Status() nats.Status {
return w.conn.Status()
}
func (w *natsConnWrapper) Close() {
w.conn.Close()
}
func (w *natsConnWrapper) NATSConn() *nats.Conn {
return w.conn
}
func (w *natsConnWrapper) JetStream() (jetstream.JetStream, error) {
return jetstream.New(w.conn)
}
// NewConnectionManager creates a new ConnectionManager.
func NewConnectionManager(
cfg *Config,
logger pubsub.Logger,
natsConnector Connector,
jetStreamCreator JetStreamCreator) *ConnectionManager {
// if logger is nil panic
if logger == nil {
panic("logger is required")
}
if natsConnector == nil {
natsConnector = &defaultConnector{}
}
if jetStreamCreator == nil {
jetStreamCreator = &DefaultJetStreamCreator{}
}
return &ConnectionManager{
config: cfg,
logger: logger,
natsConnector: natsConnector,
jetStreamCreator: jetStreamCreator,
}
}
// Connect establishes a connection to NATS and sets up JetStream.
func (cm *ConnectionManager) Connect() error {
opts := []nats.Option{nats.Name("GoFr NATS JetStreamClient")}
if cm.config.CredsFile != "" {
opts = append(opts, nats.UserCredentials(cm.config.CredsFile))
}
connInterface, err := cm.natsConnector.Connect(cm.config.Server, opts...)
if err != nil {
cm.logger.Debugf("Failed to connect to NATS server at %v: %v", cm.config.Server, err)
return err
}
js, err := cm.jetStreamCreator.New(connInterface)
if err != nil {
connInterface.Close()
cm.logger.Debugf("Failed to create jStream context: %v", err)
return err
}
cm.conn = connInterface
cm.jStream = js
cm.logger.Logf("Successfully connected to NATS server at %v", cm.config.Server)
return nil
}
func (cm *ConnectionManager) Close(_ context.Context) {
if cm.conn != nil {
cm.conn.Close()
}
}
func (cm *ConnectionManager) Publish(ctx context.Context, subject string, message []byte, metrics Metrics) error {
metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "subject", subject)
if !cm.isConnected() {
return errClientNotConnected
}
if err := cm.validateJetStream(subject); err != nil {
return err
}
ctx, span, headers := startPublishSpan(ctx, otel.GetTracerProvider().Tracer(tracerName), subject)
defer span.End()
msg := &nats.Msg{
Subject: subject,
Data: message,
Header: headers,
}
_, err := cm.jStream.PublishMsg(ctx, msg)
if err != nil {
cm.logger.Errorf("failed to publish message to NATS jStream: %v", err)
return err
}
metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "subject", subject)
return nil
}
func (cm *ConnectionManager) validateJetStream(subject string) error {
if cm.jStream == nil || subject == "" {
err := errJetStreamNotConfigured
cm.logger.Error(err.Error())
return err
}
return nil
}
func (cm *ConnectionManager) Health() datasource.Health {
if cm.conn == nil {
return datasource.Health{
Status: datasource.StatusDown,
}
}
status := cm.conn.Status()
if status == nats.CONNECTED {
return datasource.Health{
Status: datasource.StatusUp,
Details: map[string]any{
"server": cm.config.Server,
},
}
}
return datasource.Health{
Status: datasource.StatusDown,
Details: map[string]any{
"server": cm.config.Server,
},
}
}
func (cm *ConnectionManager) isConnected() bool {
if cm.conn == nil {
return false
}
return cm.conn.Status() == nats.CONNECTED
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/connection_manager_test.go
================================================
package nats
import (
"bytes"
"fmt"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/logging"
)
// natsMsgMatcher is a gomock matcher that validates nats.Msg fields.
type natsMsgMatcher struct {
subject string
data []byte
}
func (m natsMsgMatcher) Matches(x any) bool {
msg, ok := x.(*nats.Msg)
return ok && msg.Subject == m.subject && bytes.Equal(msg.Data, m.data)
}
func (m natsMsgMatcher) String() string {
return fmt.Sprintf("nats.Msg{Subject: %q, Data: %q}", m.subject, m.data)
}
func TestNewConnectionManager(t *testing.T) {
cfg := &Config{Server: "nats://localhost:4222"}
logger := logging.NewMockLogger(logging.DEBUG)
natsConnector := &MockNATSConnector{}
jsCreator := &MockJetStreamCreator{}
cm := NewConnectionManager(cfg, logger, natsConnector, jsCreator)
assert.NotNil(t, cm)
assert.Equal(t, cfg, cm.config)
assert.Equal(t, logger, cm.logger)
assert.Equal(t, natsConnector, cm.natsConnector)
assert.Equal(t, jsCreator, cm.jetStreamCreator)
}
func TestConnectionManager_Connect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnInterface(ctrl)
mockJS := NewMockJetStream(ctrl)
mockNATSConnector := NewMockNATSConnector(ctrl)
mockJSCreator := NewMockJetStreamCreator(ctrl)
cm := NewConnectionManager(
&Config{Server: "nats://localhost:4222"},
logging.NewMockLogger(logging.DEBUG),
mockNATSConnector,
mockJSCreator,
)
mockNATSConnector.EXPECT().Connect(gomock.Any(), gomock.Any()).Return(mockConn, nil)
// We don't need to expect NATSConn() call anymore, as we're passing mockConn directly to New()
mockJSCreator.EXPECT().New(mockConn).Return(mockJS, nil)
err := cm.Connect()
time.Sleep(100 * time.Millisecond)
require.NoError(t, err)
assert.Equal(t, mockConn, cm.conn)
assert.Equal(t, mockJS, cm.jStream)
}
func TestConnectionManager_Close(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnInterface(ctrl)
cm := &ConnectionManager{
conn: mockConn,
}
mockConn.EXPECT().Close()
ctx := t.Context()
cm.Close(ctx)
}
func TestConnectionManager_Publish(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockConn := NewMockConnInterface(ctrl)
cm := &ConnectionManager{
conn: mockConn,
jStream: mockJS,
logger: logging.NewMockLogger(logging.DEBUG),
}
ctx := t.Context()
subject := "test.subject"
message := []byte("test message")
gomock.InOrder(
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_total_count", "subject", subject),
mockConn.EXPECT().Status().Return(nats.CONNECTED),
mockJS.EXPECT().PublishMsg(gomock.Any(), natsMsgMatcher{subject: subject, data: message}).Return(&jetstream.PubAck{}, nil),
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_publish_success_count", "subject", subject),
)
err := cm.Publish(ctx, subject, message, mockMetrics)
require.NoError(t, err)
}
func TestConnectionManager_validateJetStream(t *testing.T) {
cm := &ConnectionManager{
jStream: NewMockJetStream(gomock.NewController(t)),
logger: logging.NewMockLogger(logging.DEBUG),
}
err := cm.validateJetStream("test.subject")
require.NoError(t, err)
cm.jStream = nil
err = cm.validateJetStream("test.subject")
assert.Equal(t, errJetStreamNotConfigured, err)
cm.jStream = NewMockJetStream(gomock.NewController(t))
err = cm.validateJetStream("")
assert.Equal(t, errJetStreamNotConfigured, err)
}
func TestConnectionManager_Health(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConn := NewMockConnInterface(ctrl)
cm := &ConnectionManager{
conn: mockConn,
config: &Config{
Server: "nats://localhost:4222",
},
}
mockConn.EXPECT().Status().Return(nats.CONNECTED)
health := cm.Health()
assert.Equal(t, datasource.StatusUp, health.Status)
assert.Equal(t, "nats://localhost:4222", health.Details["server"])
mockConn.EXPECT().Status().Return(nats.CLOSED)
health = cm.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Equal(t, "nats://localhost:4222", health.Details["server"])
cm.conn = nil
health = cm.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
}
func TestConnectionManager_JetStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
cm := &ConnectionManager{
jStream: mockJS,
}
js, err := cm.jetStream()
require.NoError(t, err)
assert.Equal(t, mockJS, js)
}
func TestConnectionManager_JetStream_Nil(t *testing.T) {
cm := &ConnectionManager{
jStream: nil,
}
js, err := cm.jetStream()
require.Error(t, err)
assert.Nil(t, js)
assert.EqualError(t, err, "jStream is not configured")
}
func TestNatsConnWrapper_Status(t *testing.T) {
mockConn := &nats.Conn{}
wrapper := &natsConnWrapper{mockConn}
assert.Equal(t, mockConn.Status(), wrapper.Status())
}
func TestNatsConnWrapper_Close(t *testing.T) {
// Start a NATS server
ns, url := startNATSServer(t)
defer ns.Shutdown()
// Create a real NATS connection
nc, err := nats.Connect(url)
require.NoError(t, err, "Failed to connect to NATS")
// Create the wrapper with the real connection
wrapper := &natsConnWrapper{conn: nc}
// Check initial status
assert.Equal(t, nats.CONNECTED, wrapper.Status(), "Initial status should be CONNECTED")
// Close the connection
wrapper.Close()
// Check final status
assert.Equal(t, nats.CLOSED, wrapper.Status(), "Final status should be CLOSED")
}
// startNATSServer starts a NATS server and returns the server instance and the client URL.
func startNATSServer(t *testing.T) (s *server.Server, u string) {
t.Helper()
opts := &server.Options{
Host: "127.0.0.1",
Port: -1, // Random available port
}
ns, err := server.NewServer(opts)
require.NoError(t, err, "Failed to create NATS server")
go ns.Start()
if !ns.ReadyForConnections(10 * time.Second) {
t.Fatal("NATS server not ready for connections")
}
u = ns.ClientURL()
return ns, u
}
func TestNatsConnWrapper_NatsConn(t *testing.T) {
mockConn := &nats.Conn{}
wrapper := &natsConnWrapper{mockConn}
assert.Equal(t, mockConn, wrapper.NATSConn())
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/connectors.go
================================================
// Package nats connector.go
package nats
import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
)
type defaultConnector struct{}
func (*defaultConnector) Connect(serverURL string, opts ...nats.Option) (ConnInterface, error) {
nc, err := nats.Connect(serverURL, opts...)
if err != nil {
return nil, err
}
return &natsConnWrapper{nc}, nil
}
type DefaultJetStreamCreator struct{}
func (*DefaultJetStreamCreator) New(conn ConnInterface) (jetstream.JetStream, error) {
return conn.JetStream()
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/connectors_test.go
================================================
package nats
import (
"testing"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestDefaultNATSConnector_Connect(t *testing.T) {
// Start a NATS server
ns, url := startNATSServer(t)
defer ns.Shutdown()
connector := &defaultConnector{}
// Test successful connection
conn, err := connector.Connect(url)
require.NoError(t, err)
assert.NotNil(t, conn)
assert.Implements(t, (*ConnInterface)(nil), conn)
// Close the connection
conn.Close()
// Test connection failure
_, err = connector.Connect("nats://invalid-url:4222")
assert.Error(t, err)
}
func TestDefaultJetStreamCreator_New(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
t.Run("Successful jStream creation", func(t *testing.T) {
// Start a NATS server
ns, url := startNATSServer(t)
defer ns.Shutdown()
// Create a real NATS connection
nc, err := nats.Connect(url)
require.NoError(t, err)
defer nc.Close()
// Wrap the real connection
wrapper := &natsConnWrapper{conn: nc}
creator := &DefaultJetStreamCreator{}
// Test successful jStream creation
js, err := creator.New(wrapper)
require.NoError(t, err)
assert.NotNil(t, js)
})
t.Run("jStream creation failure", func(t *testing.T) {
// Create a mock NATS connection
mockConn := NewMockConnInterface(ctrl)
// Mock the jStream method to return an error
expectedError := errJetStreamCreationFailed
mockConn.EXPECT().JetStream().Return(nil, expectedError)
creator := &DefaultJetStreamCreator{}
// Test jStream creation failure
js, err := creator.New(mockConn)
require.Error(t, err)
assert.Nil(t, js)
assert.Equal(t, expectedError, err)
})
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/errors.go
================================================
package nats
import "errors"
var (
// Client Errors.
errServerNotProvided = errors.New("client server address not provided")
errSubjectsNotProvided = errors.New("subjects not provided")
errConsumerNotProvided = errors.New("consumer name not provided")
errConsumerCreationError = errors.New("consumer creation error")
errFailedToDeleteStream = errors.New("failed to delete stream")
errPublishError = errors.New("publish error")
errJetStreamNotConfigured = errors.New("jStream is not configured")
errJetStreamCreationFailed = errors.New("jStream creation failed")
errJetStream = errors.New("jStream error")
errCreateStream = errors.New("create stream error")
errDeleteStream = errors.New("delete stream error")
errGetStream = errors.New("get stream error")
errCreateOrUpdateStream = errors.New("create or update stream error")
errHandlerError = errors.New("handler error")
errConnectionError = errors.New("connection error")
errSubscriptionError = errors.New("subscription error")
)
================================================
FILE: pkg/gofr/datasource/pubsub/nats/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/pubsub/nats
go 1.25.0
require (
github.com/nats-io/nats-server/v2 v2.11.12
github.com/nats-io/nats.go v1.48.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
gofr.dev v1.55.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/nats-io/jwt/v2 v2.8.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/pubsub/nats/go.sum
================================================
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM=
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.11.12 h1:jGDXTkcjqQ5fCRstwIxvv1K0RHfftFUoSCT/iIZcqOc=
github.com/nats-io/nats-server/v2 v2.11.12/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/pubsub/nats/health.go
================================================
package nats
import (
"context"
"gofr.dev/pkg/gofr/datasource"
)
const (
natsBackend = "Client"
jetStreamStatusOK = "OK"
jetStreamStatusError = "Error"
jetStreamConnected = "CONNECTED"
jetStreamDisconnecting = "DISCONNECTED"
)
// Health checks the health of the NATS connection.
func (c *Client) Health() datasource.Health {
if c.connManager == nil {
return datasource.Health{
Status: datasource.StatusDown,
}
}
health := c.connManager.Health()
health.Details["backend"] = natsBackend
js, err := c.connManager.jetStream()
if err != nil {
health.Details["jetstream_enabled"] = false
health.Details["jetstream_status"] = jetStreamStatusError + ": " + err.Error()
return health
}
// Call AccountInfo() to get jStream status
jetStreamStatus, err := GetJetStreamStatus(context.Background(), js)
if err != nil {
jetStreamStatus = jetStreamStatusError + ": " + err.Error()
}
health.Details["jetstream_enabled"] = true
health.Details["jetstream_status"] = jetStreamStatus
return health
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/health_test.go
================================================
package nats
import (
"testing"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/logging"
)
const (
NATSServer = "nats://localhost:4222"
)
func TestNATSClient_Health(t *testing.T) {
testCases := defineHealthTestCases()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
runHealthTestCase(t, tc)
})
}
}
func defineHealthTestCases() []healthTestCase {
return []healthTestCase{
{
name: "HealthyConnection",
setupMocks: func(mockConnManager *MockConnectionManagerInterface, mockJS *MockJetStream) {
mockConnManager.EXPECT().Health().Return(datasource.Health{
Status: datasource.StatusUp,
Details: map[string]any{
"host": NATSServer,
"connection_status": jetStreamConnected,
},
})
mockConnManager.EXPECT().JetStream().Return(mockJS, nil)
mockJS.EXPECT().AccountInfo(gomock.Any()).Return(&jetstream.AccountInfo{}, nil)
},
expectedStatus: datasource.StatusUp,
expectedDetails: map[string]any{
"host": NATSServer,
"backend": natsBackend,
"connection_status": jetStreamConnected,
"jetstream_enabled": true,
"jetstream_status": jetStreamStatusOK,
},
},
{
name: "DisconnectedStatus",
setupMocks: func(mockConnManager *MockConnectionManagerInterface, _ *MockJetStream) {
mockConnManager.EXPECT().Health().Return(datasource.Health{
Status: datasource.StatusDown,
Details: map[string]any{
"host": NATSServer,
"connection_status": jetStreamDisconnecting,
},
})
mockConnManager.EXPECT().JetStream().Return(nil, errJetStreamNotConfigured)
},
expectedStatus: datasource.StatusDown,
expectedDetails: map[string]any{
"host": NATSServer,
"backend": natsBackend,
"connection_status": jetStreamDisconnecting,
"jetstream_enabled": false,
"jetstream_status": jetStreamStatusError + ": jStream is not configured",
},
},
{
name: "JetStreamError",
setupMocks: func(mockConnManager *MockConnectionManagerInterface, mockJS *MockJetStream) {
mockConnManager.EXPECT().Health().Return(datasource.Health{
Status: datasource.StatusUp,
Details: map[string]any{
"host": NATSServer,
"connection_status": jetStreamConnected,
},
})
mockConnManager.EXPECT().JetStream().Return(mockJS, nil)
mockJS.EXPECT().AccountInfo(gomock.Any()).Return(nil, errJetStream)
},
expectedStatus: datasource.StatusUp,
expectedDetails: map[string]any{
"host": NATSServer,
"backend": natsBackend,
"connection_status": jetStreamConnected,
"jetstream_enabled": true,
"jetstream_status": jetStreamStatusError + ": " + errJetStream.Error(),
},
},
}
}
func runHealthTestCase(t *testing.T, tc healthTestCase) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConnManager := NewMockConnectionManagerInterface(ctrl)
mockJS := NewMockJetStream(ctrl)
tc.setupMocks(mockConnManager, mockJS)
client := &Client{
connManager: mockConnManager,
Config: &Config{Server: NATSServer},
logger: logging.NewMockLogger(logging.DEBUG),
}
h := client.Health()
assert.Equal(t, tc.expectedStatus, h.Status)
assert.Equal(t, tc.expectedDetails, h.Details)
}
type healthTestCase struct {
name string
setupMocks func(*MockConnectionManagerInterface, *MockJetStream)
expectedStatus string
expectedDetails map[string]any
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/interfaces.go
================================================
package nats
import (
"context"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
//go:generate mockgen -destination=mock_client.go -package=nats -source=./interfaces.go Client,Subscription,ConnInterface,ConnectionManagerInterface,SubscriptionManagerInterface,StreamManagerInterface
// ConnInterface represents the main Client connection.
type ConnInterface interface {
Status() nats.Status
Close()
NATSConn() *nats.Conn
JetStream() (jetstream.JetStream, error)
}
// Connector represents the main Client connection.
type Connector interface {
Connect(string, ...nats.Option) (ConnInterface, error)
}
// JetStreamCreator represents the main Client jStream Client.
type JetStreamCreator interface {
New(conn ConnInterface) (jetstream.JetStream, error)
}
// JetStreamClient represents the main Client jStream Client.
type JetStreamClient interface {
Publish(ctx context.Context, subject string, message []byte) error
Subscribe(ctx context.Context, subject string, handler messageHandler) error
Close(ctx context.Context) error
DeleteStream(ctx context.Context, name string) error
CreateStream(ctx context.Context, cfg StreamConfig) error
CreateOrUpdateStream(ctx context.Context, cfg jetstream.StreamConfig) (jetstream.Stream, error)
Health() datasource.Health
}
// ConnectionManagerInterface represents the main Client connection.
type ConnectionManagerInterface interface {
Connect() error
Close(ctx context.Context)
Publish(ctx context.Context, subject string, message []byte, metrics Metrics) error
Health() datasource.Health
jetStream() (jetstream.JetStream, error)
isConnected() bool
}
// SubscriptionManagerInterface represents the main Subscription Manager.
type SubscriptionManagerInterface interface {
Subscribe(
ctx context.Context,
topic string,
js jetstream.JetStream,
cfg *Config,
logger pubsub.Logger,
metrics Metrics) (*pubsub.Message, error)
Close()
}
// StreamManagerInterface represents the main Stream Manager.
type StreamManagerInterface interface {
CreateStream(ctx context.Context, cfg *StreamConfig) error
DeleteStream(ctx context.Context, name string) error
CreateOrUpdateStream(ctx context.Context, cfg *jetstream.StreamConfig) (jetstream.Stream, error)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/message.go
================================================
package nats
import (
"github.com/nats-io/nats.go/jetstream"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
type natsMessage struct {
msg jetstream.Msg
logger pubsub.Logger
}
func newNATSMessage(msg jetstream.Msg, logger pubsub.Logger) *natsMessage {
return &natsMessage{
msg: msg,
logger: logger,
}
}
func (nmsg *natsMessage) Commit() {
if err := nmsg.msg.Ack(); err != nil {
nmsg.logger.Errorf("unable to acknowledge message on Client jStream: %v", err)
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/message_test.go
================================================
package nats
import (
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestNewNATSMessage(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
logger := logging.NewMockLogger(logging.ERROR)
n := newNATSMessage(mockMsg, logger)
assert.NotNil(t, n)
assert.Equal(t, mockMsg, n.msg)
assert.Equal(t, logger, n.logger)
}
func TestNATSMessage_Commit(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
logger := logging.NewMockLogger(logging.ERROR)
n := newNATSMessage(mockMsg, logger)
mockMsg.EXPECT().Ack().Return(nil)
n.Commit()
}
func TestNATSMessage_CommitError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockMsg := NewMockMsg(ctrl)
out := testutil.StderrOutputForFunc(func() {
logger := logging.NewMockLogger(logging.ERROR)
n := newNATSMessage(mockMsg, logger)
mockMsg.EXPECT().Ack().Return(testutil.CustomError{ErrorMessage: "ack error"})
n.Commit()
})
assert.Contains(t, out, "unable to acknowledge message on Client jStream")
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/metrics.go
================================================
package nats
import "context"
//go:generate mockgen -destination=mock_metrics.go -package=nats -source=./metrics.go
// Metrics represents the metrics interface.
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/mock_client.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ./interfaces.go
//
// Generated by this command:
//
// mockgen -destination=mock_client.go -package=nats -source=./interfaces.go Client,Subscription,ConnInterface,ConnectionManagerInterface,SubscriptionManagerInterface,StreamManagerInterface
//
// Package nats is a generated GoMock package.
package nats
import (
context "context"
reflect "reflect"
nats "github.com/nats-io/nats.go"
jetstream "github.com/nats-io/nats.go/jetstream"
gomock "go.uber.org/mock/gomock"
datasource "gofr.dev/pkg/gofr/datasource"
pubsub "gofr.dev/pkg/gofr/datasource/pubsub"
)
// MockConnInterface is a mock of ConnInterface interface.
type MockConnInterface struct {
ctrl *gomock.Controller
recorder *MockConnInterfaceMockRecorder
}
// MockConnInterfaceMockRecorder is the mock recorder for MockConnInterface.
type MockConnInterfaceMockRecorder struct {
mock *MockConnInterface
}
// NewMockConnInterface creates a new mock instance.
func NewMockConnInterface(ctrl *gomock.Controller) *MockConnInterface {
mock := &MockConnInterface{ctrl: ctrl}
mock.recorder = &MockConnInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConnInterface) EXPECT() *MockConnInterfaceMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockConnInterface) Close() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Close")
}
// Close indicates an expected call of Close.
func (mr *MockConnInterfaceMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConnInterface)(nil).Close))
}
// JetStream mocks base method.
func (m *MockConnInterface) JetStream() (jetstream.JetStream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "jStream")
ret0, _ := ret[0].(jetstream.JetStream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// JetStream indicates an expected call of JetStream.
func (mr *MockConnInterfaceMockRecorder) JetStream() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "jStream", reflect.TypeOf((*MockConnInterface)(nil).JetStream))
}
// NATSConn mocks base method.
func (m *MockConnInterface) NATSConn() *nats.Conn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NATSConn")
ret0, _ := ret[0].(*nats.Conn)
return ret0
}
// NATSConn indicates an expected call of NATSConn.
func (mr *MockConnInterfaceMockRecorder) NATSConn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NATSConn", reflect.TypeOf((*MockConnInterface)(nil).NATSConn))
}
// Status mocks base method.
func (m *MockConnInterface) Status() nats.Status {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Status")
ret0, _ := ret[0].(nats.Status)
return ret0
}
// Status indicates an expected call of Status.
func (mr *MockConnInterfaceMockRecorder) Status() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockConnInterface)(nil).Status))
}
// MockNATSConnector is a mock of Connector interface.
type MockNATSConnector struct {
ctrl *gomock.Controller
recorder *MockNATSConnectorMockRecorder
}
// MockNATSConnectorMockRecorder is the mock recorder for MockNATSConnector.
type MockNATSConnectorMockRecorder struct {
mock *MockNATSConnector
}
// NewMockNATSConnector creates a new mock instance.
func NewMockNATSConnector(ctrl *gomock.Controller) *MockNATSConnector {
mock := &MockNATSConnector{ctrl: ctrl}
mock.recorder = &MockNATSConnectorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockNATSConnector) EXPECT() *MockNATSConnectorMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockNATSConnector) Connect(arg0 string, arg1 ...nats.Option) (ConnInterface, error) {
m.ctrl.T.Helper()
varargs := []any{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Connect", varargs...)
ret0, _ := ret[0].(ConnInterface)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Connect indicates an expected call of Connect.
func (mr *MockNATSConnectorMockRecorder) Connect(arg0 any, arg1 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockNATSConnector)(nil).Connect), varargs...)
}
// MockJetStreamCreator is a mock of JetStreamCreator interface.
type MockJetStreamCreator struct {
ctrl *gomock.Controller
recorder *MockJetStreamCreatorMockRecorder
}
// MockJetStreamCreatorMockRecorder is the mock recorder for MockJetStreamCreator.
type MockJetStreamCreatorMockRecorder struct {
mock *MockJetStreamCreator
}
// NewMockJetStreamCreator creates a new mock instance.
func NewMockJetStreamCreator(ctrl *gomock.Controller) *MockJetStreamCreator {
mock := &MockJetStreamCreator{ctrl: ctrl}
mock.recorder = &MockJetStreamCreatorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJetStreamCreator) EXPECT() *MockJetStreamCreatorMockRecorder {
return m.recorder
}
// New mocks base method.
func (m *MockJetStreamCreator) New(conn ConnInterface) (jetstream.JetStream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", conn)
ret0, _ := ret[0].(jetstream.JetStream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New.
func (mr *MockJetStreamCreatorMockRecorder) New(conn any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockJetStreamCreator)(nil).New), conn)
}
// MockJetStreamClient is a mock of JetStreamClient interface.
type MockJetStreamClient struct {
ctrl *gomock.Controller
recorder *MockJetStreamClientMockRecorder
}
// MockJetStreamClientMockRecorder is the mock recorder for MockJetStreamClient.
type MockJetStreamClientMockRecorder struct {
mock *MockJetStreamClient
}
// NewMockJetStreamClient creates a new mock instance.
func NewMockJetStreamClient(ctrl *gomock.Controller) *MockJetStreamClient {
mock := &MockJetStreamClient{ctrl: ctrl}
mock.recorder = &MockJetStreamClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJetStreamClient) EXPECT() *MockJetStreamClientMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockJetStreamClient) Close(ctx context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close", ctx)
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockJetStreamClientMockRecorder) Close(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockJetStreamClient)(nil).Close), ctx)
}
// CreateOrUpdateStream mocks base method.
func (m *MockJetStreamClient) CreateOrUpdateStream(ctx context.Context, cfg jetstream.StreamConfig) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, cfg)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream.
func (mr *MockJetStreamClientMockRecorder) CreateOrUpdateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockJetStreamClient)(nil).CreateOrUpdateStream), ctx, cfg)
}
// CreateStream mocks base method.
func (m *MockJetStreamClient) CreateStream(ctx context.Context, cfg StreamConfig) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateStream", ctx, cfg)
ret0, _ := ret[0].(error)
return ret0
}
// CreateStream indicates an expected call of CreateStream.
func (mr *MockJetStreamClientMockRecorder) CreateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStream", reflect.TypeOf((*MockJetStreamClient)(nil).CreateStream), ctx, cfg)
}
// DeleteStream mocks base method.
func (m *MockJetStreamClient) DeleteStream(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteStream", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteStream indicates an expected call of DeleteStream.
func (mr *MockJetStreamClientMockRecorder) DeleteStream(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStream", reflect.TypeOf((*MockJetStreamClient)(nil).DeleteStream), ctx, name)
}
// Health mocks base method.
func (m *MockJetStreamClient) Health() datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health")
ret0, _ := ret[0].(datasource.Health)
return ret0
}
// Health indicates an expected call of Health.
func (mr *MockJetStreamClientMockRecorder) Health() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockJetStreamClient)(nil).Health))
}
// Publish mocks base method.
func (m *MockJetStreamClient) Publish(ctx context.Context, subject string, message []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, subject, message)
ret0, _ := ret[0].(error)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockJetStreamClientMockRecorder) Publish(ctx, subject, message any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockJetStreamClient)(nil).Publish), ctx, subject, message)
}
// Subscribe mocks base method.
func (m *MockJetStreamClient) Subscribe(ctx context.Context, subject string, handler messageHandler) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscribe", ctx, subject, handler)
ret0, _ := ret[0].(error)
return ret0
}
// Subscribe indicates an expected call of Subscribe.
func (mr *MockJetStreamClientMockRecorder) Subscribe(ctx, subject, handler any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockJetStreamClient)(nil).Subscribe), ctx, subject, handler)
}
// MockConnectionManagerInterface is a mock of ConnectionManagerInterface interface.
type MockConnectionManagerInterface struct {
ctrl *gomock.Controller
recorder *MockConnectionManagerInterfaceMockRecorder
}
// MockConnectionManagerInterfaceMockRecorder is the mock recorder for MockConnectionManagerInterface.
type MockConnectionManagerInterfaceMockRecorder struct {
mock *MockConnectionManagerInterface
}
// NewMockConnectionManagerInterface creates a new mock instance.
func NewMockConnectionManagerInterface(ctrl *gomock.Controller) *MockConnectionManagerInterface {
mock := &MockConnectionManagerInterface{ctrl: ctrl}
mock.recorder = &MockConnectionManagerInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConnectionManagerInterface) EXPECT() *MockConnectionManagerInterfaceMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockConnectionManagerInterface) Close(ctx context.Context) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Close", ctx)
}
// Close indicates an expected call of Close.
func (mr *MockConnectionManagerInterfaceMockRecorder) Close(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConnectionManagerInterface)(nil).Close), ctx)
}
// Connect mocks base method.
func (m *MockConnectionManagerInterface) Connect() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Connect")
ret0, _ := ret[0].(error)
return ret0
}
// Connect indicates an expected call of Connect.
func (mr *MockConnectionManagerInterfaceMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockConnectionManagerInterface)(nil).Connect))
}
// Health mocks base method.
func (m *MockConnectionManagerInterface) Health() datasource.Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health")
ret0, _ := ret[0].(datasource.Health)
return ret0
}
// Health indicates an expected call of Health.
func (mr *MockConnectionManagerInterfaceMockRecorder) Health() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockConnectionManagerInterface)(nil).Health))
}
// IsConnected mocks base method.
func (m *MockConnectionManagerInterface) isConnected() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "isConnected")
ret0, _ := ret[0].(bool)
return ret0
}
// IsConnected indicates an expected call of IsConnected.
func (mr *MockConnectionManagerInterfaceMockRecorder) IsConnected() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "isConnected", reflect.TypeOf((*MockConnectionManagerInterface)(nil).isConnected))
}
// Publish mocks base method.
func (m *MockConnectionManagerInterface) Publish(ctx context.Context, subject string, message []byte, metrics Metrics) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Publish", ctx, subject, message, metrics)
ret0, _ := ret[0].(error)
return ret0
}
// Publish indicates an expected call of Publish.
func (mr *MockConnectionManagerInterfaceMockRecorder) Publish(ctx, subject, message, metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockConnectionManagerInterface)(nil).Publish), ctx, subject, message, metrics)
}
// jetStream mocks base method.
func (m *MockConnectionManagerInterface) JetStream() (jetstream.JetStream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "jetStream")
ret0, _ := ret[0].(jetstream.JetStream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// JetStream mocks base method.
func (m *MockConnectionManagerInterface) jetStream() (jetstream.JetStream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "jStream")
ret0, _ := ret[0].(jetstream.JetStream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
func (mr *MockConnectionManagerInterfaceMockRecorder) JetStream() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "jStream", reflect.TypeOf((*MockConnectionManagerInterface)(nil).jetStream))
}
// MockSubscriptionManagerInterface is a mock of SubscriptionManagerInterface interface.
type MockSubscriptionManagerInterface struct {
ctrl *gomock.Controller
recorder *MockSubscriptionManagerInterfaceMockRecorder
}
// MockSubscriptionManagerInterfaceMockRecorder is the mock recorder for MockSubscriptionManagerInterface.
type MockSubscriptionManagerInterfaceMockRecorder struct {
mock *MockSubscriptionManagerInterface
}
// NewMockSubscriptionManagerInterface creates a new mock instance.
func NewMockSubscriptionManagerInterface(ctrl *gomock.Controller) *MockSubscriptionManagerInterface {
mock := &MockSubscriptionManagerInterface{ctrl: ctrl}
mock.recorder = &MockSubscriptionManagerInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSubscriptionManagerInterface) EXPECT() *MockSubscriptionManagerInterfaceMockRecorder {
return m.recorder
}
// Close mocks base method.
func (m *MockSubscriptionManagerInterface) Close() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Close")
}
// Close indicates an expected call of Close.
func (mr *MockSubscriptionManagerInterfaceMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockSubscriptionManagerInterface)(nil).Close))
}
// Subscribe mocks base method.
func (m *MockSubscriptionManagerInterface) Subscribe(ctx context.Context, topic string, js jetstream.JetStream, cfg *Config, logger pubsub.Logger, metrics Metrics) (*pubsub.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subscribe", ctx, topic, js, cfg, logger, metrics)
ret0, _ := ret[0].(*pubsub.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Subscribe indicates an expected call of Subscribe.
func (mr *MockSubscriptionManagerInterfaceMockRecorder) Subscribe(ctx, topic, js, cfg, logger, metrics any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subscribe", reflect.TypeOf((*MockSubscriptionManagerInterface)(nil).Subscribe), ctx, topic, js, cfg, logger, metrics)
}
// MockStreamManagerInterface is a mock of StreamManagerInterface interface.
type MockStreamManagerInterface struct {
ctrl *gomock.Controller
recorder *MockStreamManagerInterfaceMockRecorder
}
// MockStreamManagerInterfaceMockRecorder is the mock recorder for MockStreamManagerInterface.
type MockStreamManagerInterfaceMockRecorder struct {
mock *MockStreamManagerInterface
}
// NewMockStreamManagerInterface creates a new mock instance.
func NewMockStreamManagerInterface(ctrl *gomock.Controller) *MockStreamManagerInterface {
mock := &MockStreamManagerInterface{ctrl: ctrl}
mock.recorder = &MockStreamManagerInterfaceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStreamManagerInterface) EXPECT() *MockStreamManagerInterfaceMockRecorder {
return m.recorder
}
// CreateOrUpdateStream mocks base method.
func (m *MockStreamManagerInterface) CreateOrUpdateStream(ctx context.Context, cfg *jetstream.StreamConfig) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, cfg)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream.
func (mr *MockStreamManagerInterfaceMockRecorder) CreateOrUpdateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockStreamManagerInterface)(nil).CreateOrUpdateStream), ctx, cfg)
}
// CreateStream mocks base method.
func (m *MockStreamManagerInterface) CreateStream(ctx context.Context, cfg *StreamConfig) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateStream", ctx, cfg)
ret0, _ := ret[0].(error)
return ret0
}
// CreateStream indicates an expected call of CreateStream.
func (mr *MockStreamManagerInterfaceMockRecorder) CreateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStream", reflect.TypeOf((*MockStreamManagerInterface)(nil).CreateStream), ctx, cfg)
}
// DeleteStream mocks base method.
func (m *MockStreamManagerInterface) DeleteStream(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteStream", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteStream indicates an expected call of DeleteStream.
func (mr *MockStreamManagerInterfaceMockRecorder) DeleteStream(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStream", reflect.TypeOf((*MockStreamManagerInterface)(nil).DeleteStream), ctx, name)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/mock_jetstream.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/nats-io/nats.go/jetstream (interfaces: JetStream,Stream,Consumer,Msg,MessageBatch)
//
// Generated by this command:
//
// mockgen -destination=mock_jetstream.go -package=nats github.com/nats-io/nats.go/jetstream JetStream,Stream,Consumer,Msg,MessageBatch
//
// Package nats is a generated GoMock package.
package nats
import (
context "context"
reflect "reflect"
time "time"
nats "github.com/nats-io/nats.go"
jetstream "github.com/nats-io/nats.go/jetstream"
gomock "go.uber.org/mock/gomock"
)
// MockJetStream is a mock of JetStream interface.
type MockJetStream struct {
ctrl *gomock.Controller
recorder *MockJetStreamMockRecorder
isgomock struct{}
}
// MockJetStreamMockRecorder is the mock recorder for MockJetStream.
type MockJetStreamMockRecorder struct {
mock *MockJetStream
}
// NewMockJetStream creates a new mock instance.
func NewMockJetStream(ctrl *gomock.Controller) *MockJetStream {
mock := &MockJetStream{ctrl: ctrl}
mock.recorder = &MockJetStreamMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockJetStream) EXPECT() *MockJetStreamMockRecorder {
return m.recorder
}
// AccountInfo mocks base method.
func (m *MockJetStream) AccountInfo(ctx context.Context) (*jetstream.AccountInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AccountInfo", ctx)
ret0, _ := ret[0].(*jetstream.AccountInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// AccountInfo indicates an expected call of AccountInfo.
func (mr *MockJetStreamMockRecorder) AccountInfo(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AccountInfo", reflect.TypeOf((*MockJetStream)(nil).AccountInfo), ctx)
}
// CleanupPublisher mocks base method.
func (m *MockJetStream) CleanupPublisher() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "CleanupPublisher")
}
// CleanupPublisher indicates an expected call of CleanupPublisher.
func (mr *MockJetStreamMockRecorder) CleanupPublisher() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupPublisher", reflect.TypeOf((*MockJetStream)(nil).CleanupPublisher))
}
// Conn mocks base method.
func (m *MockJetStream) Conn() *nats.Conn {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Conn")
ret0, _ := ret[0].(*nats.Conn)
return ret0
}
// Conn indicates an expected call of Conn.
func (mr *MockJetStreamMockRecorder) Conn() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Conn", reflect.TypeOf((*MockJetStream)(nil).Conn))
}
// Consumer mocks base method.
func (m *MockJetStream) Consumer(ctx context.Context, stream, consumer string) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Consumer", ctx, stream, consumer)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Consumer indicates an expected call of Consumer.
func (mr *MockJetStreamMockRecorder) Consumer(ctx, stream, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Consumer", reflect.TypeOf((*MockJetStream)(nil).Consumer), ctx, stream, consumer)
}
// CreateConsumer mocks base method.
func (m *MockJetStream) CreateConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateConsumer indicates an expected call of CreateConsumer.
func (mr *MockJetStreamMockRecorder) CreateConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateConsumer", reflect.TypeOf((*MockJetStream)(nil).CreateConsumer), ctx, stream, cfg)
}
// CreateKeyValue mocks base method.
func (m *MockJetStream) CreateKeyValue(ctx context.Context, cfg jetstream.KeyValueConfig) (jetstream.KeyValue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateKeyValue", ctx, cfg)
ret0, _ := ret[0].(jetstream.KeyValue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateKeyValue indicates an expected call of CreateKeyValue.
func (mr *MockJetStreamMockRecorder) CreateKeyValue(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateKeyValue", reflect.TypeOf((*MockJetStream)(nil).CreateKeyValue), ctx, cfg)
}
// CreateObjectStore mocks base method.
func (m *MockJetStream) CreateObjectStore(ctx context.Context, cfg jetstream.ObjectStoreConfig) (jetstream.ObjectStore, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateObjectStore", ctx, cfg)
ret0, _ := ret[0].(jetstream.ObjectStore)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateObjectStore indicates an expected call of CreateObjectStore.
func (mr *MockJetStreamMockRecorder) CreateObjectStore(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateObjectStore", reflect.TypeOf((*MockJetStream)(nil).CreateObjectStore), ctx, cfg)
}
// CreateOrUpdateConsumer mocks base method.
func (m *MockJetStream) CreateOrUpdateConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateConsumer indicates an expected call of CreateOrUpdateConsumer.
func (mr *MockJetStreamMockRecorder) CreateOrUpdateConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateConsumer", reflect.TypeOf((*MockJetStream)(nil).CreateOrUpdateConsumer), ctx, stream, cfg)
}
// CreateOrUpdateKeyValue mocks base method.
func (m *MockJetStream) CreateOrUpdateKeyValue(ctx context.Context, cfg jetstream.KeyValueConfig) (jetstream.KeyValue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateKeyValue", ctx, cfg)
ret0, _ := ret[0].(jetstream.KeyValue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateKeyValue indicates an expected call of CreateOrUpdateKeyValue.
func (mr *MockJetStreamMockRecorder) CreateOrUpdateKeyValue(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateKeyValue", reflect.TypeOf((*MockJetStream)(nil).CreateOrUpdateKeyValue), ctx, cfg)
}
// CreateOrUpdateObjectStore mocks base method.
func (m *MockJetStream) CreateOrUpdateObjectStore(ctx context.Context, cfg jetstream.ObjectStoreConfig) (jetstream.ObjectStore, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateObjectStore", ctx, cfg)
ret0, _ := ret[0].(jetstream.ObjectStore)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateObjectStore indicates an expected call of CreateOrUpdateObjectStore.
func (mr *MockJetStreamMockRecorder) CreateOrUpdateObjectStore(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateObjectStore", reflect.TypeOf((*MockJetStream)(nil).CreateOrUpdateObjectStore), ctx, cfg)
}
// CreateOrUpdatePushConsumer mocks base method.
func (m *MockJetStream) CreateOrUpdatePushConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdatePushConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdatePushConsumer indicates an expected call of CreateOrUpdatePushConsumer.
func (mr *MockJetStreamMockRecorder) CreateOrUpdatePushConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdatePushConsumer", reflect.TypeOf((*MockJetStream)(nil).CreateOrUpdatePushConsumer), ctx, stream, cfg)
}
// CreateOrUpdateStream mocks base method.
func (m *MockJetStream) CreateOrUpdateStream(ctx context.Context, cfg jetstream.StreamConfig) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateStream", ctx, cfg)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateStream indicates an expected call of CreateOrUpdateStream.
func (mr *MockJetStreamMockRecorder) CreateOrUpdateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateStream", reflect.TypeOf((*MockJetStream)(nil).CreateOrUpdateStream), ctx, cfg)
}
// CreatePushConsumer mocks base method.
func (m *MockJetStream) CreatePushConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatePushConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreatePushConsumer indicates an expected call of CreatePushConsumer.
func (mr *MockJetStreamMockRecorder) CreatePushConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePushConsumer", reflect.TypeOf((*MockJetStream)(nil).CreatePushConsumer), ctx, stream, cfg)
}
// CreateStream mocks base method.
func (m *MockJetStream) CreateStream(ctx context.Context, cfg jetstream.StreamConfig) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateStream", ctx, cfg)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateStream indicates an expected call of CreateStream.
func (mr *MockJetStreamMockRecorder) CreateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateStream", reflect.TypeOf((*MockJetStream)(nil).CreateStream), ctx, cfg)
}
// DeleteConsumer mocks base method.
func (m *MockJetStream) DeleteConsumer(ctx context.Context, stream, consumer string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteConsumer", ctx, stream, consumer)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteConsumer indicates an expected call of DeleteConsumer.
func (mr *MockJetStreamMockRecorder) DeleteConsumer(ctx, stream, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteConsumer", reflect.TypeOf((*MockJetStream)(nil).DeleteConsumer), ctx, stream, consumer)
}
// DeleteKeyValue mocks base method.
func (m *MockJetStream) DeleteKeyValue(ctx context.Context, bucket string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteKeyValue", ctx, bucket)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteKeyValue indicates an expected call of DeleteKeyValue.
func (mr *MockJetStreamMockRecorder) DeleteKeyValue(ctx, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteKeyValue", reflect.TypeOf((*MockJetStream)(nil).DeleteKeyValue), ctx, bucket)
}
// DeleteObjectStore mocks base method.
func (m *MockJetStream) DeleteObjectStore(ctx context.Context, bucket string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteObjectStore", ctx, bucket)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteObjectStore indicates an expected call of DeleteObjectStore.
func (mr *MockJetStreamMockRecorder) DeleteObjectStore(ctx, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectStore", reflect.TypeOf((*MockJetStream)(nil).DeleteObjectStore), ctx, bucket)
}
// DeleteStream mocks base method.
func (m *MockJetStream) DeleteStream(ctx context.Context, stream string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteStream", ctx, stream)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteStream indicates an expected call of DeleteStream.
func (mr *MockJetStreamMockRecorder) DeleteStream(ctx, stream any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteStream", reflect.TypeOf((*MockJetStream)(nil).DeleteStream), ctx, stream)
}
// KeyValue mocks base method.
func (m *MockJetStream) KeyValue(ctx context.Context, bucket string) (jetstream.KeyValue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyValue", ctx, bucket)
ret0, _ := ret[0].(jetstream.KeyValue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KeyValue indicates an expected call of KeyValue.
func (mr *MockJetStreamMockRecorder) KeyValue(ctx, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyValue", reflect.TypeOf((*MockJetStream)(nil).KeyValue), ctx, bucket)
}
// KeyValueStoreNames mocks base method.
func (m *MockJetStream) KeyValueStoreNames(ctx context.Context) jetstream.KeyValueNamesLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyValueStoreNames", ctx)
ret0, _ := ret[0].(jetstream.KeyValueNamesLister)
return ret0
}
// KeyValueStoreNames indicates an expected call of KeyValueStoreNames.
func (mr *MockJetStreamMockRecorder) KeyValueStoreNames(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyValueStoreNames", reflect.TypeOf((*MockJetStream)(nil).KeyValueStoreNames), ctx)
}
// KeyValueStores mocks base method.
func (m *MockJetStream) KeyValueStores(ctx context.Context) jetstream.KeyValueLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyValueStores", ctx)
ret0, _ := ret[0].(jetstream.KeyValueLister)
return ret0
}
// KeyValueStores indicates an expected call of KeyValueStores.
func (mr *MockJetStreamMockRecorder) KeyValueStores(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyValueStores", reflect.TypeOf((*MockJetStream)(nil).KeyValueStores), ctx)
}
// ListStreams mocks base method.
func (m *MockJetStream) ListStreams(arg0 context.Context, arg1 ...jetstream.StreamListOpt) jetstream.StreamInfoLister {
m.ctrl.T.Helper()
varargs := []any{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ListStreams", varargs...)
ret0, _ := ret[0].(jetstream.StreamInfoLister)
return ret0
}
// ListStreams indicates an expected call of ListStreams.
func (mr *MockJetStreamMockRecorder) ListStreams(arg0 any, arg1 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStreams", reflect.TypeOf((*MockJetStream)(nil).ListStreams), varargs...)
}
// ObjectStore mocks base method.
func (m *MockJetStream) ObjectStore(ctx context.Context, bucket string) (jetstream.ObjectStore, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectStore", ctx, bucket)
ret0, _ := ret[0].(jetstream.ObjectStore)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ObjectStore indicates an expected call of ObjectStore.
func (mr *MockJetStreamMockRecorder) ObjectStore(ctx, bucket any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectStore", reflect.TypeOf((*MockJetStream)(nil).ObjectStore), ctx, bucket)
}
// ObjectStoreNames mocks base method.
func (m *MockJetStream) ObjectStoreNames(ctx context.Context) jetstream.ObjectStoreNamesLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectStoreNames", ctx)
ret0, _ := ret[0].(jetstream.ObjectStoreNamesLister)
return ret0
}
// ObjectStoreNames indicates an expected call of ObjectStoreNames.
func (mr *MockJetStreamMockRecorder) ObjectStoreNames(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectStoreNames", reflect.TypeOf((*MockJetStream)(nil).ObjectStoreNames), ctx)
}
// ObjectStores mocks base method.
func (m *MockJetStream) ObjectStores(ctx context.Context) jetstream.ObjectStoresLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ObjectStores", ctx)
ret0, _ := ret[0].(jetstream.ObjectStoresLister)
return ret0
}
// ObjectStores indicates an expected call of ObjectStores.
func (mr *MockJetStreamMockRecorder) ObjectStores(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ObjectStores", reflect.TypeOf((*MockJetStream)(nil).ObjectStores), ctx)
}
// Options mocks base method.
func (m *MockJetStream) Options() jetstream.JetStreamOptions {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Options")
ret0, _ := ret[0].(jetstream.JetStreamOptions)
return ret0
}
// Options indicates an expected call of Options.
func (mr *MockJetStreamMockRecorder) Options() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Options", reflect.TypeOf((*MockJetStream)(nil).Options))
}
// OrderedConsumer mocks base method.
func (m *MockJetStream) OrderedConsumer(ctx context.Context, stream string, cfg jetstream.OrderedConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OrderedConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OrderedConsumer indicates an expected call of OrderedConsumer.
func (mr *MockJetStreamMockRecorder) OrderedConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderedConsumer", reflect.TypeOf((*MockJetStream)(nil).OrderedConsumer), ctx, stream, cfg)
}
// PauseConsumer mocks base method.
func (m *MockJetStream) PauseConsumer(ctx context.Context, stream, consumer string, pauseUntil time.Time) (*jetstream.ConsumerPauseResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PauseConsumer", ctx, stream, consumer, pauseUntil)
ret0, _ := ret[0].(*jetstream.ConsumerPauseResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PauseConsumer indicates an expected call of PauseConsumer.
func (mr *MockJetStreamMockRecorder) PauseConsumer(ctx, stream, consumer, pauseUntil any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseConsumer", reflect.TypeOf((*MockJetStream)(nil).PauseConsumer), ctx, stream, consumer, pauseUntil)
}
// Publish mocks base method.
func (m *MockJetStream) Publish(ctx context.Context, subject string, payload []byte, opts ...jetstream.PublishOpt) (*jetstream.PubAck, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, subject, payload}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Publish", varargs...)
ret0, _ := ret[0].(*jetstream.PubAck)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Publish indicates an expected call of Publish.
func (mr *MockJetStreamMockRecorder) Publish(ctx, subject, payload any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, subject, payload}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Publish", reflect.TypeOf((*MockJetStream)(nil).Publish), varargs...)
}
// PublishAsync mocks base method.
func (m *MockJetStream) PublishAsync(subject string, payload []byte, opts ...jetstream.PublishOpt) (jetstream.PubAckFuture, error) {
m.ctrl.T.Helper()
varargs := []any{subject, payload}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PublishAsync", varargs...)
ret0, _ := ret[0].(jetstream.PubAckFuture)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishAsync indicates an expected call of PublishAsync.
func (mr *MockJetStreamMockRecorder) PublishAsync(subject, payload any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{subject, payload}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAsync", reflect.TypeOf((*MockJetStream)(nil).PublishAsync), varargs...)
}
// PublishAsyncComplete mocks base method.
func (m *MockJetStream) PublishAsyncComplete() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishAsyncComplete")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// PublishAsyncComplete indicates an expected call of PublishAsyncComplete.
func (mr *MockJetStreamMockRecorder) PublishAsyncComplete() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAsyncComplete", reflect.TypeOf((*MockJetStream)(nil).PublishAsyncComplete))
}
// PublishAsyncPending mocks base method.
func (m *MockJetStream) PublishAsyncPending() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PublishAsyncPending")
ret0, _ := ret[0].(int)
return ret0
}
// PublishAsyncPending indicates an expected call of PublishAsyncPending.
func (mr *MockJetStreamMockRecorder) PublishAsyncPending() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishAsyncPending", reflect.TypeOf((*MockJetStream)(nil).PublishAsyncPending))
}
// PublishMsg mocks base method.
func (m *MockJetStream) PublishMsg(ctx context.Context, msg *nats.Msg, opts ...jetstream.PublishOpt) (*jetstream.PubAck, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, msg}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PublishMsg", varargs...)
ret0, _ := ret[0].(*jetstream.PubAck)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishMsg indicates an expected call of PublishMsg.
func (mr *MockJetStreamMockRecorder) PublishMsg(ctx, msg any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, msg}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishMsg", reflect.TypeOf((*MockJetStream)(nil).PublishMsg), varargs...)
}
// PublishMsgAsync mocks base method.
func (m *MockJetStream) PublishMsgAsync(msg *nats.Msg, opts ...jetstream.PublishOpt) (jetstream.PubAckFuture, error) {
m.ctrl.T.Helper()
varargs := []any{msg}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "PublishMsgAsync", varargs...)
ret0, _ := ret[0].(jetstream.PubAckFuture)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PublishMsgAsync indicates an expected call of PublishMsgAsync.
func (mr *MockJetStreamMockRecorder) PublishMsgAsync(msg any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{msg}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PublishMsgAsync", reflect.TypeOf((*MockJetStream)(nil).PublishMsgAsync), varargs...)
}
// PushConsumer mocks base method.
func (m *MockJetStream) PushConsumer(ctx context.Context, stream, consumer string) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PushConsumer", ctx, stream, consumer)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PushConsumer indicates an expected call of PushConsumer.
func (mr *MockJetStreamMockRecorder) PushConsumer(ctx, stream, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushConsumer", reflect.TypeOf((*MockJetStream)(nil).PushConsumer), ctx, stream, consumer)
}
// ResumeConsumer mocks base method.
func (m *MockJetStream) ResumeConsumer(ctx context.Context, stream, consumer string) (*jetstream.ConsumerPauseResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResumeConsumer", ctx, stream, consumer)
ret0, _ := ret[0].(*jetstream.ConsumerPauseResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResumeConsumer indicates an expected call of ResumeConsumer.
func (mr *MockJetStreamMockRecorder) ResumeConsumer(ctx, stream, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeConsumer", reflect.TypeOf((*MockJetStream)(nil).ResumeConsumer), ctx, stream, consumer)
}
// Stream mocks base method.
func (m *MockJetStream) Stream(ctx context.Context, stream string) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Stream", ctx, stream)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Stream indicates an expected call of Stream.
func (mr *MockJetStreamMockRecorder) Stream(ctx, stream any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stream", reflect.TypeOf((*MockJetStream)(nil).Stream), ctx, stream)
}
// StreamNameBySubject mocks base method.
func (m *MockJetStream) StreamNameBySubject(ctx context.Context, subject string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StreamNameBySubject", ctx, subject)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StreamNameBySubject indicates an expected call of StreamNameBySubject.
func (mr *MockJetStreamMockRecorder) StreamNameBySubject(ctx, subject any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamNameBySubject", reflect.TypeOf((*MockJetStream)(nil).StreamNameBySubject), ctx, subject)
}
// StreamNames mocks base method.
func (m *MockJetStream) StreamNames(arg0 context.Context, arg1 ...jetstream.StreamListOpt) jetstream.StreamNameLister {
m.ctrl.T.Helper()
varargs := []any{arg0}
for _, a := range arg1 {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "StreamNames", varargs...)
ret0, _ := ret[0].(jetstream.StreamNameLister)
return ret0
}
// StreamNames indicates an expected call of StreamNames.
func (mr *MockJetStreamMockRecorder) StreamNames(arg0 any, arg1 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0}, arg1...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamNames", reflect.TypeOf((*MockJetStream)(nil).StreamNames), varargs...)
}
// UpdateConsumer mocks base method.
func (m *MockJetStream) UpdateConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateConsumer indicates an expected call of UpdateConsumer.
func (mr *MockJetStreamMockRecorder) UpdateConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateConsumer", reflect.TypeOf((*MockJetStream)(nil).UpdateConsumer), ctx, stream, cfg)
}
// UpdateKeyValue mocks base method.
func (m *MockJetStream) UpdateKeyValue(ctx context.Context, cfg jetstream.KeyValueConfig) (jetstream.KeyValue, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateKeyValue", ctx, cfg)
ret0, _ := ret[0].(jetstream.KeyValue)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateKeyValue indicates an expected call of UpdateKeyValue.
func (mr *MockJetStreamMockRecorder) UpdateKeyValue(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKeyValue", reflect.TypeOf((*MockJetStream)(nil).UpdateKeyValue), ctx, cfg)
}
// UpdateObjectStore mocks base method.
func (m *MockJetStream) UpdateObjectStore(ctx context.Context, cfg jetstream.ObjectStoreConfig) (jetstream.ObjectStore, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateObjectStore", ctx, cfg)
ret0, _ := ret[0].(jetstream.ObjectStore)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateObjectStore indicates an expected call of UpdateObjectStore.
func (mr *MockJetStreamMockRecorder) UpdateObjectStore(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateObjectStore", reflect.TypeOf((*MockJetStream)(nil).UpdateObjectStore), ctx, cfg)
}
// UpdatePushConsumer mocks base method.
func (m *MockJetStream) UpdatePushConsumer(ctx context.Context, stream string, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePushConsumer", ctx, stream, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdatePushConsumer indicates an expected call of UpdatePushConsumer.
func (mr *MockJetStreamMockRecorder) UpdatePushConsumer(ctx, stream, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePushConsumer", reflect.TypeOf((*MockJetStream)(nil).UpdatePushConsumer), ctx, stream, cfg)
}
// UpdateStream mocks base method.
func (m *MockJetStream) UpdateStream(ctx context.Context, cfg jetstream.StreamConfig) (jetstream.Stream, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateStream", ctx, cfg)
ret0, _ := ret[0].(jetstream.Stream)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateStream indicates an expected call of UpdateStream.
func (mr *MockJetStreamMockRecorder) UpdateStream(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateStream", reflect.TypeOf((*MockJetStream)(nil).UpdateStream), ctx, cfg)
}
// MockStream is a mock of Stream interface.
type MockStream struct {
ctrl *gomock.Controller
recorder *MockStreamMockRecorder
isgomock struct{}
}
// MockStreamMockRecorder is the mock recorder for MockStream.
type MockStreamMockRecorder struct {
mock *MockStream
}
// NewMockStream creates a new mock instance.
func NewMockStream(ctrl *gomock.Controller) *MockStream {
mock := &MockStream{ctrl: ctrl}
mock.recorder = &MockStreamMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockStream) EXPECT() *MockStreamMockRecorder {
return m.recorder
}
// CachedInfo mocks base method.
func (m *MockStream) CachedInfo() *jetstream.StreamInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CachedInfo")
ret0, _ := ret[0].(*jetstream.StreamInfo)
return ret0
}
// CachedInfo indicates an expected call of CachedInfo.
func (mr *MockStreamMockRecorder) CachedInfo() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CachedInfo", reflect.TypeOf((*MockStream)(nil).CachedInfo))
}
// Consumer mocks base method.
func (m *MockStream) Consumer(ctx context.Context, consumer string) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Consumer", ctx, consumer)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Consumer indicates an expected call of Consumer.
func (mr *MockStreamMockRecorder) Consumer(ctx, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Consumer", reflect.TypeOf((*MockStream)(nil).Consumer), ctx, consumer)
}
// ConsumerNames mocks base method.
func (m *MockStream) ConsumerNames(arg0 context.Context) jetstream.ConsumerNameLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ConsumerNames", arg0)
ret0, _ := ret[0].(jetstream.ConsumerNameLister)
return ret0
}
// ConsumerNames indicates an expected call of ConsumerNames.
func (mr *MockStreamMockRecorder) ConsumerNames(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConsumerNames", reflect.TypeOf((*MockStream)(nil).ConsumerNames), arg0)
}
// CreateConsumer mocks base method.
func (m *MockStream) CreateConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateConsumer indicates an expected call of CreateConsumer.
func (mr *MockStreamMockRecorder) CreateConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateConsumer", reflect.TypeOf((*MockStream)(nil).CreateConsumer), ctx, cfg)
}
// CreateOrUpdateConsumer mocks base method.
func (m *MockStream) CreateOrUpdateConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdateConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdateConsumer indicates an expected call of CreateOrUpdateConsumer.
func (mr *MockStreamMockRecorder) CreateOrUpdateConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdateConsumer", reflect.TypeOf((*MockStream)(nil).CreateOrUpdateConsumer), ctx, cfg)
}
// CreateOrUpdatePushConsumer mocks base method.
func (m *MockStream) CreateOrUpdatePushConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateOrUpdatePushConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreateOrUpdatePushConsumer indicates an expected call of CreateOrUpdatePushConsumer.
func (mr *MockStreamMockRecorder) CreateOrUpdatePushConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOrUpdatePushConsumer", reflect.TypeOf((*MockStream)(nil).CreateOrUpdatePushConsumer), ctx, cfg)
}
// CreatePushConsumer mocks base method.
func (m *MockStream) CreatePushConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreatePushConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// CreatePushConsumer indicates an expected call of CreatePushConsumer.
func (mr *MockStreamMockRecorder) CreatePushConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePushConsumer", reflect.TypeOf((*MockStream)(nil).CreatePushConsumer), ctx, cfg)
}
// DeleteConsumer mocks base method.
func (m *MockStream) DeleteConsumer(ctx context.Context, consumer string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteConsumer", ctx, consumer)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteConsumer indicates an expected call of DeleteConsumer.
func (mr *MockStreamMockRecorder) DeleteConsumer(ctx, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteConsumer", reflect.TypeOf((*MockStream)(nil).DeleteConsumer), ctx, consumer)
}
// DeleteMsg mocks base method.
func (m *MockStream) DeleteMsg(ctx context.Context, seq uint64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMsg", ctx, seq)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteMsg indicates an expected call of DeleteMsg.
func (mr *MockStreamMockRecorder) DeleteMsg(ctx, seq any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMsg", reflect.TypeOf((*MockStream)(nil).DeleteMsg), ctx, seq)
}
// GetLastMsgForSubject mocks base method.
func (m *MockStream) GetLastMsgForSubject(ctx context.Context, subject string) (*jetstream.RawStreamMsg, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLastMsgForSubject", ctx, subject)
ret0, _ := ret[0].(*jetstream.RawStreamMsg)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLastMsgForSubject indicates an expected call of GetLastMsgForSubject.
func (mr *MockStreamMockRecorder) GetLastMsgForSubject(ctx, subject any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLastMsgForSubject", reflect.TypeOf((*MockStream)(nil).GetLastMsgForSubject), ctx, subject)
}
// GetMsg mocks base method.
func (m *MockStream) GetMsg(ctx context.Context, seq uint64, opts ...jetstream.GetMsgOpt) (*jetstream.RawStreamMsg, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, seq}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "GetMsg", varargs...)
ret0, _ := ret[0].(*jetstream.RawStreamMsg)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMsg indicates an expected call of GetMsg.
func (mr *MockStreamMockRecorder) GetMsg(ctx, seq any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, seq}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMsg", reflect.TypeOf((*MockStream)(nil).GetMsg), varargs...)
}
// Info mocks base method.
func (m *MockStream) Info(ctx context.Context, opts ...jetstream.StreamInfoOpt) (*jetstream.StreamInfo, error) {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Info", varargs...)
ret0, _ := ret[0].(*jetstream.StreamInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockStreamMockRecorder) Info(ctx any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockStream)(nil).Info), varargs...)
}
// ListConsumers mocks base method.
func (m *MockStream) ListConsumers(arg0 context.Context) jetstream.ConsumerInfoLister {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListConsumers", arg0)
ret0, _ := ret[0].(jetstream.ConsumerInfoLister)
return ret0
}
// ListConsumers indicates an expected call of ListConsumers.
func (mr *MockStreamMockRecorder) ListConsumers(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListConsumers", reflect.TypeOf((*MockStream)(nil).ListConsumers), arg0)
}
// OrderedConsumer mocks base method.
func (m *MockStream) OrderedConsumer(ctx context.Context, cfg jetstream.OrderedConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "OrderedConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// OrderedConsumer indicates an expected call of OrderedConsumer.
func (mr *MockStreamMockRecorder) OrderedConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderedConsumer", reflect.TypeOf((*MockStream)(nil).OrderedConsumer), ctx, cfg)
}
// PauseConsumer mocks base method.
func (m *MockStream) PauseConsumer(ctx context.Context, consumer string, pauseUntil time.Time) (*jetstream.ConsumerPauseResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PauseConsumer", ctx, consumer, pauseUntil)
ret0, _ := ret[0].(*jetstream.ConsumerPauseResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PauseConsumer indicates an expected call of PauseConsumer.
func (mr *MockStreamMockRecorder) PauseConsumer(ctx, consumer, pauseUntil any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PauseConsumer", reflect.TypeOf((*MockStream)(nil).PauseConsumer), ctx, consumer, pauseUntil)
}
// Purge mocks base method.
func (m *MockStream) Purge(ctx context.Context, opts ...jetstream.StreamPurgeOpt) error {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Purge", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Purge indicates an expected call of Purge.
func (mr *MockStreamMockRecorder) Purge(ctx any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Purge", reflect.TypeOf((*MockStream)(nil).Purge), varargs...)
}
// PushConsumer mocks base method.
func (m *MockStream) PushConsumer(ctx context.Context, consumer string) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PushConsumer", ctx, consumer)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PushConsumer indicates an expected call of PushConsumer.
func (mr *MockStreamMockRecorder) PushConsumer(ctx, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushConsumer", reflect.TypeOf((*MockStream)(nil).PushConsumer), ctx, consumer)
}
// ResumeConsumer mocks base method.
func (m *MockStream) ResumeConsumer(ctx context.Context, consumer string) (*jetstream.ConsumerPauseResponse, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ResumeConsumer", ctx, consumer)
ret0, _ := ret[0].(*jetstream.ConsumerPauseResponse)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ResumeConsumer indicates an expected call of ResumeConsumer.
func (mr *MockStreamMockRecorder) ResumeConsumer(ctx, consumer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ResumeConsumer", reflect.TypeOf((*MockStream)(nil).ResumeConsumer), ctx, consumer)
}
// SecureDeleteMsg mocks base method.
func (m *MockStream) SecureDeleteMsg(ctx context.Context, seq uint64) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SecureDeleteMsg", ctx, seq)
ret0, _ := ret[0].(error)
return ret0
}
// SecureDeleteMsg indicates an expected call of SecureDeleteMsg.
func (mr *MockStreamMockRecorder) SecureDeleteMsg(ctx, seq any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SecureDeleteMsg", reflect.TypeOf((*MockStream)(nil).SecureDeleteMsg), ctx, seq)
}
// UnpinConsumer mocks base method.
func (m *MockStream) UnpinConsumer(ctx context.Context, consumer, group string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnpinConsumer", ctx, consumer, group)
ret0, _ := ret[0].(error)
return ret0
}
// UnpinConsumer indicates an expected call of UnpinConsumer.
func (mr *MockStreamMockRecorder) UnpinConsumer(ctx, consumer, group any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnpinConsumer", reflect.TypeOf((*MockStream)(nil).UnpinConsumer), ctx, consumer, group)
}
// UpdateConsumer mocks base method.
func (m *MockStream) UpdateConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.Consumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.Consumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateConsumer indicates an expected call of UpdateConsumer.
func (mr *MockStreamMockRecorder) UpdateConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateConsumer", reflect.TypeOf((*MockStream)(nil).UpdateConsumer), ctx, cfg)
}
// UpdatePushConsumer mocks base method.
func (m *MockStream) UpdatePushConsumer(ctx context.Context, cfg jetstream.ConsumerConfig) (jetstream.PushConsumer, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdatePushConsumer", ctx, cfg)
ret0, _ := ret[0].(jetstream.PushConsumer)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdatePushConsumer indicates an expected call of UpdatePushConsumer.
func (mr *MockStreamMockRecorder) UpdatePushConsumer(ctx, cfg any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePushConsumer", reflect.TypeOf((*MockStream)(nil).UpdatePushConsumer), ctx, cfg)
}
// MockConsumer is a mock of Consumer interface.
type MockConsumer struct {
ctrl *gomock.Controller
recorder *MockConsumerMockRecorder
isgomock struct{}
}
// MockConsumerMockRecorder is the mock recorder for MockConsumer.
type MockConsumerMockRecorder struct {
mock *MockConsumer
}
// NewMockConsumer creates a new mock instance.
func NewMockConsumer(ctrl *gomock.Controller) *MockConsumer {
mock := &MockConsumer{ctrl: ctrl}
mock.recorder = &MockConsumerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConsumer) EXPECT() *MockConsumerMockRecorder {
return m.recorder
}
// CachedInfo mocks base method.
func (m *MockConsumer) CachedInfo() *jetstream.ConsumerInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CachedInfo")
ret0, _ := ret[0].(*jetstream.ConsumerInfo)
return ret0
}
// CachedInfo indicates an expected call of CachedInfo.
func (mr *MockConsumerMockRecorder) CachedInfo() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CachedInfo", reflect.TypeOf((*MockConsumer)(nil).CachedInfo))
}
// Consume mocks base method.
func (m *MockConsumer) Consume(handler jetstream.MessageHandler, opts ...jetstream.PullConsumeOpt) (jetstream.ConsumeContext, error) {
m.ctrl.T.Helper()
varargs := []any{handler}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Consume", varargs...)
ret0, _ := ret[0].(jetstream.ConsumeContext)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Consume indicates an expected call of Consume.
func (mr *MockConsumerMockRecorder) Consume(handler any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{handler}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Consume", reflect.TypeOf((*MockConsumer)(nil).Consume), varargs...)
}
// Fetch mocks base method.
func (m *MockConsumer) Fetch(batch int, opts ...jetstream.FetchOpt) (jetstream.MessageBatch, error) {
m.ctrl.T.Helper()
varargs := []any{batch}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Fetch", varargs...)
ret0, _ := ret[0].(jetstream.MessageBatch)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Fetch indicates an expected call of Fetch.
func (mr *MockConsumerMockRecorder) Fetch(batch any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{batch}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockConsumer)(nil).Fetch), varargs...)
}
// FetchBytes mocks base method.
func (m *MockConsumer) FetchBytes(maxBytes int, opts ...jetstream.FetchOpt) (jetstream.MessageBatch, error) {
m.ctrl.T.Helper()
varargs := []any{maxBytes}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "FetchBytes", varargs...)
ret0, _ := ret[0].(jetstream.MessageBatch)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchBytes indicates an expected call of FetchBytes.
func (mr *MockConsumerMockRecorder) FetchBytes(maxBytes any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{maxBytes}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchBytes", reflect.TypeOf((*MockConsumer)(nil).FetchBytes), varargs...)
}
// FetchNoWait mocks base method.
func (m *MockConsumer) FetchNoWait(batch int) (jetstream.MessageBatch, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FetchNoWait", batch)
ret0, _ := ret[0].(jetstream.MessageBatch)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// FetchNoWait indicates an expected call of FetchNoWait.
func (mr *MockConsumerMockRecorder) FetchNoWait(batch any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchNoWait", reflect.TypeOf((*MockConsumer)(nil).FetchNoWait), batch)
}
// Info mocks base method.
func (m *MockConsumer) Info(arg0 context.Context) (*jetstream.ConsumerInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info", arg0)
ret0, _ := ret[0].(*jetstream.ConsumerInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockConsumerMockRecorder) Info(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockConsumer)(nil).Info), arg0)
}
// Messages mocks base method.
func (m *MockConsumer) Messages(opts ...jetstream.PullMessagesOpt) (jetstream.MessagesContext, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Messages", varargs...)
ret0, _ := ret[0].(jetstream.MessagesContext)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Messages indicates an expected call of Messages.
func (mr *MockConsumerMockRecorder) Messages(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Messages", reflect.TypeOf((*MockConsumer)(nil).Messages), opts...)
}
// Next mocks base method.
func (m *MockConsumer) Next(opts ...jetstream.FetchOpt) (jetstream.Msg, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Next", varargs...)
ret0, _ := ret[0].(jetstream.Msg)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Next indicates an expected call of Next.
func (mr *MockConsumerMockRecorder) Next(opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*MockConsumer)(nil).Next), opts...)
}
// MockMsg is a mock of Msg interface.
type MockMsg struct {
ctrl *gomock.Controller
recorder *MockMsgMockRecorder
isgomock struct{}
}
// MockMsgMockRecorder is the mock recorder for MockMsg.
type MockMsgMockRecorder struct {
mock *MockMsg
}
// NewMockMsg creates a new mock instance.
func NewMockMsg(ctrl *gomock.Controller) *MockMsg {
mock := &MockMsg{ctrl: ctrl}
mock.recorder = &MockMsgMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMsg) EXPECT() *MockMsgMockRecorder {
return m.recorder
}
// Ack mocks base method.
func (m *MockMsg) Ack() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Ack")
ret0, _ := ret[0].(error)
return ret0
}
// Ack indicates an expected call of Ack.
func (mr *MockMsgMockRecorder) Ack() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Ack", reflect.TypeOf((*MockMsg)(nil).Ack))
}
// Data mocks base method.
func (m *MockMsg) Data() []byte {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Data")
ret0, _ := ret[0].([]byte)
return ret0
}
// Data indicates an expected call of Data.
func (mr *MockMsgMockRecorder) Data() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Data", reflect.TypeOf((*MockMsg)(nil).Data))
}
// DoubleAck mocks base method.
func (m *MockMsg) DoubleAck(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DoubleAck", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// DoubleAck indicates an expected call of DoubleAck.
func (mr *MockMsgMockRecorder) DoubleAck(arg0 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DoubleAck", reflect.TypeOf((*MockMsg)(nil).DoubleAck), arg0)
}
// Headers mocks base method.
func (m *MockMsg) Headers() nats.Header {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Headers")
ret0, _ := ret[0].(nats.Header)
return ret0
}
// Headers indicates an expected call of Headers.
func (mr *MockMsgMockRecorder) Headers() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Headers", reflect.TypeOf((*MockMsg)(nil).Headers))
}
// InProgress mocks base method.
func (m *MockMsg) InProgress() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InProgress")
ret0, _ := ret[0].(error)
return ret0
}
// InProgress indicates an expected call of InProgress.
func (mr *MockMsgMockRecorder) InProgress() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InProgress", reflect.TypeOf((*MockMsg)(nil).InProgress))
}
// Metadata mocks base method.
func (m *MockMsg) Metadata() (*jetstream.MsgMetadata, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Metadata")
ret0, _ := ret[0].(*jetstream.MsgMetadata)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Metadata indicates an expected call of Metadata.
func (mr *MockMsgMockRecorder) Metadata() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Metadata", reflect.TypeOf((*MockMsg)(nil).Metadata))
}
// Nak mocks base method.
func (m *MockMsg) Nak() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Nak")
ret0, _ := ret[0].(error)
return ret0
}
// Nak indicates an expected call of Nak.
func (mr *MockMsgMockRecorder) Nak() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Nak", reflect.TypeOf((*MockMsg)(nil).Nak))
}
// NakWithDelay mocks base method.
func (m *MockMsg) NakWithDelay(delay time.Duration) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NakWithDelay", delay)
ret0, _ := ret[0].(error)
return ret0
}
// NakWithDelay indicates an expected call of NakWithDelay.
func (mr *MockMsgMockRecorder) NakWithDelay(delay any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NakWithDelay", reflect.TypeOf((*MockMsg)(nil).NakWithDelay), delay)
}
// Reply mocks base method.
func (m *MockMsg) Reply() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Reply")
ret0, _ := ret[0].(string)
return ret0
}
// Reply indicates an expected call of Reply.
func (mr *MockMsgMockRecorder) Reply() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reply", reflect.TypeOf((*MockMsg)(nil).Reply))
}
// Subject mocks base method.
func (m *MockMsg) Subject() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Subject")
ret0, _ := ret[0].(string)
return ret0
}
// Subject indicates an expected call of Subject.
func (mr *MockMsgMockRecorder) Subject() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Subject", reflect.TypeOf((*MockMsg)(nil).Subject))
}
// Term mocks base method.
func (m *MockMsg) Term() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Term")
ret0, _ := ret[0].(error)
return ret0
}
// Term indicates an expected call of Term.
func (mr *MockMsgMockRecorder) Term() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Term", reflect.TypeOf((*MockMsg)(nil).Term))
}
// TermWithReason mocks base method.
func (m *MockMsg) TermWithReason(reason string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TermWithReason", reason)
ret0, _ := ret[0].(error)
return ret0
}
// TermWithReason indicates an expected call of TermWithReason.
func (mr *MockMsgMockRecorder) TermWithReason(reason any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TermWithReason", reflect.TypeOf((*MockMsg)(nil).TermWithReason), reason)
}
// MockMessageBatch is a mock of MessageBatch interface.
type MockMessageBatch struct {
ctrl *gomock.Controller
recorder *MockMessageBatchMockRecorder
isgomock struct{}
}
// MockMessageBatchMockRecorder is the mock recorder for MockMessageBatch.
type MockMessageBatchMockRecorder struct {
mock *MockMessageBatch
}
// NewMockMessageBatch creates a new mock instance.
func NewMockMessageBatch(ctrl *gomock.Controller) *MockMessageBatch {
mock := &MockMessageBatch{ctrl: ctrl}
mock.recorder = &MockMessageBatchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMessageBatch) EXPECT() *MockMessageBatchMockRecorder {
return m.recorder
}
// Error mocks base method.
func (m *MockMessageBatch) Error() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Error")
ret0, _ := ret[0].(error)
return ret0
}
// Error indicates an expected call of Error.
func (mr *MockMessageBatchMockRecorder) Error() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockMessageBatch)(nil).Error))
}
// Messages mocks base method.
func (m *MockMessageBatch) Messages() <-chan jetstream.Msg {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Messages")
ret0, _ := ret[0].(<-chan jetstream.Msg)
return ret0
}
// Messages indicates an expected call of Messages.
func (mr *MockMessageBatchMockRecorder) Messages() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Messages", reflect.TypeOf((*MockMessageBatch)(nil).Messages))
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ./metrics.go
//
// Generated by this command:
//
// mockgen -destination=mock_metrics.go -package=nats -source=./metrics.go
//
// Package nats is a generated GoMock package.
package nats
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/mock_tracer.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: go.opentelemetry.io/otel/trace (interfaces: Tracer)
//
// Generated by this command:
//
// mockgen -destination=mock_tracer.go -package=nats go.opentelemetry.io/otel/trace Tracer
//
// Package nats is a generated GoMock package.
package nats
import (
context "context"
reflect "reflect"
trace "go.opentelemetry.io/otel/trace"
gomock "go.uber.org/mock/gomock"
)
// MockTracer is a mock of Tracer interface.
type MockTracer struct {
ctrl *gomock.Controller
recorder *MockTracerMockRecorder
isgomock struct{}
}
// MockTracerMockRecorder is the mock recorder for MockTracer.
type MockTracerMockRecorder struct {
mock *MockTracer
}
// NewMockTracer creates a new mock instance.
func NewMockTracer(ctrl *gomock.Controller) *MockTracer {
mock := &MockTracer{ctrl: ctrl}
mock.recorder = &MockTracerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTracer) EXPECT() *MockTracerMockRecorder {
return m.recorder
}
// Start mocks base method.
func (m *MockTracer) Start(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
m.ctrl.T.Helper()
varargs := []any{ctx, spanName}
for _, a := range opts {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Start", varargs...)
ret0, _ := ret[0].(context.Context)
ret1, _ := ret[1].(trace.Span)
return ret0, ret1
}
// Start indicates an expected call of Start.
func (mr *MockTracerMockRecorder) Start(ctx, spanName any, opts ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, spanName}, opts...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockTracer)(nil).Start), varargs...)
}
// tracer mocks base method.
func (m *MockTracer) tracer() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "tracer")
}
// tracer indicates an expected call of tracer.
func (mr *MockTracerMockRecorder) tracer() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "tracer", reflect.TypeOf((*MockTracer)(nil).tracer))
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/pubsub_wrapper.go
================================================
package nats
import (
"context"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// PubSubWrapper adapts Client to pubsub.JetStreamClient.
type PubSubWrapper struct {
Client *Client
}
func (w *PubSubWrapper) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
return w.Client.Query(ctx, query, args...)
}
// Publish publishes a message to a topic.
func (w *PubSubWrapper) Publish(ctx context.Context, topic string, message []byte) error {
return w.Client.Publish(ctx, topic, message)
}
// Subscribe subscribes to a topic and returns a single message.
func (w *PubSubWrapper) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
return w.Client.Subscribe(ctx, topic)
}
// CreateTopic creates a new topic (stream) in NATS jStream.
func (w *PubSubWrapper) CreateTopic(ctx context.Context, name string) error {
return w.Client.CreateTopic(ctx, name)
}
// DeleteTopic deletes a topic (stream) in NATS jStream.
func (w *PubSubWrapper) DeleteTopic(ctx context.Context, name string) error {
return w.Client.DeleteTopic(ctx, name)
}
// Close closes the Client.
func (w *PubSubWrapper) Close() error {
ctx := context.Background()
return w.Client.Close(ctx)
}
// Health returns the health status of the Client.
func (w *PubSubWrapper) Health() datasource.Health {
return w.Client.Health()
}
// Connect establishes a connection to NATS.
func (w *PubSubWrapper) Connect() {
if w.Client.connManager != nil && w.Client.connManager.Health().Status == datasource.StatusUp {
w.Client.logger.Log("NATS connection already established")
return
}
err := w.Client.Connect()
if err != nil {
w.Client.logger.Errorf("PubSubWrapper: Error connecting to NATS: %v", err)
}
}
// UseLogger sets the logger for the NATS client.
func (w *PubSubWrapper) UseLogger(logger any) {
w.Client.UseLogger(logger)
}
// UseMetrics sets the metrics for the NATS client.
func (w *PubSubWrapper) UseMetrics(metrics any) {
w.Client.UseMetrics(metrics)
}
// UseTracer sets the tracer for the NATS client.
func (w *PubSubWrapper) UseTracer(tracer any) {
w.Client.UseTracer(tracer)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/stream_manager.go
================================================
package nats
import (
"context"
"errors"
"strings"
"github.com/nats-io/nats.go/jetstream"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// StreamManager is a manager for jStream streams.
type StreamManager struct {
js jetstream.JetStream
logger pubsub.Logger
}
// newStreamManager creates a new StreamManager.
func newStreamManager(js jetstream.JetStream, logger pubsub.Logger) *StreamManager {
return &StreamManager{
js: js,
logger: logger,
}
}
// CreateStream creates a new jStream stream.
func (sm *StreamManager) CreateStream(ctx context.Context, cfg *StreamConfig) error {
jsCfg := jetstream.StreamConfig{
Name: cfg.Stream,
Subjects: cfg.Subjects,
MaxBytes: cfg.MaxBytes,
MaxAge: cfg.MaxAge,
}
if cfg.Storage != "" {
if cfg.Storage == "file" {
jsCfg.Storage = jetstream.FileStorage
} else if cfg.Storage == "memory" {
jsCfg.Storage = jetstream.MemoryStorage
}
}
if cfg.Retention != "" {
switch cfg.Retention {
case "limits":
jsCfg.Retention = jetstream.LimitsPolicy
case "interest":
jsCfg.Retention = jetstream.InterestPolicy
case "workqueue":
jsCfg.Retention = jetstream.WorkQueuePolicy
}
}
_, err := sm.js.CreateStream(ctx, jsCfg)
if err != nil {
if strings.Contains(err.Error(), "stream name already in use") {
return nil
}
sm.logger.Errorf("failed to create stream: %v", err)
return err
}
return nil
}
// DeleteStream deletes a jStream stream.
func (sm *StreamManager) DeleteStream(ctx context.Context, name string) error {
sm.logger.Debugf("deleting stream %s", name)
err := sm.js.DeleteStream(ctx, name)
if err != nil {
if errors.Is(err, jetstream.ErrStreamNotFound) {
sm.logger.Debugf("stream %s not found, considering delete successful", name)
return nil // If the stream doesn't exist, we consider it a success
}
sm.logger.Errorf("failed to delete stream %s: %v", name, err)
return err
}
sm.logger.Debugf("successfully deleted stream %s", name)
return nil
}
// CreateOrUpdateStream creates or updates a jStream stream.
func (sm *StreamManager) CreateOrUpdateStream(ctx context.Context, cfg *jetstream.StreamConfig) (jetstream.Stream, error) {
sm.logger.Debugf("creating or updating stream %s", cfg.Name)
stream, err := sm.js.CreateOrUpdateStream(ctx, *cfg)
if err != nil {
sm.logger.Errorf("failed to create or update stream: %v", err)
return nil, err
}
return stream, nil
}
// GetStream gets a jStream stream.
func (sm *StreamManager) GetStream(ctx context.Context, name string) (jetstream.Stream, error) {
sm.logger.Debugf("getting stream %s", name)
stream, err := sm.js.Stream(ctx, name)
if err != nil {
if errors.Is(err, jetstream.ErrStreamNotFound) {
sm.logger.Debugf("stream %s not found", name)
return nil, err
}
sm.logger.Errorf("failed to get stream %s: %v", name, err)
return nil, err
}
return stream, nil
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/stream_manager_test.go
================================================
package nats
import (
"testing"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
func TestNewStreamManager(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
assert.NotNil(t, sm)
assert.Equal(t, mockJS, sm.js)
assert.Equal(t, logger, sm.logger)
}
func TestStreamManager_CreateStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
cfg := StreamConfig{
Stream: "test-stream",
Subjects: []string{"test.subject"},
}
mockJS.EXPECT().CreateStream(ctx, gomock.Any()).Return(nil, nil)
err := sm.CreateStream(ctx, &cfg)
require.NoError(t, err)
}
func TestStreamManager_CreateStream_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
cfg := StreamConfig{
Stream: "test-stream",
Subjects: []string{"test.subject"},
}
expectedErr := errCreateStream
mockJS.EXPECT().CreateStream(ctx, gomock.Any()).Return(nil, expectedErr)
err := sm.CreateStream(ctx, &cfg)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func TestStreamManager_DeleteStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
mockJS.EXPECT().DeleteStream(ctx, streamName).Return(nil)
err := sm.DeleteStream(ctx, streamName)
require.NoError(t, err)
}
func TestStreamManager_DeleteStream_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
mockJS.EXPECT().DeleteStream(ctx, streamName).Return(jetstream.ErrStreamNotFound)
err := sm.DeleteStream(ctx, streamName)
require.NoError(t, err)
}
func TestStreamManager_DeleteStream_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
expectedErr := errDeleteStream
mockJS.EXPECT().DeleteStream(ctx, streamName).Return(expectedErr)
err := sm.DeleteStream(ctx, streamName)
require.Error(t, err)
assert.Equal(t, expectedErr, err)
}
func TestStreamManager_CreateOrUpdateStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
cfg := &jetstream.StreamConfig{
Name: "test-stream",
Subjects: []string{"test.subject"},
}
mockStream := NewMockStream(ctrl)
mockJS.EXPECT().CreateOrUpdateStream(ctx, *cfg).Return(mockStream, nil)
stream, err := sm.CreateOrUpdateStream(ctx, cfg)
require.NoError(t, err)
assert.Equal(t, mockStream, stream)
}
func TestStreamManager_CreateOrUpdateStream_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
cfg := &jetstream.StreamConfig{
Name: "test-stream",
Subjects: []string{"test.subject"},
}
expectedErr := errCreateOrUpdateStream
mockJS.EXPECT().CreateOrUpdateStream(ctx, *cfg).Return(nil, expectedErr)
stream, err := sm.CreateOrUpdateStream(ctx, cfg)
require.Error(t, err)
assert.Nil(t, stream)
assert.Equal(t, expectedErr, err)
}
func TestStreamManager_GetStream(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
mockStream := NewMockStream(ctrl)
mockJS.EXPECT().Stream(ctx, streamName).Return(mockStream, nil)
stream, err := sm.GetStream(ctx, streamName)
require.NoError(t, err)
assert.Equal(t, mockStream, stream)
}
func TestStreamManager_GetStream_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
mockJS.EXPECT().Stream(ctx, streamName).Return(nil, jetstream.ErrStreamNotFound)
stream, err := sm.GetStream(ctx, streamName)
require.Error(t, err)
assert.Nil(t, stream)
assert.Equal(t, jetstream.ErrStreamNotFound, err)
}
func TestStreamManager_GetStream_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
logger := logging.NewMockLogger(logging.DEBUG)
sm := newStreamManager(mockJS, logger)
ctx := t.Context()
streamName := "test-stream"
expectedErr := errGetStream
mockJS.EXPECT().Stream(ctx, streamName).Return(nil, expectedErr)
stream, err := sm.GetStream(ctx, streamName)
require.Error(t, err)
assert.Nil(t, stream)
assert.Equal(t, expectedErr, err)
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/subscription_manager.go
================================================
package nats
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go/jetstream"
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
const (
consumeMessageDelay = 100 * time.Millisecond
)
type SubscriptionManager struct {
subscriptions map[string]*subscription
subMutex sync.Mutex
topicBuffers map[string]chan *pubsub.Message
bufferMutex sync.RWMutex
bufferSize int
}
type subscription struct {
cancel context.CancelFunc
}
func newSubscriptionManager(bufferSize int) *SubscriptionManager {
return &SubscriptionManager{
subscriptions: make(map[string]*subscription),
topicBuffers: make(map[string]chan *pubsub.Message),
bufferSize: bufferSize,
}
}
func (sm *SubscriptionManager) Subscribe(
ctx context.Context,
topic string,
js jetstream.JetStream,
cfg *Config,
logger pubsub.Logger,
metrics Metrics) (*pubsub.Message, error) {
metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic)
if err := sm.validateSubscribePrerequisites(js, cfg); err != nil {
return nil, err
}
sm.subMutex.Lock()
_, exists := sm.subscriptions[topic]
if !exists {
cons, err := sm.createOrUpdateConsumer(ctx, js, topic, cfg)
if err != nil {
sm.subMutex.Unlock()
return nil, err
}
subCtx, cancel := context.WithCancel(ctx)
sm.subscriptions[topic] = &subscription{cancel: cancel}
buffer := sm.getOrCreateBuffer(topic)
go sm.consumeMessages(subCtx, cons, topic, buffer, cfg, logger)
}
sm.subMutex.Unlock()
buffer := sm.getOrCreateBuffer(topic)
select {
case msg := <-buffer:
metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic)
return msg, nil
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (*SubscriptionManager) validateSubscribePrerequisites(js jetstream.JetStream, cfg *Config) error {
if js == nil {
return errJetStreamNotConfigured
}
if cfg.Consumer == "" {
return errConsumerNotProvided
}
return nil
}
func (sm *SubscriptionManager) getOrCreateBuffer(topic string) chan *pubsub.Message {
sm.bufferMutex.Lock()
defer sm.bufferMutex.Unlock()
if buffer, exists := sm.topicBuffers[topic]; exists {
return buffer
}
buffer := make(chan *pubsub.Message, sm.bufferSize)
sm.topicBuffers[topic] = buffer
return buffer
}
func (*SubscriptionManager) createOrUpdateConsumer(
ctx context.Context, js jetstream.JetStream, topic string, cfg *Config) (jetstream.Consumer, error) {
consumerName := fmt.Sprintf("%s_%s", cfg.Consumer, strings.ReplaceAll(topic, ".", "_"))
cons, err := js.CreateOrUpdateConsumer(ctx, cfg.Stream.Stream, jetstream.ConsumerConfig{
Durable: consumerName,
AckPolicy: jetstream.AckExplicitPolicy,
FilterSubject: topic,
MaxDeliver: cfg.Stream.MaxDeliver,
DeliverPolicy: jetstream.DeliverNewPolicy,
AckWait: defaultAckWait,
})
return cons, err
}
func (sm *SubscriptionManager) consumeMessages(
ctx context.Context,
cons jetstream.Consumer,
topic string,
buffer chan *pubsub.Message,
cfg *Config,
logger pubsub.Logger) {
// TODO: propagate errors to caller
for {
select {
case <-ctx.Done():
return
default:
if err := sm.fetchAndProcessMessages(ctx, cons, topic, buffer, cfg, logger); err != nil {
logger.Errorf("Error fetching messages for topic %s: %v", topic, err)
}
}
}
}
func (sm *SubscriptionManager) fetchAndProcessMessages(
ctx context.Context,
cons jetstream.Consumer,
topic string,
buffer chan *pubsub.Message,
cfg *Config,
logger pubsub.Logger) error {
msgs, err := cons.Fetch(1, jetstream.FetchMaxWait(cfg.MaxWait))
if err != nil {
return sm.handleFetchError(err, topic, logger)
}
return sm.processFetchedMessages(ctx, msgs, topic, buffer, logger)
}
func (*SubscriptionManager) handleFetchError(err error, topic string, logger pubsub.Logger) error {
if !errors.Is(err, context.DeadlineExceeded) {
logger.Errorf("Error fetching messages for topic %s: %v", topic, err)
}
time.Sleep(consumeMessageDelay)
return nil
}
func (sm *SubscriptionManager) processFetchedMessages(
ctx context.Context,
msgs jetstream.MessageBatch,
topic string,
buffer chan *pubsub.Message,
logger pubsub.Logger) error {
for msg := range msgs.Messages() {
pubsubMsg := sm.createPubSubMessage(ctx, msg, topic)
if !sm.sendToBuffer(pubsubMsg, buffer) {
logger.Logf("Message buffer is full for topic %s. Consider increasing buffer size or processing messages faster.", topic)
}
}
return sm.checkBatchError(msgs, topic, logger)
}
func (*SubscriptionManager) createPubSubMessage(ctx context.Context, msg jetstream.Msg, topic string) *pubsub.Message {
spanCtx, span := startSubscribeSpan(ctx, otel.GetTracerProvider().Tracer(tracerName), topic, msg.Headers())
pubsubMsg := pubsub.NewMessage(spanCtx)
pubsubMsg.Topic = topic
pubsubMsg.Value = msg.Data()
pubsubMsg.MetaData = msg.Headers()
pubsubMsg.Committer = &natsCommitter{msg: msg, span: span}
return pubsubMsg
}
func (*SubscriptionManager) sendToBuffer(msg *pubsub.Message, buffer chan *pubsub.Message) bool {
select {
case buffer <- msg:
return true
default:
return false
}
}
func (*SubscriptionManager) checkBatchError(msgs jetstream.MessageBatch, topic string, logger pubsub.Logger) error {
if err := msgs.Error(); err != nil {
logger.Errorf("Error in message batch for topic %s: %v", topic, err)
return err
}
return nil
}
func (sm *SubscriptionManager) Close() {
sm.subMutex.Lock()
for _, sub := range sm.subscriptions {
sub.cancel()
}
sm.subscriptions = make(map[string]*subscription)
sm.subMutex.Unlock()
sm.bufferMutex.Lock()
for _, buffer := range sm.topicBuffers {
close(buffer)
}
sm.topicBuffers = make(map[string]chan *pubsub.Message)
sm.bufferMutex.Unlock()
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/subscription_manager_test.go
================================================
package nats
import (
"context"
"testing"
"time"
"github.com/nats-io/nats.go/jetstream"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
)
func TestNewSubscriptionManager(t *testing.T) {
sm := newSubscriptionManager(100)
assert.NotNil(t, sm)
assert.Equal(t, 100, sm.bufferSize)
assert.NotNil(t, sm.subscriptions)
assert.NotNil(t, sm.topicBuffers)
}
func TestSubscriptionManager_Subscribe(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
mockConsumer := NewMockConsumer(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
sm := newSubscriptionManager(1)
cfg := &Config{
Consumer: "test-consumer",
Stream: StreamConfig{
Stream: "test-stream",
MaxDeliver: 3,
},
MaxWait: time.Second,
}
ctx, cancel := context.WithTimeout(t.Context(), 2*time.Second)
defer cancel()
topic := "test.topic"
mockJS.EXPECT().CreateOrUpdateConsumer(gomock.Any(), cfg.Stream.Stream, gomock.Any()).Return(mockConsumer, nil)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", topic)
mockConsumer.EXPECT().Fetch(gomock.Any(), gomock.Any()).Return(createMockMessageBatch(ctrl), nil).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_success_count", "topic", topic)
msg, err := sm.Subscribe(ctx, topic, mockJS, cfg, mockLogger, mockMetrics)
require.NoError(t, err)
assert.NotNil(t, msg)
assert.Equal(t, topic, msg.Topic)
}
func TestSubscriptionManager_Subscribe_Error(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
sm := newSubscriptionManager(1)
cfg := &Config{
Consumer: "test-consumer",
Stream: StreamConfig{
Stream: "test-stream",
},
}
ctx := t.Context()
topic := "test.topic"
expectedErr := errConsumerCreationError
mockJS.EXPECT().CreateOrUpdateConsumer(gomock.Any(), cfg.Stream.Stream, gomock.Any()).Return(nil, expectedErr)
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "app_pubsub_subscribe_total_count", "topic", topic)
msg, err := sm.Subscribe(ctx, topic, mockJS, cfg, mockLogger, mockMetrics)
require.Error(t, err)
assert.Nil(t, msg)
assert.Equal(t, expectedErr, err)
}
func TestSubscriptionManager_validateSubscribePrerequisites(t *testing.T) {
sm := newSubscriptionManager(1)
mockJS := NewMockJetStream(gomock.NewController(t))
cfg := &Config{Consumer: "test-consumer"}
err := sm.validateSubscribePrerequisites(mockJS, cfg)
require.NoError(t, err)
err = sm.validateSubscribePrerequisites(nil, cfg)
assert.Equal(t, errJetStreamNotConfigured, err)
err = sm.validateSubscribePrerequisites(mockJS, &Config{})
assert.Equal(t, errConsumerNotProvided, err)
}
func TestSubscriptionManager_getOrCreateBuffer(t *testing.T) {
sm := newSubscriptionManager(1)
topic := "test.topic"
buffer := sm.getOrCreateBuffer(topic)
assert.NotNil(t, buffer)
assert.Empty(t, buffer)
assert.Equal(t, 1, cap(buffer))
// Check that the same buffer is returned for the same topic
sameBuffer := sm.getOrCreateBuffer(topic)
assert.Equal(t, buffer, sameBuffer)
}
func TestSubscriptionManager_createOrUpdateConsumer(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockJS := NewMockJetStream(ctrl)
mockConsumer := NewMockConsumer(ctrl)
sm := newSubscriptionManager(1)
cfg := &Config{
Consumer: "test-consumer",
Stream: StreamConfig{
Stream: "test-stream",
MaxDeliver: 3,
},
}
ctx := t.Context()
topic := "test.topic"
mockJS.EXPECT().CreateOrUpdateConsumer(ctx, cfg.Stream.Stream, gomock.Any()).Return(mockConsumer, nil)
consumer, err := sm.createOrUpdateConsumer(ctx, mockJS, topic, cfg)
require.NoError(t, err)
assert.Equal(t, mockConsumer, consumer)
}
func TestSubscriptionManager_consumeMessages(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockConsumer := NewMockConsumer(ctrl)
mockLogger := logging.NewMockLogger(logging.DEBUG)
sm := newSubscriptionManager(1)
cfg := &Config{MaxWait: time.Second}
topic := "test.topic"
buffer := make(chan *pubsub.Message, 1)
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mockBatch := createMockMessageBatch(ctrl)
mockConsumer.EXPECT().Fetch(gomock.Any(), gomock.Any()).Return(mockBatch, nil).AnyTimes()
go sm.consumeMessages(ctx, mockConsumer, topic, buffer, cfg, mockLogger)
select {
case msg := <-buffer:
assert.NotNil(t, msg)
assert.Equal(t, topic, msg.Topic)
case <-time.After(2 * time.Second):
t.Fatal("Timed out waiting for message")
}
}
func createMockMessageBatch(ctrl *gomock.Controller) jetstream.MessageBatch {
mockBatch := NewMockMessageBatch(ctrl)
mockMsg := NewMockMsg(ctrl)
mockMsg.EXPECT().Data().Return([]byte("test message")).AnyTimes()
mockMsg.EXPECT().Headers().Return(nil).AnyTimes()
msgChan := make(chan jetstream.Msg, 1)
msgChan <- mockMsg
close(msgChan)
mockBatch.EXPECT().Messages().Return(msgChan).AnyTimes()
mockBatch.EXPECT().Error().Return(nil).AnyTimes()
return mockBatch
}
func TestSubscriptionManager_createPubSubMessage(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
t.Cleanup(func() {
_ = tp.Shutdown(context.Background())
})
mockMsg := NewMockMsg(ctrl)
mockMsg.EXPECT().Data().Return([]byte("test message"))
mockMsg.EXPECT().Headers().Return(nil).Times(2)
sm := newSubscriptionManager(1)
ctx := t.Context()
topic := "test.topic"
msg := sm.createPubSubMessage(ctx, mockMsg, topic)
require.NotNil(t, msg)
assert.Equal(t, topic, msg.Topic)
assert.Equal(t, []byte("test message"), msg.Value)
// Verify the message context carries a valid span.
require.NotNil(t, msg.Context())
// Verify the committer holds the subscribe span and ends it on Commit.
committer, ok := msg.Committer.(*natsCommitter)
require.True(t, ok, "Committer should be a *natsCommitter")
assert.NotNil(t, committer.span)
assert.True(t, committer.span.SpanContext().IsValid())
}
func TestSubscriptionManager_Close(t *testing.T) {
sm := newSubscriptionManager(1)
topic := "test.topic"
// Create a subscription and buffer
ctx, cancel := context.WithCancel(t.Context())
sm.subscriptions[topic] = &subscription{cancel: cancel}
sm.topicBuffers[topic] = make(chan *pubsub.Message, 1)
sm.Close()
assert.Empty(t, sm.subscriptions)
assert.Empty(t, sm.topicBuffers)
// Check that the context was canceled
if ctx.Err() == nil {
t.Fatal("Context was not canceled")
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/tracing.go
================================================
package nats
import (
"context"
"github.com/nats-io/nats.go"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const tracerName = "gofr-nats"
// headerCarrier implements propagation.TextMapCarrier for NATS message headers.
type headerCarrier nats.Header
// Get returns the first value for a given key from NATS message headers.
func (c headerCarrier) Get(key string) string {
vals := nats.Header(c).Values(key)
if len(vals) == 0 {
return ""
}
return vals[0]
}
// Set sets a key-value pair in the NATS message headers.
func (c headerCarrier) Set(key, value string) {
nats.Header(c).Set(key, value)
}
// Keys returns all keys in the NATS message headers.
func (c headerCarrier) Keys() []string {
h := nats.Header(c)
keys := make([]string, 0, len(h))
for k := range h {
keys = append(keys, k)
}
return keys
}
// injectTraceContext injects the current trace context into NATS message headers.
func injectTraceContext(ctx context.Context, headers nats.Header) nats.Header {
if headers == nil {
headers = make(nats.Header)
}
carrier := headerCarrier(headers)
otel.GetTextMapPropagator().Inject(ctx, carrier)
return headers
}
// extractTraceLinks extracts the trace context from NATS message headers
// and returns span links to the producer span.
// If no trace context is found, returns nil (creating an orphan span).
func extractTraceLinks(headers nats.Header) []trace.Link {
if len(headers) == 0 {
return nil
}
carrier := headerCarrier(headers)
extractedCtx := otel.GetTextMapPropagator().Extract(context.Background(), carrier)
spanCtx := trace.SpanContextFromContext(extractedCtx)
if spanCtx.IsValid() {
return []trace.Link{
{
SpanContext: spanCtx,
},
}
}
return nil
}
// startPublishSpan creates a new span for publishing with trace context injection.
// Returns the updated context, the span, and NATS headers with injected trace context.
func startPublishSpan(ctx context.Context, tracer trace.Tracer, subject string) (context.Context, trace.Span, nats.Header) {
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindProducer),
trace.WithAttributes(
attribute.String("messaging.system", "nats"),
attribute.String("messaging.destination.name", subject),
attribute.String("messaging.operation", "publish"),
),
}
ctx, span := tracer.Start(ctx, "nats-publish", opts...)
headers := injectTraceContext(ctx, nil)
return ctx, span, headers
}
// startSubscribeSpan creates a new span for subscribing with links to the producer span.
// If trace context exists in message headers, creates a span linked to the producer.
// Otherwise, creates an orphan span (new trace).
func startSubscribeSpan(ctx context.Context, tracer trace.Tracer, topic string, headers nats.Header) (context.Context, trace.Span) {
links := extractTraceLinks(headers)
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("messaging.system", "nats"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "receive"),
),
}
if len(links) > 0 {
opts = append(opts, trace.WithLinks(links...))
}
ctx, span := tracer.Start(ctx, "nats-subscribe", opts...)
return ctx, span
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/tracing_test.go
================================================
package nats
import (
"context"
"testing"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
// setupOTel installs a real in-memory tracer provider and TraceContext propagator,
// returning the exporter so callers can inspect recorded spans.
// All globals are restored when t finishes.
func setupOTel(t *testing.T) (*tracetest.InMemoryExporter, *sdktrace.TracerProvider) {
t.Helper()
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
prevTP := otel.GetTracerProvider()
prevProp := otel.GetTextMapPropagator()
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
t.Cleanup(func() {
_ = tp.Shutdown(context.Background())
otel.SetTracerProvider(prevTP)
otel.SetTextMapPropagator(prevProp)
})
return exporter, tp
}
func TestHeaderCarrier_GetSetKeys(t *testing.T) {
carrier := make(headerCarrier)
// Test Set
carrier.Set("traceparent", "00-1234567890abcdef-fedcba0987654321-01")
carrier.Set("tracestate", "foo=bar")
// Test Get
assert.Equal(t, "00-1234567890abcdef-fedcba0987654321-01", carrier.Get("traceparent"))
assert.Equal(t, "foo=bar", carrier.Get("tracestate"))
assert.Empty(t, carrier.Get("nonexistent"))
// Test Keys
keys := carrier.Keys()
assert.Contains(t, keys, "traceparent")
assert.Contains(t, keys, "tracestate")
// Test Set updates existing key
carrier.Set("traceparent", "00-updated-value")
assert.Equal(t, "00-updated-value", carrier.Get("traceparent"))
}
func TestInjectTraceContext(t *testing.T) {
_, tp := setupOTel(t)
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
headers := injectTraceContext(ctx, nil)
require.NotNil(t, headers)
traceparent := headers.Get("traceparent")
require.NotEmpty(t, traceparent, "traceparent header should be injected")
assert.Contains(t, traceparent, span.SpanContext().TraceID().String())
}
func TestInjectTraceContext_PreservesExistingHeaders(t *testing.T) {
_, tp := setupOTel(t)
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
existing := nats.Header{}
existing.Set("custom-header", "custom-value")
headers := injectTraceContext(ctx, existing)
assert.Equal(t, "custom-value", headers.Get("custom-header"))
traceparent := headers.Get("traceparent")
assert.NotEmpty(t, traceparent, "traceparent should be injected alongside existing headers")
}
func TestExtractTraceLinks(t *testing.T) {
_, tp := setupOTel(t)
ctx, producerSpan := tp.Tracer("test").Start(context.Background(), "producer-span")
headers := injectTraceContext(ctx, nil)
producerSpan.End()
links := extractTraceLinks(headers)
require.Len(t, links, 1, "should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), links[0].SpanContext.SpanID())
}
func TestExtractTraceLinks_NoHeaders(t *testing.T) {
setupOTel(t)
links := extractTraceLinks(nil)
assert.Nil(t, links, "should return nil for nil headers")
}
func TestExtractTraceLinks_EmptyHeaders(t *testing.T) {
setupOTel(t)
links := extractTraceLinks(make(nats.Header))
assert.Nil(t, links, "should return nil for empty headers")
}
func TestStartPublishSpan(t *testing.T) {
_, tp := setupOTel(t)
tracer := tp.Tracer(tracerName)
ctx, span, headers := startPublishSpan(context.Background(), tracer, "test-subject")
defer span.End()
require.NotNil(t, span)
assert.True(t, span.SpanContext().IsValid())
require.NotNil(t, ctx)
traceparent := headers.Get("traceparent")
assert.NotEmpty(t, traceparent, "headers should contain traceparent")
}
func TestStartSubscribeSpan_WithLinks(t *testing.T) {
exporter, tp := setupOTel(t)
tracer := tp.Tracer(tracerName)
_, producerSpan, headers := startPublishSpan(context.Background(), tracer, "test-subject")
producerSpan.End()
_, subscribeSpan := startSubscribeSpan(context.Background(), tracer, "test-subject", headers)
subscribeSpan.End()
spans := exporter.GetSpans()
require.GreaterOrEqual(t, len(spans), 2)
var subSpan *tracetest.SpanStub
for i := range spans {
if spans[i].Name == "nats-subscribe" {
subSpan = &spans[i]
break
}
}
require.NotNil(t, subSpan, "subscribe span should exist")
require.Len(t, subSpan.Links, 1, "subscribe span should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), subSpan.Links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), subSpan.Links[0].SpanContext.SpanID())
}
func TestStartSubscribeSpan_NoLinks(t *testing.T) {
exporter, tp := setupOTel(t)
tracer := tp.Tracer(tracerName)
_, subscribeSpan := startSubscribeSpan(context.Background(), tracer, "test-subject", nil)
subscribeSpan.End()
spans := exporter.GetSpans()
require.Len(t, spans, 1)
assert.Empty(t, spans[0].Links, "orphan span should have no links")
}
================================================
FILE: pkg/gofr/datasource/pubsub/nats/wrapper_test.go
================================================
package nats
import (
"fmt"
"net"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
)
func TestNATSConnWrapper(t *testing.T) {
// Start an embedded NATS server
opts := &server.Options{
Host: "127.0.0.1",
Port: -1, // Random available port
}
ns, err := server.NewServer(opts)
if err != nil {
t.Fatalf("Error starting NATS server: %v", err)
}
go ns.Start()
defer ns.Shutdown()
if !ns.ReadyForConnections(10 * time.Second) {
t.Fatal("NATS server not ready for connections")
}
// Get the server's listen address
addr := ns.Addr().(*net.TCPAddr)
url := fmt.Sprintf("nats://%s:%d", addr.IP.String(), addr.Port)
t.Run("Status", func(t *testing.T) {
nc, err := nats.Connect(url)
if err != nil {
t.Fatal(err)
}
defer nc.Close()
wrapper := &natsConnWrapper{conn: nc}
status := wrapper.Status()
expectedStatus := nats.CONNECTED
if status != expectedStatus {
t.Errorf("Expected status %v, got %v", expectedStatus, status)
}
})
t.Run("Close", func(t *testing.T) {
nc, err := nats.Connect(url)
if err != nil {
t.Fatal(err)
}
wrapper := &natsConnWrapper{conn: nc}
wrapper.Close()
status := wrapper.Status()
expectedStatus := nats.CLOSED
if status != expectedStatus {
t.Errorf("Expected status %v, got %v", expectedStatus, status)
}
})
t.Run("NATSConn", func(t *testing.T) {
nc, err := nats.Connect(url)
if err != nil {
t.Fatal(err)
}
defer nc.Close()
wrapper := &natsConnWrapper{conn: nc}
returnedConn := wrapper.NATSConn()
if returnedConn != nc {
t.Errorf("Expected NATSConn to return the original connection")
}
})
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/pubsub/sqs
go 1.25.0
require (
github.com/aws/aws-sdk-go-v2 v1.32.7
github.com/aws/aws-sdk-go-v2/config v1.28.7
github.com/aws/aws-sdk-go-v2/credentials v1.17.48
github.com/aws/aws-sdk-go-v2/service/sqs v1.37.3
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
gofr.dev v1.55.0
)
require (
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/go.sum
================================================
github.com/aws/aws-sdk-go-v2 v1.32.7 h1:ky5o35oENWi0JYWUZkB7WYvVPP+bcRF5/Iq7JWSb5Rw=
github.com/aws/aws-sdk-go-v2 v1.32.7/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/config v1.28.7 h1:GduUnoTXlhkgnxTD93g1nv4tVPILbdNQOzav+Wpg7AE=
github.com/aws/aws-sdk-go-v2/config v1.28.7/go.mod h1:vZGX6GVkIE8uECSUHB6MWAUsd4ZcG2Yq/dMa4refR3M=
github.com/aws/aws-sdk-go-v2/credentials v1.17.48 h1:IYdLD1qTJ0zanRavulofmqut4afs45mOWEI+MzZtTfQ=
github.com/aws/aws-sdk-go-v2/credentials v1.17.48/go.mod h1:tOscxHN3CGmuX9idQ3+qbkzrjVIx32lqDSU1/0d/qXs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22 h1:kqOrpojG71DxJm/KDPO+Z/y1phm1JlC8/iT+5XRmAn8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.22/go.mod h1:NtSFajXVVL8TA2QNngagVZmUtXciyrHOt7xgz4faS/M=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26 h1:I/5wmGMffY4happ8NOCuIUEWGUvvFp5NSeQcXl9RHcI=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.26/go.mod h1:FR8f4turZtNy6baO0KJ5FJUmXH/cSkI9fOngs0yl6mA=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26 h1:zXFLuEuMMUOvEARXFUVJdfqZ4bvvSgdGRq/ATcrQxzM=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.26/go.mod h1:3o2Wpy0bogG1kyOPrgkXA8pgIfEEv0+m19O9D5+W8y8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7 h1:8eUsivBQzZHqe/3FE+cqwfH+0p5Jo8PFM/QYQSmeZ+M=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.7/go.mod h1:kLPQvGUmxn/fqiCrDeohwG33bq2pQpGeY62yRO6Nrh0=
github.com/aws/aws-sdk-go-v2/service/sqs v1.37.3 h1:94lmK3kN/iRSHrvWt+JujIqjVE53v0wrQ1lbPTmg6gM=
github.com/aws/aws-sdk-go-v2/service/sqs v1.37.3/go.mod h1:171mrsbgz6DahPMnLJzQiH3bXXrdsWhpE9USZiM19Lk=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8 h1:CvuUmnXI7ebaUAhbJcDy9YQx8wHR69eZ9I7q5hszt/g=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.8/go.mod h1:XDeGv1opzwm8ubxddF0cgqkZWsyOtw4lr6dxwmb6YQg=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7 h1:F2rBfNAL5UyswqoeWv9zs74N/NanhK16ydHW1pahX6E=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.7/go.mod h1:JfyQ0g2JG8+Krq0EuZNnRwX0mU0HrwY/tG6JNfcqh4k=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3 h1:Xgv/hyNgvLda/M9l9qxXc4UFSgppnRczLxlMs5Ae/QY=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.3/go.mod h1:5Gn+d+VaaRgsjewpMvGazt0WfcFO+Md4wLOuBfGR9Bc=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
gofr.dev v1.55.0 h1:Ipvk4eBgIv3iuYCCANj8iNKo2sxWelv880A43nLxshQ=
gofr.dev v1.55.0/go.mod h1:W7AHXoLehhOTWqTtMk4oLpkEjSKpHV85D8dpEEuZHjw=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/health.go
================================================
package sqs
import (
"context"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"gofr.dev/pkg/gofr/datasource"
)
const healthCheckTimeout = 5 * time.Second
// Health returns the health status of the SQS connection.
func (c *Client) Health() datasource.Health {
health := datasource.Health{
Details: map[string]any{
"backend": "SQS",
"region": c.cfg.Region,
},
}
if c.conn == nil {
health.Status = datasource.StatusDown
health.Details["error"] = "client not connected"
return health
}
ctx, cancel := context.WithTimeout(context.Background(), healthCheckTimeout)
defer cancel()
// Use ListQueues with MaxResults=1 as a lightweight health check
_, err := c.conn.ListQueues(ctx, &sqs.ListQueuesInput{
MaxResults: aws.Int32(1),
})
if err != nil {
health.Status = datasource.StatusDown
health.Details["error"] = err.Error()
return health
}
health.Status = datasource.StatusUp
return health
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/health_test.go
================================================
package sqs
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/datasource"
)
func TestClient_Health_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
health := client.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Equal(t, "SQS", health.Details["backend"])
assert.Equal(t, "us-east-1", health.Details["region"])
assert.Equal(t, "client not connected", health.Details["error"])
}
func TestClient_Health_NilConfig(t *testing.T) {
client := New(nil)
client.UseLogger(NewMockLogger())
health := client.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Equal(t, "SQS", health.Details["backend"])
assert.Empty(t, health.Details["region"])
assert.Equal(t, "client not connected", health.Details["error"])
}
func TestClient_Health_Connected(t *testing.T) {
mockClient := &mockSQSClient{
listQueuesFunc: func(context.Context, *sqs.ListQueuesInput) (*sqs.ListQueuesOutput, error) {
return &sqs.ListQueuesOutput{}, nil
},
}
client := newTestClient(mockClient)
health := client.Health()
assert.Equal(t, datasource.StatusUp, health.Status)
assert.Equal(t, "SQS", health.Details["backend"])
assert.Equal(t, "us-east-1", health.Details["region"])
assert.Nil(t, health.Details["error"])
}
func TestClient_Health_ListQueuesError(t *testing.T) {
mockClient := &mockSQSClient{
listQueuesFunc: func(context.Context, *sqs.ListQueuesInput) (*sqs.ListQueuesOutput, error) {
return nil, errMockListQueues
},
}
client := newTestClient(mockClient)
health := client.Health()
assert.Equal(t, datasource.StatusDown, health.Status)
assert.Equal(t, "SQS", health.Details["backend"])
assert.Equal(t, "us-east-1", health.Details["region"])
assert.Equal(t, "list queues failed", health.Details["error"])
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/interfaces.go
================================================
package sqs
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/sqs"
)
// sqsClient interface wraps the AWS SQS client methods used by this package.
// This allows for easier testing with mock implementations.
type sqsClient interface {
SendMessage(ctx context.Context, params *sqs.SendMessageInput,
optFns ...func(*sqs.Options)) (*sqs.SendMessageOutput, error)
ReceiveMessage(ctx context.Context, params *sqs.ReceiveMessageInput,
optFns ...func(*sqs.Options)) (*sqs.ReceiveMessageOutput, error)
DeleteMessage(ctx context.Context, params *sqs.DeleteMessageInput,
optFns ...func(*sqs.Options)) (*sqs.DeleteMessageOutput, error)
CreateQueue(ctx context.Context, params *sqs.CreateQueueInput,
optFns ...func(*sqs.Options)) (*sqs.CreateQueueOutput, error)
DeleteQueue(ctx context.Context, params *sqs.DeleteQueueInput,
optFns ...func(*sqs.Options)) (*sqs.DeleteQueueOutput, error)
GetQueueUrl(ctx context.Context, params *sqs.GetQueueUrlInput,
optFns ...func(*sqs.Options)) (*sqs.GetQueueUrlOutput, error)
ListQueues(ctx context.Context, params *sqs.ListQueuesInput,
optFns ...func(*sqs.Options)) (*sqs.ListQueuesOutput, error)
}
// Metrics interface for recording SQS metrics.
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/message.go
================================================
package sqs
import (
"context"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// messageClient interface for message deletion operations.
type messageClient interface {
DeleteMessage(ctx context.Context, params *sqs.DeleteMessageInput, optFns ...func(*sqs.Options)) (*sqs.DeleteMessageOutput, error)
}
// Message implements the pubsub.Committer interface for SQS messages.
type Message struct {
receiptHandle string
queueURL string
client messageClient
logger pubsub.Logger
}
// newMessage creates a new Message for acknowledging SQS messages.
func newMessage(receiptHandle, queueURL string, client messageClient, logger pubsub.Logger) *Message {
return &Message{
receiptHandle: receiptHandle,
queueURL: queueURL,
client: client,
logger: logger,
}
}
// Commit deletes the message from the SQS queue, acknowledging its successful processing.
func (m *Message) Commit() {
if m.receiptHandle == "" || m.client == nil {
return
}
_, err := m.client.DeleteMessage(context.Background(), &sqs.DeleteMessageInput{
QueueUrl: &m.queueURL,
ReceiptHandle: &m.receiptHandle,
})
if err != nil && m.logger != nil {
m.logger.Errorf("failed to delete SQS message: %v", err)
}
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/message_test.go
================================================
package sqs
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/stretchr/testify/assert"
)
// mockMessageClient is a mock for messageClient interface.
type mockMessageClient struct {
deleteMessageFunc func(ctx context.Context,
params *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error)
}
func (m *mockMessageClient) DeleteMessage(
ctx context.Context, params *sqs.DeleteMessageInput, _ ...func(*sqs.Options),
) (*sqs.DeleteMessageOutput, error) {
if m.deleteMessageFunc != nil {
return m.deleteMessageFunc(ctx, params)
}
return &sqs.DeleteMessageOutput{}, nil
}
func TestNewMessage(t *testing.T) {
logger := NewMockLogger()
mockClient := &mockMessageClient{}
msg := newMessage("receipt-123", "http://localhost:4566/queue/test", mockClient, logger)
assert.NotNil(t, msg)
assert.Equal(t, "receipt-123", msg.receiptHandle)
assert.Equal(t, "http://localhost:4566/queue/test", msg.queueURL)
assert.NotNil(t, msg.client)
assert.Equal(t, logger, msg.logger)
}
func TestMessage_Commit_EmptyReceiptHandle(*testing.T) {
msg := &Message{
receiptHandle: "",
queueURL: "http://localhost:4566/queue/test",
client: &mockMessageClient{},
logger: NewMockLogger(),
}
// Should not panic, just return early
msg.Commit()
}
func TestMessage_Commit_NilClient(*testing.T) {
msg := &Message{
receiptHandle: "receipt-123",
queueURL: "http://localhost:4566/queue/test",
client: nil,
logger: NewMockLogger(),
}
// Should not panic, just return early
msg.Commit()
}
func TestMessage_Commit_Success(t *testing.T) {
deleteMessageCalled := false
mockClient := &mockMessageClient{
deleteMessageFunc: func(context.Context, *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) {
deleteMessageCalled = true
return &sqs.DeleteMessageOutput{}, nil
},
}
msg := &Message{
receiptHandle: "receipt-123",
queueURL: "http://localhost:4566/queue/test",
client: mockClient,
logger: NewMockLogger(),
}
msg.Commit()
assert.True(t, deleteMessageCalled)
}
func TestMessage_Commit_Error(t *testing.T) {
mockClient := &mockMessageClient{
deleteMessageFunc: func(context.Context, *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) {
return nil, errMockDeleteFailed
},
}
logger := NewMockLogger()
msg := &Message{
receiptHandle: "receipt-123",
queueURL: "http://localhost:4566/queue/test",
client: mockClient,
logger: logger,
}
msg.Commit()
// Should log error but not panic
assert.NotEmpty(t, logger.lastError)
}
func TestMessage_Commit_ErrorWithNilLogger(*testing.T) {
mockClient := &mockMessageClient{
deleteMessageFunc: func(context.Context, *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error) {
return nil, errMockDeleteFailed
},
}
msg := &Message{
receiptHandle: "receipt-123",
queueURL: "http://localhost:4566/queue/test",
client: mockClient,
logger: nil,
}
// Should not panic even with nil logger
msg.Commit()
}
func TestMessage_Commit_BothEmpty(*testing.T) {
msg := &Message{
receiptHandle: "",
queueURL: "",
client: nil,
logger: nil,
}
// Should not panic
msg.Commit()
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/mock_client.go
================================================
package sqs
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
)
// MockLogger is a mock implementation of pubsub.Logger interface.
type MockLogger struct {
lastError string
}
// NewMockLogger creates a new MockLogger.
func NewMockLogger() *MockLogger {
return &MockLogger{}
}
func (*MockLogger) Debug(...any) {}
func (*MockLogger) Debugf(string, ...any) {}
func (*MockLogger) Log(...any) {}
func (*MockLogger) Logf(string, ...any) {}
func (*MockLogger) Error(...any) {}
func (m *MockLogger) Errorf(format string, args ...any) {
if len(args) > 0 {
m.lastError = format
}
}
// MockMetrics is a mock implementation of Metrics interface.
type MockMetrics struct{}
// NewMockMetrics creates a new MockMetrics.
func NewMockMetrics() *MockMetrics {
return &MockMetrics{}
}
func (*MockMetrics) IncrementCounter(context.Context, string, ...string) {}
// mockSQSClient is a mock implementation of sqsClient interface for testing.
type mockSQSClient struct {
sendMessageFunc func(ctx context.Context, params *sqs.SendMessageInput) (*sqs.SendMessageOutput, error)
receiveMessageFunc func(ctx context.Context, params *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error)
deleteMessageFunc func(ctx context.Context, params *sqs.DeleteMessageInput) (*sqs.DeleteMessageOutput, error)
createQueueFunc func(ctx context.Context, params *sqs.CreateQueueInput) (*sqs.CreateQueueOutput, error)
deleteQueueFunc func(ctx context.Context, params *sqs.DeleteQueueInput) (*sqs.DeleteQueueOutput, error)
getQueueURLFunc func(ctx context.Context, params *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error)
listQueuesFunc func(ctx context.Context, params *sqs.ListQueuesInput) (*sqs.ListQueuesOutput, error)
}
func (m *mockSQSClient) SendMessage(
ctx context.Context, params *sqs.SendMessageInput, _ ...func(*sqs.Options),
) (*sqs.SendMessageOutput, error) {
if m.sendMessageFunc != nil {
return m.sendMessageFunc(ctx, params)
}
return &sqs.SendMessageOutput{MessageId: aws.String("test-message-id")}, nil
}
func (m *mockSQSClient) ReceiveMessage(
ctx context.Context, params *sqs.ReceiveMessageInput, _ ...func(*sqs.Options),
) (*sqs.ReceiveMessageOutput, error) {
if m.receiveMessageFunc != nil {
return m.receiveMessageFunc(ctx, params)
}
return &sqs.ReceiveMessageOutput{
Messages: []types.Message{
{
MessageId: aws.String("msg-123"),
Body: aws.String(`{"test":"data"}`),
ReceiptHandle: aws.String("receipt-handle-123"),
},
},
}, nil
}
func (m *mockSQSClient) DeleteMessage(
ctx context.Context, params *sqs.DeleteMessageInput, _ ...func(*sqs.Options),
) (*sqs.DeleteMessageOutput, error) {
if m.deleteMessageFunc != nil {
return m.deleteMessageFunc(ctx, params)
}
return &sqs.DeleteMessageOutput{}, nil
}
func (m *mockSQSClient) CreateQueue(
ctx context.Context, params *sqs.CreateQueueInput, _ ...func(*sqs.Options),
) (*sqs.CreateQueueOutput, error) {
if m.createQueueFunc != nil {
return m.createQueueFunc(ctx, params)
}
return &sqs.CreateQueueOutput{QueueUrl: aws.String("http://localhost:4566/queue/test")}, nil
}
func (m *mockSQSClient) DeleteQueue(
ctx context.Context, params *sqs.DeleteQueueInput, _ ...func(*sqs.Options),
) (*sqs.DeleteQueueOutput, error) {
if m.deleteQueueFunc != nil {
return m.deleteQueueFunc(ctx, params)
}
return &sqs.DeleteQueueOutput{}, nil
}
//nolint:revive,staticcheck // GetQueueUrl matches AWS SDK method name
func (m *mockSQSClient) GetQueueUrl(
ctx context.Context, params *sqs.GetQueueUrlInput, _ ...func(*sqs.Options),
) (*sqs.GetQueueUrlOutput, error) {
if m.getQueueURLFunc != nil {
return m.getQueueURLFunc(ctx, params)
}
return &sqs.GetQueueUrlOutput{
QueueUrl: aws.String("http://localhost:4566/queue/" + *params.QueueName),
}, nil
}
func (m *mockSQSClient) ListQueues(
ctx context.Context, params *sqs.ListQueuesInput, _ ...func(*sqs.Options),
) (*sqs.ListQueuesOutput, error) {
if m.listQueuesFunc != nil {
return m.listQueuesFunc(ctx, params)
}
return &sqs.ListQueuesOutput{}, nil
}
// Helper to create a connected client with mock for testing.
func newTestClient(mockClient *mockSQSClient) *Client {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
client.UseMetrics(NewMockMetrics())
client.conn = mockClient
return client
}
// Test errors - static errors for testing purposes.
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/sqs.go
================================================
package sqs
import (
"context"
"errors"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
var (
errClientNotConnected = errors.New("sqs client not connected")
errQueueNotFound = errors.New("sqs queue not found")
errEmptyQueueName = errors.New("queue name cannot be empty")
)
// Config holds the configuration for the SQS client.
type Config struct {
Region string // Region is the AWS region where the SQS queue is located.Example: "us-east-1", "eu-west-1"
Endpoint string // Endpoint is the custom endpoint URL for SQS.
AccessKeyID string // AccessKeyID is the AWS access key ID for authentication.
SecretAccessKey string // SecretAccessKey is the AWS secret access key for authentication.
SessionToken string // SessionToken is the AWS session token for temporary credentials.
}
// Internal configuration with sensible defaults managed by the framework.
const (
defaultMaxMessages = int32(1)
defaultWaitTimeSeconds = int32(20)
defaultVisibilityTimeout = int32(30)
defaultRetryDuration = 5 * time.Second
defaultQueryTimeout = 30 * time.Second
defaultQueryMaxMessages = int32(10)
)
// Client represents an SQS client that implements the pubsub.Client interface.
type Client struct {
conn sqsClient
cfg *Config
logger pubsub.Logger
metrics Metrics
tracer trace.Tracer
// queueURLCache caches queue URLs to avoid repeated GetQueueUrl API calls.
queueURLCache map[string]string
cacheMu sync.RWMutex
connMu sync.Mutex
isRetrying atomic.Bool
}
// New creates a new SQS client with the provided configuration.
// The client is not connected until Connect() is called.
func New(cfg *Config) *Client {
if cfg == nil {
cfg = &Config{}
}
return &Client{
cfg: cfg,
queueURLCache: make(map[string]string),
}
}
// UseLogger sets the logger for the SQS client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(pubsub.Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the SQS client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the SQS client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Connect establishes a connection to AWS SQS.
func (c *Client) Connect() {
c.connMu.Lock()
defer c.connMu.Unlock()
if c.logger == nil {
return
}
if c.cfg.Region == "" {
c.logger.Error("SQS region is required")
return
}
c.logger.Debugf("connecting to AWS SQS in region: %s", c.cfg.Region)
ctx := context.Background()
awsCfg, err := c.loadAWSConfig(ctx)
if err != nil {
c.logger.Errorf("failed to load AWS config: %v", err)
return
}
// Create SQS client with custom endpoint if provided.
opts := func(o *sqs.Options) {
if c.cfg.Endpoint != "" {
o.BaseEndpoint = aws.String(c.cfg.Endpoint)
}
}
c.conn = sqs.NewFromConfig(awsCfg, opts)
// Verify connection by listing queues
_, err = c.conn.ListQueues(ctx, &sqs.ListQueuesInput{
MaxResults: aws.Int32(1),
})
if err != nil {
c.logger.Errorf("failed to connect to SQS: %v", err)
c.conn = nil
// Only start retry goroutine if not already retrying
if c.isRetrying.CompareAndSwap(false, true) {
go c.retryConnect()
}
return
}
c.logger.Logf("connected to AWS SQS in region: %s", c.cfg.Region)
}
// loadAWSConfig loads the AWS configuration with credentials.
func (c *Client) loadAWSConfig(ctx context.Context) (aws.Config, error) {
var opts []func(*config.LoadOptions) error
opts = append(opts, config.WithRegion(c.cfg.Region))
// Use static credentials if provided
if c.cfg.AccessKeyID != "" && c.cfg.SecretAccessKey != "" {
staticCreds := credentials.NewStaticCredentialsProvider(
c.cfg.AccessKeyID,
c.cfg.SecretAccessKey,
c.cfg.SessionToken,
)
opts = append(opts, config.WithCredentialsProvider(staticCreds))
}
return config.LoadDefaultConfig(ctx, opts...)
}
// retryConnect attempts to reconnect to SQS on failure.
func (c *Client) retryConnect() {
defer c.isRetrying.Store(false)
for {
time.Sleep(defaultRetryDuration)
c.logger.Debugf("retrying connection to SQS...")
c.connectInternal()
if c.isConnected() {
c.logger.Logf("successfully reconnected to SQS")
return
}
}
}
// connectInternal establishes connection without spawning retry goroutine.
func (c *Client) connectInternal() {
c.connMu.Lock()
defer c.connMu.Unlock()
ctx := context.Background()
awsCfg, err := c.loadAWSConfig(ctx)
if err != nil {
return
}
opts := func(o *sqs.Options) {
if c.cfg.Endpoint != "" {
o.BaseEndpoint = aws.String(c.cfg.Endpoint)
}
}
c.conn = sqs.NewFromConfig(awsCfg, opts)
_, err = c.conn.ListQueues(ctx, &sqs.ListQueuesInput{
MaxResults: aws.Int32(1),
})
if err != nil {
c.conn = nil
}
}
// isConnected checks if the client is connected.
func (c *Client) isConnected() bool {
c.connMu.Lock()
defer c.connMu.Unlock()
return c.conn != nil
}
// Publish sends a message to the specified SQS queue.
// The topic parameter is the queue name (not the full URL).
func (c *Client) Publish(ctx context.Context, topic string, message []byte) error {
if !c.isConnected() {
return errClientNotConnected
}
if topic == "" {
return errEmptyQueueName
}
ctx, span, traceAttrs := startPublishSpan(ctx, topic)
defer span.End()
c.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
queueURL, err := c.getQueueURL(ctx, topic)
if err != nil {
c.logger.Errorf("failed to get queue URL for %s: %v", topic, err)
return err
}
start := time.Now()
input := &sqs.SendMessageInput{
QueueUrl: aws.String(queueURL),
MessageBody: aws.String(string(message)),
MessageAttributes: traceAttrs,
}
result, err := c.conn.SendMessage(ctx, input)
if err != nil {
c.logger.Errorf("failed to publish message to queue %s: %v", topic, err)
return err
}
c.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: c.cfg.Region,
PubSubBackend: "SQS",
Time: time.Since(start).Microseconds(),
})
c.logger.Debugf("message published to queue %s with ID: %s", topic, *result.MessageId)
c.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
// Subscribe receives a single message from the specified SQS queue.
// The topic parameter is the queue name (not the full URL).
func (c *Client) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
if !c.isConnected() {
time.Sleep(defaultRetryDuration)
return nil, errClientNotConnected
}
if topic == "" {
return nil, errEmptyQueueName
}
// Span will be created after fetching message to access attributes for span links
var span trace.Span
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_total_count", "topic", topic)
queueURL, err := c.getQueueURL(ctx, topic)
if err != nil {
// Don't log for context cancellation or if it's a transient error
if !isContextCanceled(err) && !isConnectionError(err) {
c.logger.Errorf("queue %s not found: %v", topic, err)
}
return nil, err
}
start := time.Now()
input := &sqs.ReceiveMessageInput{
QueueUrl: aws.String(queueURL),
MaxNumberOfMessages: defaultMaxMessages,
WaitTimeSeconds: defaultWaitTimeSeconds,
VisibilityTimeout: defaultVisibilityTimeout,
MessageAttributeNames: []string{
"All",
},
MessageSystemAttributeNames: []types.MessageSystemAttributeName{
types.MessageSystemAttributeNameAll,
},
}
result, err := c.conn.ReceiveMessage(ctx, input)
if err != nil {
// Only log non-transient errors, avoid flooding logs
if !isContextCanceled(err) && !isConnectionError(err) {
c.logger.Errorf("failed to receive message from queue %s: %v", topic, err)
}
return nil, err
}
if len(result.Messages) == 0 {
return nil, nil // No messages available
}
sqsMsg := result.Messages[0]
duration := time.Since(start)
// Create span with links to producer span from message attributes
ctx, span = startSubscribeSpan(ctx, topic, sqsMsg.MessageAttributes)
defer span.End()
// Create pubsub message
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte(*sqsMsg.Body)
msg.MetaData = sqsMsg.MessageAttributes
msg.Committer = newMessage(
*sqsMsg.ReceiptHandle,
queueURL,
c.conn,
c.logger,
)
c.logger.Debug(&pubsub.Log{
Mode: "SUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(msg.Value),
Topic: topic,
Host: c.cfg.Region,
PubSubBackend: "SQS",
Time: duration.Microseconds(),
})
c.metrics.IncrementCounter(ctx, "app_pubsub_subscribe_success_count", "topic", topic)
return msg, nil
}
// CreateTopic creates a new SQS queue with the specified name.
// In SQS terminology, this creates a queue, not a topic.
func (c *Client) CreateTopic(ctx context.Context, name string) error {
if !c.isConnected() {
return errClientNotConnected
}
if name == "" {
return errEmptyQueueName
}
c.logger.Debugf("creating SQS queue: %s", name)
_, err := c.conn.CreateQueue(ctx, &sqs.CreateQueueInput{
QueueName: aws.String(name),
})
if err != nil {
c.logger.Errorf("failed to create queue %s: %v", name, err)
return err
}
c.logger.Logf("SQS queue created: %s", name)
return nil
}
// DeleteTopic deletes an SQS queue with the specified name.
func (c *Client) DeleteTopic(ctx context.Context, name string) error {
if !c.isConnected() {
return errClientNotConnected
}
if name == "" {
return errEmptyQueueName
}
queueURL, err := c.getQueueURL(ctx, name)
if err != nil {
return err
}
c.logger.Debugf("deleting SQS queue: %s", name)
_, err = c.conn.DeleteQueue(ctx, &sqs.DeleteQueueInput{
QueueUrl: aws.String(queueURL),
})
if err != nil {
c.logger.Errorf("failed to delete queue %s: %v", name, err)
return err
}
// Remove from cache
c.cacheMu.Lock()
delete(c.queueURLCache, name)
c.cacheMu.Unlock()
c.logger.Logf("SQS queue deleted: %s", name)
return nil
}
// Query retrieves multiple messages from an SQS queue.
// Args: [0] = limit (int32, max 10).
func (c *Client) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if !c.isConnected() {
return nil, errClientNotConnected
}
if query == "" {
return nil, errEmptyQueueName
}
queueURL, err := c.getQueueURL(ctx, query)
if err != nil {
return nil, err
}
limit := parseQueryArgs(args...)
// Use provided context or add default timeout
readCtx := ctx
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
var cancel context.CancelFunc
readCtx, cancel = context.WithTimeout(ctx, defaultQueryTimeout)
defer cancel()
}
result, err := c.conn.ReceiveMessage(readCtx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(queueURL),
MaxNumberOfMessages: limit,
WaitTimeSeconds: defaultWaitTimeSeconds,
})
if err != nil {
return nil, err
}
if len(result.Messages) == 0 {
return nil, nil
}
// Combine all message bodies
messages := make([]string, 0, len(result.Messages))
for _, msg := range result.Messages {
messages = append(messages, *msg.Body)
}
return []byte("[" + strings.Join(messages, ",") + "]"), nil
}
// Close closes the SQS client connection.
// SQS client doesn't require explicit closing, but we implement this for interface compliance.
func (c *Client) Close() error {
c.connMu.Lock()
defer c.connMu.Unlock()
c.logger.Debug("closing SQS client")
c.conn = nil
return nil
}
// getQueueURL retrieves the queue URL for a given queue name.
// It caches the result to avoid repeated API calls.
func (c *Client) getQueueURL(ctx context.Context, queueName string) (string, error) {
// Check cache first
c.cacheMu.RLock()
if url, ok := c.queueURLCache[queueName]; ok {
c.cacheMu.RUnlock()
return url, nil
}
c.cacheMu.RUnlock()
// Get queue URL from SQS
result, err := c.conn.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{
QueueName: aws.String(queueName),
})
if err != nil {
return "", errQueueNotFound
}
// Cache the result
c.cacheMu.Lock()
c.queueURLCache[queueName] = *result.QueueUrl
c.cacheMu.Unlock()
return *result.QueueUrl, nil
}
// parseQueryArgs parses the query arguments for Query method.
func parseQueryArgs(args ...any) int32 {
if len(args) > 0 {
if l, ok := args[0].(int32); ok && l > 0 && l <= 10 {
return l
}
}
return defaultQueryMaxMessages
}
// isContextCanceled checks if the error is due to context cancellation.
// This is used to suppress error logs during graceful shutdown.
func isContextCanceled(err error) bool {
if err == nil {
return false
}
// Check for standard context errors
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return true
}
// Check error message for "canceled" (AWS SDK wraps context errors)
errMsg := err.Error()
return strings.Contains(errMsg, "context canceled") ||
strings.Contains(errMsg, "context deadline exceeded") ||
strings.Contains(errMsg, "canceled")
}
// isConnectionError checks if the error is a connection-related error.
// These errors are transient and don't need to be logged as errors during normal retry flow.
func isConnectionError(err error) bool {
if err == nil {
return false
}
errMsg := err.Error()
return strings.Contains(errMsg, "connection refused") ||
strings.Contains(errMsg, "no such host") ||
strings.Contains(errMsg, "network is unreachable") ||
strings.Contains(errMsg, "exceeded maximum number of attempts")
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/sqs_test.go
================================================
package sqs
import (
"context"
"errors"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// Test errors - static errors for testing purposes.
var (
errWrappedContextCanceled = errors.New("operation failed: context canceled")
errWrappedDeadlineExceeded = errors.New("timeout: context deadline exceeded")
errRequestCanceled = errors.New("request was canceled")
errGeneric = errors.New("some other error")
errConnectionRefused = errors.New("dial tcp: connection refused")
errNoSuchHost = errors.New("dial tcp: no such host")
errNetworkUnreachable = errors.New("dial tcp: network is unreachable")
errMaxAttemptsExceeded = errors.New("exceeded maximum number of attempts")
errMockSendMessage = errors.New("mock send message error")
errMockReceiveMessage = errors.New("mock receive message error")
errMockCreateQueue = errors.New("mock create queue error")
errMockDeleteQueue = errors.New("mock delete queue error")
errMockGetQueueURL = errors.New("mock get queue url error")
errMockDeleteFailed = errors.New("delete failed")
errMockListQueues = errors.New("list queues failed")
)
func TestNew(t *testing.T) {
t.Run("with nil config", func(t *testing.T) {
client := New(nil)
require.NotNil(t, client)
assert.NotNil(t, client.cfg)
assert.NotNil(t, client.queueURLCache)
})
t.Run("with config", func(t *testing.T) {
cfg := &Config{
Region: "us-east-1",
Endpoint: "http://localhost:4566",
AccessKeyID: "test-key",
SecretAccessKey: "test-secret",
SessionToken: "test-token",
}
client := New(cfg)
require.NotNil(t, client)
assert.Equal(t, "us-east-1", client.cfg.Region)
assert.Equal(t, "http://localhost:4566", client.cfg.Endpoint)
assert.Equal(t, "test-key", client.cfg.AccessKeyID)
assert.Equal(t, "test-secret", client.cfg.SecretAccessKey)
assert.Equal(t, "test-token", client.cfg.SessionToken)
})
}
func TestClient_UseLogger(t *testing.T) {
client := New(&Config{})
logger := NewMockLogger()
client.UseLogger(logger)
assert.NotNil(t, client.logger)
client.UseLogger("invalid")
assert.Equal(t, logger, client.logger)
client.UseLogger(nil)
assert.Equal(t, logger, client.logger)
}
func TestClient_UseMetrics(t *testing.T) {
client := New(&Config{})
metrics := NewMockMetrics()
client.UseMetrics(metrics)
assert.NotNil(t, client.metrics)
client.UseMetrics("invalid")
assert.Equal(t, metrics, client.metrics)
client.UseMetrics(nil)
assert.Equal(t, metrics, client.metrics)
}
func TestClient_UseTracer(t *testing.T) {
client := New(&Config{})
client.UseTracer("invalid")
assert.Nil(t, client.tracer)
client.UseTracer(nil)
assert.Nil(t, client.tracer)
// Test with valid tracer
tracer := otel.GetTracerProvider().Tracer("test")
client.UseTracer(tracer)
assert.NotNil(t, client.tracer)
}
func TestClient_Connect_NoRegion(t *testing.T) {
client := New(&Config{})
client.UseLogger(NewMockLogger())
client.Connect()
assert.Nil(t, client.conn)
}
func TestClient_Connect_NoLogger(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.Connect()
assert.Nil(t, client.conn)
}
func TestClient_isConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
assert.False(t, client.isConnected())
client.conn = &mockSQSClient{}
assert.True(t, client.isConnected())
}
func TestClient_Publish_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
client.UseMetrics(NewMockMetrics())
err := client.Publish(context.Background(), "test-queue", []byte("test message"))
assert.ErrorIs(t, err, errClientNotConnected)
}
func TestClient_Publish_EmptyTopic(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.Publish(context.Background(), "", []byte("test message"))
assert.ErrorIs(t, err, errEmptyQueueName)
}
func TestClient_Publish_Success(t *testing.T) {
mockClient := &mockSQSClient{}
client := newTestClient(mockClient)
err := client.Publish(context.Background(), "test-queue", []byte(`{"test":"data"}`))
assert.NoError(t, err)
}
func TestClient_Publish_GetQueueURLError(t *testing.T) {
mockClient := &mockSQSClient{
getQueueURLFunc: func(context.Context, *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) {
return nil, errMockGetQueueURL
},
}
client := newTestClient(mockClient)
err := client.Publish(context.Background(), "test-queue", []byte("test"))
assert.ErrorIs(t, err, errQueueNotFound)
}
func TestClient_Publish_SendMessageError(t *testing.T) {
mockClient := &mockSQSClient{
sendMessageFunc: func(context.Context, *sqs.SendMessageInput) (*sqs.SendMessageOutput, error) {
return nil, errMockSendMessage
},
}
client := newTestClient(mockClient)
err := client.Publish(context.Background(), "test-queue", []byte("test"))
assert.Error(t, err)
}
func TestClient_Subscribe_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
client.UseMetrics(NewMockMetrics())
ctx, cancel := context.WithCancel(context.Background())
cancel()
msg, err := client.Subscribe(ctx, "test-queue")
require.ErrorIs(t, err, errClientNotConnected)
assert.Nil(t, msg)
}
func TestClient_Subscribe_EmptyTopic(t *testing.T) {
client := newTestClient(&mockSQSClient{})
msg, err := client.Subscribe(context.Background(), "")
require.ErrorIs(t, err, errEmptyQueueName)
assert.Nil(t, msg)
}
func TestClient_Subscribe_Success(t *testing.T) {
mockClient := &mockSQSClient{}
client := newTestClient(mockClient)
msg, err := client.Subscribe(context.Background(), "test-queue")
require.NoError(t, err)
require.NotNil(t, msg)
assert.Equal(t, "test-queue", msg.Topic)
assert.JSONEq(t, `{"test":"data"}`, string(msg.Value))
}
func TestClient_Subscribe_NoMessages(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return &sqs.ReceiveMessageOutput{Messages: []types.Message{}}, nil
},
}
client := newTestClient(mockClient)
msg, err := client.Subscribe(context.Background(), "test-queue")
require.NoError(t, err)
assert.Nil(t, msg)
}
func TestClient_Subscribe_GetQueueURLError(t *testing.T) {
mockClient := &mockSQSClient{
getQueueURLFunc: func(context.Context, *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) {
return nil, errMockGetQueueURL
},
}
client := newTestClient(mockClient)
msg, err := client.Subscribe(context.Background(), "test-queue")
require.ErrorIs(t, err, errQueueNotFound)
assert.Nil(t, msg)
}
func TestClient_Subscribe_ReceiveMessageError(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return nil, errMockReceiveMessage
},
}
client := newTestClient(mockClient)
msg, err := client.Subscribe(context.Background(), "test-queue")
require.Error(t, err)
assert.Nil(t, msg)
}
func TestClient_Subscribe_ContextCanceled(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return nil, context.Canceled
},
}
client := newTestClient(mockClient)
msg, err := client.Subscribe(context.Background(), "test-queue")
require.ErrorIs(t, err, context.Canceled)
assert.Nil(t, msg)
}
func TestClient_CreateTopic_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
err := client.CreateTopic(context.Background(), "test-queue")
assert.ErrorIs(t, err, errClientNotConnected)
}
func TestClient_CreateTopic_EmptyName(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.CreateTopic(context.Background(), "")
assert.ErrorIs(t, err, errEmptyQueueName)
}
func TestClient_CreateTopic_Success(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.CreateTopic(context.Background(), "test-queue")
assert.NoError(t, err)
}
func TestClient_CreateTopic_Error(t *testing.T) {
mockClient := &mockSQSClient{
createQueueFunc: func(context.Context, *sqs.CreateQueueInput) (*sqs.CreateQueueOutput, error) {
return nil, errMockCreateQueue
},
}
client := newTestClient(mockClient)
err := client.CreateTopic(context.Background(), "test-queue")
assert.Error(t, err)
}
func TestClient_DeleteTopic_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
err := client.DeleteTopic(context.Background(), "test-queue")
assert.ErrorIs(t, err, errClientNotConnected)
}
func TestClient_DeleteTopic_EmptyName(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.DeleteTopic(context.Background(), "")
assert.ErrorIs(t, err, errEmptyQueueName)
}
func TestClient_DeleteTopic_Success(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.DeleteTopic(context.Background(), "test-queue")
assert.NoError(t, err)
}
func TestClient_DeleteTopic_GetQueueURLError(t *testing.T) {
mockClient := &mockSQSClient{
getQueueURLFunc: func(context.Context, *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) {
return nil, errMockGetQueueURL
},
}
client := newTestClient(mockClient)
err := client.DeleteTopic(context.Background(), "test-queue")
assert.ErrorIs(t, err, errQueueNotFound)
}
func TestClient_DeleteTopic_DeleteQueueError(t *testing.T) {
mockClient := &mockSQSClient{
deleteQueueFunc: func(context.Context, *sqs.DeleteQueueInput) (*sqs.DeleteQueueOutput, error) {
return nil, errMockDeleteQueue
},
}
client := newTestClient(mockClient)
err := client.DeleteTopic(context.Background(), "test-queue")
assert.Error(t, err)
}
func TestClient_Query_NotConnected(t *testing.T) {
client := New(&Config{Region: "us-east-1"})
client.UseLogger(NewMockLogger())
result, err := client.Query(context.Background(), "test-queue")
require.ErrorIs(t, err, errClientNotConnected)
assert.Nil(t, result)
}
func TestClient_Query_EmptyQuery(t *testing.T) {
client := newTestClient(&mockSQSClient{})
result, err := client.Query(context.Background(), "")
require.ErrorIs(t, err, errEmptyQueueName)
assert.Nil(t, result)
}
func TestClient_Query_Success(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return &sqs.ReceiveMessageOutput{
Messages: []types.Message{
{Body: aws.String(`{"id":1}`)},
{Body: aws.String(`{"id":2}`)},
},
}, nil
},
}
client := newTestClient(mockClient)
result, err := client.Query(context.Background(), "test-queue", int32(5))
require.NoError(t, err)
assert.Equal(t, `[{"id":1},{"id":2}]`, string(result))
}
func TestClient_Query_NoMessages(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return &sqs.ReceiveMessageOutput{Messages: []types.Message{}}, nil
},
}
client := newTestClient(mockClient)
result, err := client.Query(context.Background(), "test-queue")
require.NoError(t, err)
assert.Nil(t, result)
}
func TestClient_Query_GetQueueURLError(t *testing.T) {
mockClient := &mockSQSClient{
getQueueURLFunc: func(context.Context, *sqs.GetQueueUrlInput) (*sqs.GetQueueUrlOutput, error) {
return nil, errMockGetQueueURL
},
}
client := newTestClient(mockClient)
result, err := client.Query(context.Background(), "test-queue")
require.ErrorIs(t, err, errQueueNotFound)
assert.Nil(t, result)
}
func TestClient_Query_ReceiveMessageError(t *testing.T) {
mockClient := &mockSQSClient{
receiveMessageFunc: func(context.Context, *sqs.ReceiveMessageInput) (*sqs.ReceiveMessageOutput, error) {
return nil, errMockReceiveMessage
},
}
client := newTestClient(mockClient)
result, err := client.Query(context.Background(), "test-queue")
require.Error(t, err)
assert.Nil(t, result)
}
func TestClient_Close(t *testing.T) {
client := newTestClient(&mockSQSClient{})
err := client.Close()
require.NoError(t, err)
assert.False(t, client.isConnected())
}
func TestClient_getQueueURL_Cached(t *testing.T) {
mockClient := &mockSQSClient{}
client := newTestClient(mockClient)
// First call - should call GetQueueUrl
url1, err := client.getQueueURL(context.Background(), "test-queue")
require.NoError(t, err)
assert.Contains(t, url1, "test-queue")
// Second call - should use cache
url2, err := client.getQueueURL(context.Background(), "test-queue")
require.NoError(t, err)
assert.Equal(t, url1, url2)
}
func TestParseQueryArgs(t *testing.T) {
tests := []struct {
name string
args []any
expected int32
}{
{"no args", nil, defaultQueryMaxMessages},
{"empty args", []any{}, defaultQueryMaxMessages},
{"valid int32 limit 1", []any{int32(1)}, 1},
{"valid int32 limit 5", []any{int32(5)}, 5},
{"valid int32 limit 10", []any{int32(10)}, 10},
{"int32 limit exceeds max", []any{int32(15)}, defaultQueryMaxMessages},
{"invalid type int", []any{5}, defaultQueryMaxMessages},
{"invalid type string", []any{"invalid"}, defaultQueryMaxMessages},
{"int32 zero", []any{int32(0)}, defaultQueryMaxMessages},
{"int32 negative", []any{int32(-1)}, defaultQueryMaxMessages},
{"multiple args uses first", []any{int32(5), int32(3)}, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseQueryArgs(tt.args...)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsContextCanceled(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"nil error", nil, false},
{"context canceled", context.Canceled, true},
{"context deadline exceeded", context.DeadlineExceeded, true},
{"wrapped context canceled", errWrappedContextCanceled, true},
{"wrapped deadline exceeded", errWrappedDeadlineExceeded, true},
{"canceled keyword", errRequestCanceled, true},
{"other error", errClientNotConnected, false},
{"generic error", errGeneric, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isContextCanceled(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestIsConnectionError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"nil error", nil, false},
{"connection refused", errConnectionRefused, true},
{"no such host", errNoSuchHost, true},
{"network unreachable", errNetworkUnreachable, true},
{"max attempts exceeded", errMaxAttemptsExceeded, true},
{"queue not found", errQueueNotFound, false},
{"client not connected", errClientNotConnected, false},
{"generic error", errGeneric, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isConnectionError(tt.err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestStartPublishSpan_CreatesValidSpan(t *testing.T) {
ctx, span, attrs := startPublishSpan(context.Background(), "test-queue")
assert.NotNil(t, ctx)
assert.NotNil(t, span)
assert.Implements(t, (*trace.Span)(nil), span)
assert.NotNil(t, attrs)
span.End()
}
func TestErrors(t *testing.T) {
assert.Equal(t, "sqs client not connected", errClientNotConnected.Error())
assert.Equal(t, "sqs queue not found", errQueueNotFound.Error())
assert.Equal(t, "queue name cannot be empty", errEmptyQueueName.Error())
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/tracing.go
================================================
package sqs
import (
"context"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
const tracerName = "gofr-sqs"
// attributeCarrier implements propagation.TextMapCarrier for SQS message attributes.
type attributeCarrier map[string]types.MessageAttributeValue
// Get returns the string value for a given key from SQS message attributes.
func (c attributeCarrier) Get(key string) string {
if attr, ok := c[key]; ok && attr.StringValue != nil {
return *attr.StringValue
}
return ""
}
// Set sets a key-value pair in the SQS message attributes as a String type.
func (c attributeCarrier) Set(key, value string) {
c[key] = types.MessageAttributeValue{
DataType: aws.String("String"),
StringValue: aws.String(value),
}
}
// Keys returns all keys in the SQS message attributes.
func (c attributeCarrier) Keys() []string {
keys := make([]string, 0, len(c))
for k := range c {
keys = append(keys, k)
}
return keys
}
// injectTraceContext injects the current trace context into SQS message attributes.
// This allows the consumer to extract the trace context and create span links.
func injectTraceContext(ctx context.Context, attrs map[string]types.MessageAttributeValue) map[string]types.MessageAttributeValue {
if attrs == nil {
attrs = make(map[string]types.MessageAttributeValue)
}
carrier := attributeCarrier(attrs)
otel.GetTextMapPropagator().Inject(ctx, carrier)
return attrs
}
// extractTraceLinks extracts the trace context from SQS message attributes
// and returns span links to the producer span.
// If no trace context is found, returns nil (creating an orphan span).
func extractTraceLinks(attrs map[string]types.MessageAttributeValue) []trace.Link {
if len(attrs) == 0 {
return nil
}
carrier := attributeCarrier(attrs)
// Extract the context from attributes
extractedCtx := otel.GetTextMapPropagator().Extract(context.Background(), carrier)
// Get span context from extracted context
spanCtx := trace.SpanContextFromContext(extractedCtx)
// If valid span context exists, create a link to it
if spanCtx.IsValid() {
return []trace.Link{
{
SpanContext: spanCtx,
},
}
}
return nil
}
// startPublishSpan creates a new span for publishing with trace context injection.
// Returns the updated context, the span, and message attributes with injected trace context.
func startPublishSpan(ctx context.Context, topic string) (context.Context, trace.Span, map[string]types.MessageAttributeValue) {
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindProducer),
trace.WithAttributes(
attribute.String("messaging.system", "aws_sqs"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "publish"),
),
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "sqs-publish", opts...)
// Inject trace context into message attributes
attrs := injectTraceContext(ctx, nil)
return ctx, span, attrs
}
// startSubscribeSpan creates a new span for subscribing with links to the producer span.
// If trace context exists in message attributes, creates a span linked to the producer.
// Otherwise, creates an orphan span (new trace).
func startSubscribeSpan(ctx context.Context, topic string, msgAttrs map[string]types.MessageAttributeValue) (context.Context, trace.Span) {
links := extractTraceLinks(msgAttrs)
opts := []trace.SpanStartOption{
trace.WithSpanKind(trace.SpanKindConsumer),
trace.WithAttributes(
attribute.String("messaging.system", "aws_sqs"),
attribute.String("messaging.destination.name", topic),
attribute.String("messaging.operation", "receive"),
),
}
if len(links) > 0 {
opts = append(opts, trace.WithLinks(links...))
}
ctx, span := otel.GetTracerProvider().Tracer(tracerName).Start(ctx, "sqs-subscribe", opts...)
return ctx, span
}
================================================
FILE: pkg/gofr/datasource/pubsub/sqs/tracing_test.go
================================================
package sqs
import (
"context"
"testing"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/trace/tracetest"
)
func TestAttributeCarrier_GetSetKeys(t *testing.T) {
carrier := make(attributeCarrier)
// Test Set
carrier.Set("traceparent", "00-1234567890abcdef-fedcba0987654321-01")
carrier.Set("tracestate", "foo=bar")
// Test Get
assert.Equal(t, "00-1234567890abcdef-fedcba0987654321-01", carrier.Get("traceparent"))
assert.Equal(t, "foo=bar", carrier.Get("tracestate"))
assert.Empty(t, carrier.Get("nonexistent"))
// Test Keys
keys := carrier.Keys()
assert.Contains(t, keys, "traceparent")
assert.Contains(t, keys, "tracestate")
// Test Set updates existing key
carrier.Set("traceparent", "00-updated-value")
assert.Equal(t, "00-updated-value", carrier.Get("traceparent"))
}
func TestAttributeCarrier_GetNilStringValue(t *testing.T) {
carrier := attributeCarrier{
"key-no-string": {
DataType: aws.String("String"),
// StringValue is nil
},
}
assert.Empty(t, carrier.Get("key-no-string"))
}
func TestInjectTraceContext(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
attrs := injectTraceContext(ctx, nil)
require.NotNil(t, attrs)
traceparentAttr, ok := attrs["traceparent"]
require.True(t, ok, "traceparent attribute should be injected")
require.NotNil(t, traceparentAttr.StringValue)
assert.Contains(t, *traceparentAttr.StringValue, span.SpanContext().TraceID().String())
}
func TestInjectTraceContext_PreservesExistingAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span := tp.Tracer("test").Start(context.Background(), "test-span")
defer span.End()
existing := map[string]types.MessageAttributeValue{
"custom-attr": {
DataType: aws.String("String"),
StringValue: aws.String("custom-value"),
},
}
attrs := injectTraceContext(ctx, existing)
// Existing attribute should still be present
assert.Equal(t, "custom-value", *attrs["custom-attr"].StringValue)
// Traceparent should also be present
_, ok := attrs["traceparent"]
assert.True(t, ok, "traceparent should be injected alongside existing attributes")
}
func TestExtractTraceLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Create a producer span and inject context
ctx, producerSpan := tp.Tracer("test").Start(context.Background(), "producer-span")
attrs := injectTraceContext(ctx, nil)
producerSpan.End()
// Extract links from attributes
links := extractTraceLinks(attrs)
require.Len(t, links, 1, "should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), links[0].SpanContext.SpanID())
}
func TestExtractTraceLinks_NoAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
links := extractTraceLinks(nil)
assert.Nil(t, links, "should return nil for nil attributes")
}
func TestExtractTraceLinks_EmptyAttributes(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
links := extractTraceLinks(make(map[string]types.MessageAttributeValue))
assert.Nil(t, links, "should return nil for empty attributes")
}
func TestStartPublishSpan(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
ctx, span, attrs := startPublishSpan(context.Background(), "test-queue")
defer span.End()
require.NotNil(t, span)
assert.True(t, span.SpanContext().IsValid())
require.NotNil(t, ctx)
_, hasTraceparent := attrs["traceparent"]
assert.True(t, hasTraceparent, "attributes should contain traceparent")
}
func TestStartSubscribeSpan_WithLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
// Create producer span and get attributes
_, producerSpan, attrs := startPublishSpan(context.Background(), "test-queue")
producerSpan.End()
// Start subscribe span with attributes
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-queue", attrs)
subscribeSpan.End()
// Get recorded spans
spans := exporter.GetSpans()
require.GreaterOrEqual(t, len(spans), 2)
// Find subscribe span and verify links
var subSpan *tracetest.SpanStub
for i := range spans {
if spans[i].Name == "sqs-subscribe" {
subSpan = &spans[i]
break
}
}
require.NotNil(t, subSpan, "subscribe span should exist")
require.Len(t, subSpan.Links, 1, "subscribe span should have one link")
assert.Equal(t, producerSpan.SpanContext().TraceID(), subSpan.Links[0].SpanContext.TraceID())
assert.Equal(t, producerSpan.SpanContext().SpanID(), subSpan.Links[0].SpanContext.SpanID())
}
func TestStartSubscribeSpan_NoLinks(t *testing.T) {
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(sdktrace.WithSyncer(exporter))
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.TraceContext{})
defer func() {
_ = tp.Shutdown(context.Background())
}()
_, subscribeSpan := startSubscribeSpan(context.Background(), "test-queue", nil)
subscribeSpan.End()
spans := exporter.GetSpans()
require.Len(t, spans, 1)
assert.Empty(t, spans[0].Links, "orphan span should have no links")
}
================================================
FILE: pkg/gofr/datasource/redis/config.go
================================================
package redis
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource"
)
const (
defaultRedisPort = 6379
defaultPubSubDB = 15 // Highest default Redis database (0-15)
modePubSub = "pubsub"
modeStreams = "streams"
defaultPubSubBufferSize = 100
defaultPubSubQueryLimit = 10
defaultPubSubQueryTimeout = 5 * time.Second
)
// getRedisConfig builds the Redis Config struct from the provided [Config].
// It supports TLS configuration using the following environment variables:
//
// REDIS_TLS_ENABLED: set to "true" to enable TLS
// REDIS_TLS_CA_CERT: PEM-encoded CA certificate (string)
// REDIS_TLS_CERT: PEM-encoded client certificate (string or file path)
// REDIS_TLS_KEY: PEM-encoded client private key (string or file path)
//
// If TLS is enabled, the function sets up the [tls.Config] for the [Redis] client.
func getRedisConfig(c config.Config, logger datasource.Logger) *Config {
var redisConfig = &Config{}
redisConfig.HostName = c.Get("REDIS_HOST")
redisConfig.Username = c.Get("REDIS_USER")
redisConfig.Password = c.Get("REDIS_PASSWORD")
port, err := strconv.Atoi(c.Get("REDIS_PORT"))
if err != nil {
port = defaultRedisPort
}
redisConfig.Port = port
db, err := strconv.Atoi(c.Get("REDIS_DB"))
if err != nil {
db = 0 // default to DB 0 if not specified
}
redisConfig.DB = db
options := new(redis.Options)
options.Addr = fmt.Sprintf("%s:%d", redisConfig.HostName, redisConfig.Port)
options.Username = redisConfig.Username
options.Password = redisConfig.Password
options.DB = redisConfig.DB
// Parse PubSub config if PUBSUB_BACKEND=REDIS
if strings.EqualFold(c.Get("PUBSUB_BACKEND"), "REDIS") {
parsePubSubConfig(c, redisConfig)
}
if c.Get("REDIS_TLS_ENABLED") != "true" {
redisConfig.Options = options
return redisConfig
}
tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12}
if caCertPath := c.Get("REDIS_TLS_CA_CERT"); caCertPath != "" {
caCert, err := os.ReadFile(caCertPath)
if err != nil {
logger.Errorf("failed to read CA cert file: %v", err)
} else {
initializeCerts(logger, caCert, tlsConfig)
}
}
// Load client cert and key from file paths
certPath := c.Get("REDIS_TLS_CERT")
keyPath := c.Get("REDIS_TLS_KEY")
if certPath != "" && keyPath != "" {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
logger.Errorf("failed to load client cert/key pair: %v", err)
} else {
tlsConfig.Certificates = []tls.Certificate{cert}
}
}
options.TLSConfig = tlsConfig
redisConfig.TLS = tlsConfig
redisConfig.Options = options
return redisConfig
}
// parsePubSubConfig parses PubSub configuration from environment variables.
func parsePubSubConfig(c config.Config, redisConfig *Config) {
parsePubSubMode(c, redisConfig)
parsePubSubCommonConfig(c, redisConfig)
}
// parsePubSubMode parses the PubSub mode configuration.
func parsePubSubMode(c config.Config, redisConfig *Config) {
mode := strings.ToLower(c.GetOrDefault("REDIS_PUBSUB_MODE", modeStreams))
if mode != modeStreams && mode != modePubSub {
mode = modeStreams
}
redisConfig.PubSubMode = mode
// Parse Streams config if mode is streams
if mode == modeStreams {
configStreams(c, redisConfig)
}
}
// parsePubSubCommonConfig parses common PubSub configuration (buffer size, query timeout, query limit).
func parsePubSubCommonConfig(c config.Config, redisConfig *Config) {
// Buffer Size
bufferSizeStr := c.GetOrDefault("REDIS_PUBSUB_BUFFER_SIZE", strconv.Itoa(defaultPubSubBufferSize))
bufferSize, err := strconv.Atoi(bufferSizeStr)
if err != nil || bufferSize <= 0 {
redisConfig.PubSubBufferSize = defaultPubSubBufferSize
} else {
redisConfig.PubSubBufferSize = bufferSize
}
// Query Timeout
timeoutStr := c.GetOrDefault("REDIS_PUBSUB_QUERY_TIMEOUT", defaultPubSubQueryTimeout.String())
timeout, err := time.ParseDuration(timeoutStr)
if err != nil {
redisConfig.PubSubQueryTimeout = defaultPubSubQueryTimeout
} else {
redisConfig.PubSubQueryTimeout = timeout
}
// Query Limit
limitStr := c.GetOrDefault("REDIS_PUBSUB_QUERY_LIMIT", strconv.Itoa(defaultPubSubQueryLimit))
limit, err := strconv.Atoi(limitStr)
if err != nil || limit <= 0 {
redisConfig.PubSubQueryLimit = defaultPubSubQueryLimit
} else {
redisConfig.PubSubQueryLimit = limit
}
}
func configStreams(c config.Config, redisConfig *Config) {
streamsConfig := &StreamsConfig{
ConsumerGroup: c.Get("REDIS_STREAMS_CONSUMER_GROUP"),
ConsumerName: c.Get("REDIS_STREAMS_CONSUMER_NAME"),
PELRatio: 0.7, // Default: 70% PEL, 30% new messages
}
streamsConfig.Block = 1 * time.Second // default - reduced from 5s for better responsiveness
if blockStr := c.Get("REDIS_STREAMS_BLOCK_TIMEOUT"); blockStr != "" {
if block, err := time.ParseDuration(blockStr); err == nil {
streamsConfig.Block = block
}
}
if maxLenStr := c.Get("REDIS_STREAMS_MAXLEN"); maxLenStr != "" {
if maxLen, err := strconv.ParseInt(maxLenStr, 10, 64); err == nil {
streamsConfig.MaxLen = maxLen
}
}
// Parse PEL ratio (0.0-1.0)
if pelRatioStr := c.Get("REDIS_STREAMS_PEL_RATIO"); pelRatioStr != "" {
if pelRatio, err := strconv.ParseFloat(pelRatioStr, 64); err == nil {
// Validate range: 0.0 to 1.0
if pelRatio >= 0.0 && pelRatio <= 1.0 {
streamsConfig.PELRatio = pelRatio
}
}
}
redisConfig.PubSubStreamsConfig = streamsConfig
}
func initializeCerts(logger datasource.Logger, caCert []byte, tlsConfig *tls.Config) {
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
logger.Errorf("failed to append CA cert to pool")
} else {
tlsConfig.RootCAs = caCertPool
}
}
================================================
FILE: pkg/gofr/datasource/redis/config_test.go
================================================
package redis
import (
"crypto/tls"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/logging"
)
func testGetRedisConfig(t *testing.T, configMap map[string]string) *Config {
t.Helper()
mockLogger := logging.NewMockLogger(logging.ERROR)
mockConfig := config.NewMockConfig(configMap)
return getRedisConfig(mockConfig, mockLogger)
}
func TestGetRedisConfig_Defaults(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS", // Required to trigger PubSub config parsing
"REDIS_HOST": "localhost",
})
assert.Equal(t, "localhost", conf.HostName)
assert.Equal(t, defaultRedisPort, conf.Port)
assert.Equal(t, 0, conf.DB)
assert.Nil(t, conf.TLS)
// PubSubStreamsConfig is initialized when mode is streams (default)
assert.NotNil(t, conf.PubSubStreamsConfig)
assert.Equal(t, "streams", conf.PubSubMode) // Default mode is now streams
}
func TestGetRedisConfig_InvalidPortAndDB(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"REDIS_HOST": "localhost",
"REDIS_PORT": "invalid",
"REDIS_DB": "invalid",
})
assert.Equal(t, defaultRedisPort, conf.Port)
assert.Equal(t, 0, conf.DB)
}
func TestGetRedisConfig_TLS(t *testing.T) {
certFile, err := os.CreateTemp(t.TempDir(), "cert-*.pem")
require.NoError(t, err)
defer os.Remove(certFile.Name())
defer certFile.Close()
keyFile, err := os.CreateTemp(t.TempDir(), "key-*.pem")
require.NoError(t, err)
defer os.Remove(keyFile.Name())
defer keyFile.Close()
caFile, err := os.CreateTemp(t.TempDir(), "ca-*.pem")
require.NoError(t, err)
defer os.Remove(caFile.Name())
defer caFile.Close()
_, _ = certFile.WriteString("-----BEGIN CERTIFICATE-----\nMIID\n-----END CERTIFICATE-----")
_, _ = keyFile.WriteString("-----BEGIN PRIVATE KEY-----\nMIIE\n-----END PRIVATE KEY-----")
_, _ = caFile.WriteString("-----BEGIN CERTIFICATE-----\nMIID\n-----END CERTIFICATE-----")
conf := testGetRedisConfig(t, map[string]string{
"REDIS_HOST": "localhost",
"REDIS_TLS_ENABLED": "true",
"REDIS_TLS_CERT": certFile.Name(),
"REDIS_TLS_KEY": keyFile.Name(),
"REDIS_TLS_CA_CERT": caFile.Name(),
})
assert.NotNil(t, conf.TLS)
assert.Equal(t, uint16(tls.VersionTLS12), conf.TLS.MinVersion)
}
func TestGetRedisConfig_TLS_InvalidFiles(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"REDIS_HOST": "localhost",
"REDIS_TLS_ENABLED": "true",
"REDIS_TLS_CERT": "nonexistent_cert.pem",
"REDIS_TLS_KEY": "nonexistent_key.pem",
"REDIS_TLS_CA_CERT": "nonexistent_ca.pem",
})
assert.NotNil(t, conf.TLS)
assert.Empty(t, conf.TLS.Certificates)
assert.Nil(t, conf.TLS.RootCAs)
}
func TestGetRedisConfig_PubSubStreams(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_CONSUMER_NAME": "myconsumer",
"REDIS_STREAMS_MAXLEN": "1000",
"REDIS_STREAMS_BLOCK_TIMEOUT": "2s",
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
assert.Equal(t, "mygroup", conf.PubSubStreamsConfig.ConsumerGroup)
assert.Equal(t, "myconsumer", conf.PubSubStreamsConfig.ConsumerName)
assert.Equal(t, int64(1000), conf.PubSubStreamsConfig.MaxLen)
assert.Equal(t, 2*time.Second, conf.PubSubStreamsConfig.Block)
}
func TestGetRedisConfig_PubSubStreams_Defaults(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
assert.Equal(t, "mygroup", conf.PubSubStreamsConfig.ConsumerGroup)
assert.Empty(t, conf.PubSubStreamsConfig.ConsumerName)
assert.Equal(t, int64(0), conf.PubSubStreamsConfig.MaxLen)
assert.Equal(t, 1*time.Second, conf.PubSubStreamsConfig.Block) // Default block (reduced from 5s for better responsiveness)
assert.InEpsilon(t, 0.7, conf.PubSubStreamsConfig.PELRatio, 0.001) // Default PEL ratio
}
func TestGetRedisConfig_PubSubStreams_InvalidValues(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_MAXLEN": "invalid",
"REDIS_STREAMS_BLOCK_TIMEOUT": "invalid",
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
assert.Equal(t, int64(0), conf.PubSubStreamsConfig.MaxLen)
assert.Equal(t, 1*time.Second, conf.PubSubStreamsConfig.Block) // Falls back to default when invalid
assert.InEpsilon(t, 0.7, conf.PubSubStreamsConfig.PELRatio, 0.001) // Falls back to default when invalid
}
func TestGetRedisConfig_PubSubStreams_PELRatio(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_PEL_RATIO": "0.5",
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
assert.InEpsilon(t, 0.5, conf.PubSubStreamsConfig.PELRatio, 0.001)
}
func TestGetRedisConfig_PubSubStreams_PELRatio_Invalid(t *testing.T) {
// Test invalid ratio (out of range) - should fall back to default
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_PEL_RATIO": "1.5", // Invalid: > 1.0
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
assert.InEpsilon(t, 0.7, conf.PubSubStreamsConfig.PELRatio, 0.001) // Should fall back to default
// Test invalid ratio (negative) - should fall back to default
conf2 := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_PEL_RATIO": "-0.1", // Invalid: < 0.0
})
require.NotNil(t, conf2.PubSubStreamsConfig)
assert.InEpsilon(t, 0.7, conf2.PubSubStreamsConfig.PELRatio, 0.001) // Should fall back to default
// Test invalid ratio (non-numeric) - should fall back to default
conf3 := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mygroup",
"REDIS_STREAMS_PEL_RATIO": "invalid",
})
require.NotNil(t, conf3.PubSubStreamsConfig)
assert.InEpsilon(t, 0.7, conf3.PubSubStreamsConfig.PELRatio, 0.001) // Should fall back to default
}
func TestGetRedisConfig_PubSubMode_InvalidFallsBackToStreams(t *testing.T) {
conf := testGetRedisConfig(t, map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": "localhost",
"REDIS_PUBSUB_MODE": "invalid-mode",
})
assert.Equal(t, "streams", conf.PubSubMode)
require.NotNil(t, conf.PubSubStreamsConfig)
}
================================================
FILE: pkg/gofr/datasource/redis/health.go
================================================
// Package redis provides a client for interacting with Redis key-value stores.This package allows creating and
// managing Redis clients, executing Redis commands, and handling connections to Redis databases.
package redis
import (
"context"
"fmt"
"strconv"
"time"
"gofr.dev/pkg/gofr/datasource"
)
const (
healthCheckTimeout = 1 * time.Second
)
// HealthCheck returns the health status of the Redis connection.
func (r *Redis) HealthCheck() datasource.Health {
h := datasource.Health{
Details: make(map[string]any),
}
h.Details["host"] = r.config.HostName + ":" + strconv.Itoa(r.config.Port)
ctx, cancel := context.WithTimeout(context.Background(), healthCheckTimeout)
defer cancel()
if r.Client == nil {
h.Status = datasource.StatusDown
h.Details["error"] = "redis not connected"
return h
}
info, err := r.InfoMap(ctx, "Stats").Result()
if err != nil {
h.Status = datasource.StatusDown
h.Details["error"] = err.Error()
return h
}
h.Status = datasource.StatusUp
h.Details["stats"] = info["Stats"]
return h
}
// Health returns the health status of the Redis PubSub connection.
func (ps *PubSub) Health() datasource.Health {
res := datasource.Health{
Status: datasource.StatusDown,
Details: map[string]any{
"backend": "REDIS",
},
}
addr := fmt.Sprintf("%s:%d", ps.config.HostName, ps.config.Port)
res.Details["host"] = addr
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
res.Details["mode"] = mode
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
defer cancel()
if err := ps.client.Ping(ctx).Err(); err != nil {
ps.logger.Errorf("PubSub health check failed: %v", err)
return res
}
res.Status = datasource.StatusUp
return res
}
================================================
FILE: pkg/gofr/datasource/redis/health_test.go
================================================
package redis
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/datasource"
)
func testHealthCheck(t *testing.T, client *testRedisClient) {
t.Helper()
h := client.PubSub.Health()
assert.Equal(t, "UP", h.Status)
assert.Equal(t, "streams", h.Details["mode"]) // Default mode is now streams
}
func TestPubSub_HealthDown(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
mock.ExpectPing().SetErr(errMockPing)
h := client.PubSub.Health()
assert.Equal(t, datasource.StatusDown, h.Status)
assert.Equal(t, "REDIS", h.Details["backend"])
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HealthUp(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
mock.ExpectPing().SetVal("PONG")
h := client.PubSub.Health()
assert.Equal(t, datasource.StatusUp, h.Status)
assert.Equal(t, "REDIS", h.Details["backend"])
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HealthDetails(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_HOST": "localhost",
"REDIS_PORT": "6380",
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
mock.ExpectPing().SetVal("PONG")
h := client.PubSub.Health()
assert.Equal(t, datasource.StatusUp, h.Status)
assert.Equal(t, "REDIS", h.Details["backend"])
assert.Equal(t, "localhost:6380", h.Details["host"])
assert.Equal(t, "pubsub", h.Details["mode"])
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HealthDefaultMode(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_HOST": "localhost",
"REDIS_PORT": "6379",
})
defer client.Close()
mock.ExpectPing().SetVal("PONG")
h := client.PubSub.Health()
require.Equal(t, datasource.StatusUp, h.Status)
assert.Equal(t, "streams", h.Details["mode"], "should default to streams when not specified")
assert.NoError(t, mock.ExpectationsWereMet())
}
================================================
FILE: pkg/gofr/datasource/redis/hook.go
================================================
package redis
import (
"context"
"fmt"
"io"
"regexp"
"strings"
"time"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/datasource"
)
// redisHook is a custom Redis hook for logging queries and their durations.
type redisHook struct {
config *Config
logger datasource.Logger
metrics Metrics
}
// QueryLog represents a logged Redis query.
type QueryLog struct {
Query string `json:"query"`
Duration int64 `json:"duration"`
Args any `json:"args,omitempty"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
if ql.Query == "pipeline" {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n",
clean(ql.Query), "REDIS", ql.Duration,
ql.String()[1:len(ql.String())-1])
} else {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %v\n",
clean(ql.Query), "REDIS", ql.Duration, ql.String())
}
}
func clean(query string) string {
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
query = strings.TrimSpace(query)
return query
}
func (ql *QueryLog) String() string {
if ql.Args == nil {
return ""
}
switch args := ql.Args.(type) {
case []any:
strArgs := make([]string, len(args))
for i, arg := range args {
strArgs[i] = fmt.Sprint(arg)
}
return strings.Join(strArgs, " ")
default:
return fmt.Sprint(ql.Args)
}
}
// logQuery logs the Redis query information.
func (r *redisHook) sendOperationStats(start time.Time, query string, args ...any) {
duration := time.Since(start).Microseconds()
r.logger.Debug(&QueryLog{
Query: query,
Duration: duration,
Args: args,
})
r.metrics.RecordHistogram(context.Background(), "app_redis_stats",
float64(duration), "hostname", r.config.HostName, "type", query)
}
// DialHook implements the redis.DialHook interface.
func (*redisHook) DialHook(next redis.DialHook) redis.DialHook {
return next
}
// ProcessHook implements the redis.ProcessHook interface.
func (r *redisHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
start := time.Now()
err := next(ctx, cmd)
r.sendOperationStats(start, cmd.Name(), cmd.Args()...)
return err
}
}
// ProcessPipelineHook implements the redis.ProcessPipelineHook interface.
func (r *redisHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
start := time.Now()
err := next(ctx, cmds)
r.sendOperationStats(start, "pipeline", cmds[:len(cmds)-1])
return err
}
}
================================================
FILE: pkg/gofr/datasource/redis/hook_test.go
================================================
package redis
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestQueryLog_PrettyPrint(t *testing.T) {
testCases := []struct {
desc string
ql *QueryLog
expOut []string
}{
{
desc: "pipeline",
ql: &QueryLog{
Query: "pipeline",
Duration: 112,
Args: []any{"[", "set a", "get a", "ex 300: OK", "]"},
},
expOut: []string{"pipeline", "112", "REDIS", "set a", "get a"},
},
{
desc: "single command",
ql: &QueryLog{
Query: "get",
Duration: 22,
Args: []any{"get", "key1"},
},
expOut: []string{"get", "REDIS", "22", "get key1"},
},
}
for _, tc := range testCases {
b := new(bytes.Buffer)
tc.ql.PrettyPrint(b)
out := b.String()
for _, v := range tc.expOut {
assert.Contains(t, out, v)
}
}
}
================================================
FILE: pkg/gofr/datasource/redis/messages.go
================================================
package redis
import (
"context"
"math"
"time"
"github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/datasource"
)
// pubSubMessage implements the Committer interface for Redis PubSub messages.
// Redis PubSub is fire-and-forget, so there's nothing to commit.
type pubSubMessage struct {
msg *redis.Message
}
func newPubSubMessage(msg *redis.Message) *pubSubMessage {
return &pubSubMessage{
msg: msg,
}
}
func (*pubSubMessage) Commit() {
// Redis PubSub is fire-and-forget, so there's nothing to commit
}
// streamMessage implements the Committer interface for Redis Stream messages.
// It handles message acknowledgment for Redis Streams.
type streamMessage struct {
client *redis.Client
stream string
group string
id string
logger datasource.Logger
}
func newStreamMessage(client *redis.Client, stream, group, id string, logger datasource.Logger) *streamMessage {
return &streamMessage{
client: client,
stream: stream,
group: group,
id: id,
logger: logger,
}
}
func (m *streamMessage) Commit() {
const maxRetries = 3
const baseDelay = 100 * time.Millisecond
const exponentialBase = 2
var err error
for attempt := 0; attempt < maxRetries; attempt++ {
ctx, cancel := context.WithTimeout(context.Background(), defaultRetryTimeout)
err = m.client.XAck(ctx, m.stream, m.group, m.id).Err()
cancel()
if err == nil {
return
}
// Exponential backoff: baseDelay * 2^attempt
if attempt < maxRetries-1 {
delay := time.Duration(float64(baseDelay) * math.Pow(exponentialBase, float64(attempt)))
time.Sleep(delay)
}
}
// All retries failed
m.logger.Errorf("failed to acknowledge message %s in stream %s after %d attempts: %v", m.id, m.stream, maxRetries, err)
}
================================================
FILE: pkg/gofr/datasource/redis/messages_test.go
================================================
package redis
import (
"context"
"testing"
"time"
"github.com/go-redis/redismock/v9"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
)
func TestPubSubMessage_Commit(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "pubsub-commit-topic"
msgChan := make(chan *pubsub.Message)
errChan := make(chan error)
go func() {
msg, err := client.PubSub.Subscribe(ctx, topic)
if err != nil {
errChan <- err
return
}
msgChan <- msg
}()
time.Sleep(100 * time.Millisecond)
err := client.PubSub.Publish(ctx, topic, []byte("test"))
require.NoError(t, err)
select {
case err := <-errChan:
require.NoError(t, err)
case msg := <-msgChan:
require.NotNil(t, msg)
require.NotNil(t, msg.Committer)
assert.NotPanics(t, func() {
msg.Committer.Commit()
})
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for message")
}
}
func TestNewStreamMessage(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockLogger := logging.NewMockLogger(logging.DEBUG)
db, _ := redismock.NewClientMock()
t.Cleanup(func() { _ = db.Close() })
tests := []struct {
name string
client *redis.Client
stream string
group string
id string
logger logging.Logger
wantNil bool
wantErr bool
}{
{
name: "valid stream message",
client: db,
stream: "test-stream",
group: "test-group",
id: "123-0",
logger: mockLogger,
wantNil: false,
wantErr: false,
},
{
name: "empty stream name",
client: db,
stream: "",
group: "test-group",
id: "123-0",
logger: mockLogger,
wantNil: false,
wantErr: false,
},
{
name: "empty group name",
client: db,
stream: "test-stream",
group: "",
id: "123-0",
logger: mockLogger,
wantNil: false,
wantErr: false,
},
{
name: "empty message id",
client: db,
stream: "test-stream",
group: "test-group",
id: "",
logger: mockLogger,
wantNil: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := newStreamMessage(tt.client, tt.stream, tt.group, tt.id, tt.logger)
if tt.wantNil {
assert.Nil(t, result)
} else {
require.NotNil(t, result)
assert.Equal(t, tt.client, result.client)
assert.Equal(t, tt.stream, result.stream)
assert.Equal(t, tt.group, result.group)
assert.Equal(t, tt.id, result.id)
assert.Equal(t, tt.logger, result.logger)
}
})
}
}
func TestStreamMessage_Commit_Success(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
db, mock := redismock.NewClientMock()
t.Cleanup(func() { _ = db.Close() })
stream := "test-stream"
group := "test-group"
id := "123-0"
mock.ExpectXAck(stream, group, id).SetVal(1)
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": group,
})
defer s.Close()
defer client.Close()
ctx := context.Background()
err := client.PubSub.CreateTopic(ctx, stream)
require.NoError(t, err)
go func() {
_ = client.PubSub.Publish(ctx, stream, []byte("test"))
}()
msg, err := client.PubSub.Subscribe(ctx, stream)
require.NoError(t, err)
require.NotNil(t, msg)
require.NotNil(t, msg.Committer)
assert.NotPanics(t, func() {
msg.Committer.Commit()
})
}
func TestStreamMessage_Commit_Error(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
stream := "test-stream-error"
err := client.PubSub.CreateTopic(ctx, stream)
require.NoError(t, err)
go func() {
_ = client.PubSub.Publish(ctx, stream, []byte("test"))
}()
msg, err := client.PubSub.Subscribe(ctx, stream)
require.NoError(t, err)
require.NotNil(t, msg)
require.NotNil(t, msg.Committer)
s.Close()
assert.NotPanics(t, func() {
msg.Committer.Commit()
})
}
func TestStreamMessage_Commit_Timeout(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
stream := "test-stream-timeout"
err := client.PubSub.CreateTopic(ctx, stream)
require.NoError(t, err)
go func() {
_ = client.PubSub.Publish(ctx, stream, []byte("test"))
}()
msg, err := client.PubSub.Subscribe(ctx, stream)
require.NoError(t, err)
require.NotNil(t, msg)
require.NotNil(t, msg.Committer)
assert.NotPanics(t, func() {
msg.Committer.Commit()
})
}
================================================
FILE: pkg/gofr/datasource/redis/metrics.go
================================================
package redis
import "context"
type Metrics interface {
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/redis/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
// Package redis is a generated GoMock package.
package redis
import (
context "context"
reflect "reflect"
"go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
================================================
FILE: pkg/gofr/datasource/redis/pubsub.go
================================================
package redis
import (
"context"
"errors"
"fmt"
"os"
"strings"
"sync"
"time"
"github.com/redis/go-redis/v9"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
// Publish publishes a message to a Redis channel or stream.
func (ps *PubSub) Publish(ctx context.Context, topic string, message []byte) error {
ctx, span := ps.tracer.Start(ctx, "redis-publish")
defer span.End()
ps.metrics.IncrementCounter(ctx, "app_pubsub_publish_total_count", "topic", topic)
if topic == "" {
return errEmptyTopicName
}
if !ps.isConnected() {
return errClientNotConnected
}
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
if mode == modeStreams {
return ps.publishToStream(ctx, topic, message, span)
}
return ps.publishToChannel(ctx, topic, message, span)
}
// publishToChannel publishes a message to a Redis PubSub channel.
func (ps *PubSub) publishToChannel(ctx context.Context, topic string, message []byte, span trace.Span) error {
start := time.Now()
err := ps.client.Publish(ctx, topic, message).Err()
end := time.Since(start)
if err != nil {
ps.logger.Errorf("failed to publish message to Redis channel '%s': %v", topic, err)
return err
}
addr := fmt.Sprintf("%s:%d", ps.config.HostName, ps.config.Port)
ps.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: addr,
PubSubBackend: "REDIS",
Time: end.Microseconds(),
})
ps.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
// publishToStream publishes a message to a Redis stream.
func (ps *PubSub) publishToStream(ctx context.Context, topic string, message []byte, span trace.Span) error {
args := &redis.XAddArgs{
Stream: topic,
Values: map[string]any{"payload": message},
}
if ps.config.PubSubStreamsConfig != nil && ps.config.PubSubStreamsConfig.MaxLen > 0 {
args.MaxLen = ps.config.PubSubStreamsConfig.MaxLen
args.Approx = true
}
start := time.Now()
_, err := ps.client.XAdd(ctx, args).Result()
end := time.Since(start)
if err != nil {
ps.logger.Errorf("failed to publish message to Redis stream '%s': %v", topic, err)
return err
}
addr := fmt.Sprintf("%s:%d", ps.config.HostName, ps.config.Port)
ps.logger.Debug(&pubsub.Log{
Mode: "PUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(message),
Topic: topic,
Host: addr,
PubSubBackend: "REDIS",
Time: end.Microseconds(),
})
ps.metrics.IncrementCounter(ctx, "app_pubsub_publish_success_count", "topic", topic)
return nil
}
// Subscribe subscribes to a Redis channel or stream and returns a single message.
func (ps *PubSub) Subscribe(ctx context.Context, topic string) (*pubsub.Message, error) {
if topic == "" {
return nil, errEmptyTopicName
}
// Check connection with shorter retry interval to avoid long blocking
for !ps.isConnected() {
select {
case <-ctx.Done():
return nil, nil
case <-time.After(subscribeRetryInterval):
ps.logger.Debugf("Redis not connected, retrying subscribe for topic '%s'", topic)
}
}
spanCtx, span := ps.tracer.Start(ctx, "redis-subscribe")
defer span.End()
// Determine mode and consumer group for metrics
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
var consumerGroup string
if mode == modeStreams && ps.config.PubSubStreamsConfig != nil {
consumerGroup = ps.config.PubSubStreamsConfig.ConsumerGroup
}
// Increment subscribe total count with consumer_group label if using streams
if consumerGroup != "" {
ps.metrics.IncrementCounter(spanCtx, "app_pubsub_subscribe_total_count", "topic", topic, "consumer_group", consumerGroup)
} else {
ps.metrics.IncrementCounter(spanCtx, "app_pubsub_subscribe_total_count", "topic", topic)
}
start := time.Now()
msgChan := ps.ensureSubscription(ctx, topic)
msg := ps.waitForMessage(ctx, spanCtx, span, topic, msgChan, start, consumerGroup)
return msg, nil
}
// ensureSubscription ensures a subscription is started for the topic.
func (ps *PubSub) ensureSubscription(_ context.Context, topic string) chan *pubsub.Message {
ps.mu.Lock()
defer ps.mu.Unlock()
// Double-check pattern: verify subscription state after acquiring lock
_, exists := ps.subStarted[topic]
if exists {
return ps.receiveChan[topic]
}
// Re-check connection after acquiring lock to avoid race condition
if !ps.isConnected() {
ps.logger.Debugf("Redis not connected when starting subscription for topic '%s'", topic)
// Still create channel and start subscription - it will retry in goroutine
}
// Initialize channel before starting subscription
bufferSize := ps.config.PubSubBufferSize
if bufferSize == 0 {
bufferSize = defaultPubSubBufferSize // fallback default
}
ps.receiveChan[topic] = make(chan *pubsub.Message, bufferSize)
ps.chanClosed[topic] = false
ps.closeOnce[topic] = &sync.Once{}
// Create cancel context for this subscription
_, cancel := context.WithCancel(context.Background())
ps.subCancel[topic] = cancel
// Create WaitGroup for this subscription
wg := &sync.WaitGroup{}
wg.Add(1)
ps.subWg[topic] = wg
// Start subscription in goroutine
go ps.runSubscriptionLoop(topic, wg, cancel)
ps.subStarted[topic] = struct{}{}
return ps.receiveChan[topic]
}
// runSubscriptionLoop runs the subscription loop in a goroutine.
func (ps *PubSub) runSubscriptionLoop(topic string, wg *sync.WaitGroup, cancel context.CancelFunc) {
defer wg.Done()
defer cancel()
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
permanentFailure := false
for {
if !ps.shouldContinueSubscription(topic) {
return
}
if permanentFailure {
ps.logger.Errorf("Subscription for topic '%s' stopped due to permanent failure", topic)
return
}
currentCtx := context.Background()
err := ps.subscribeWithMode(currentCtx, topic, mode)
if err != nil && ps.isPermanentError(err) {
permanentFailure = true
ps.logger.Errorf("Permanent failure detected for topic '%s': %v", topic, err)
continue
}
// If subscription stopped (not due to permanent failure), restart after delay
ps.logger.Debugf("Subscription stopped for topic '%s', restarting...", topic)
time.Sleep(defaultRetryTimeout)
}
}
// shouldContinueSubscription checks if subscription should continue.
func (ps *PubSub) shouldContinueSubscription(topic string) bool {
ps.mu.RLock()
defer ps.mu.RUnlock()
_, stillSubscribed := ps.subStarted[topic]
_, hasCancel := ps.subCancel[topic]
return stillSubscribed && hasCancel
}
// subscribeWithMode subscribes using the appropriate mode.
func (ps *PubSub) subscribeWithMode(ctx context.Context, topic, mode string) error {
if mode == modeStreams {
return ps.subscribeToStreamWithError(ctx, topic)
}
return ps.subscribeToChannelWithError(ctx, topic)
}
// waitForMessage waits for a message from the channel.
func (ps *PubSub) waitForMessage(ctx context.Context, spanCtx context.Context, span trace.Span,
topic string, msgChan chan *pubsub.Message, start time.Time, consumerGroup string) *pubsub.Message {
select {
case msg := <-msgChan:
// Increment subscribe success count with consumer_group label if using streams
if consumerGroup != "" {
ps.metrics.IncrementCounter(spanCtx, "app_pubsub_subscribe_success_count", "topic", topic, "consumer_group", consumerGroup)
} else {
ps.metrics.IncrementCounter(spanCtx, "app_pubsub_subscribe_success_count", "topic", topic)
}
if msg != nil {
end := time.Since(start)
addr := fmt.Sprintf("%s:%d", ps.config.HostName, ps.config.Port)
ps.logger.Debug(&pubsub.Log{
Mode: "SUB",
CorrelationID: span.SpanContext().TraceID().String(),
MessageValue: string(msg.Value),
Topic: topic,
Host: addr,
PubSubBackend: "REDIS",
Time: end.Microseconds(),
})
}
return msg
case <-ctx.Done():
return nil
}
}
// isPermanentError checks if an error indicates a permanent failure that should not be retried.
func (*PubSub) isPermanentError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// Check for permanent errors: invalid topic name, permission denied, invalid consumer group
permanentErrors := []string{
"invalid topic",
"permission denied",
"NOAUTH",
"invalid consumer group",
"WRONGTYPE", // Wrong key type (e.g., trying to use stream as channel)
}
for _, permErr := range permanentErrors {
if strings.Contains(strings.ToLower(errStr), strings.ToLower(permErr)) {
return true
}
}
return false
}
// subscribeToChannelWithError subscribes to a Redis channel and returns error if permanent failure.
func (ps *PubSub) subscribeToChannelWithError(ctx context.Context, topic string) error {
return ps.subscribeToChannel(ctx, topic)
}
// subscribeToStreamWithError subscribes to a Redis stream and returns error if permanent failure.
func (ps *PubSub) subscribeToStreamWithError(ctx context.Context, topic string) error {
return ps.subscribeToStream(ctx, topic)
}
// subscribeToChannel subscribes to a Redis channel and forwards messages to the receive channel.
func (ps *PubSub) subscribeToChannel(ctx context.Context, topic string) error {
redisPubSub := ps.client.Subscribe(ctx, topic)
if redisPubSub == nil {
ps.logger.Errorf("failed to create PubSub connection for topic '%s'", topic)
return fmt.Errorf("%w: %s", errPubSubConnectionFailedForTopic, topic)
}
ps.mu.Lock()
ps.subPubSub[topic] = redisPubSub
ps.mu.Unlock()
defer func() {
ps.mu.Lock()
delete(ps.subPubSub, topic)
ps.mu.Unlock()
if redisPubSub != nil {
redisPubSub.Close()
}
}()
ch := redisPubSub.Channel()
if ch == nil {
ps.logger.Errorf("failed to get channel from PubSub for topic '%s'", topic)
return fmt.Errorf("%w: %s", errPubSubChannelFailedForTopic, topic)
}
ps.processMessages(ctx, topic, ch)
return nil
}
// subscribeToStream subscribes to a Redis stream via a consumer group.
func (ps *PubSub) subscribeToStream(ctx context.Context, topic string) error {
if ps.config.PubSubStreamsConfig == nil || ps.config.PubSubStreamsConfig.ConsumerGroup == "" {
ps.logger.Errorf("consumer group not configured for stream '%s'", topic)
return fmt.Errorf("%w: %s", errConsumerGroupNotConfigured, topic)
}
group := ps.config.PubSubStreamsConfig.ConsumerGroup
if !ps.ensureConsumerGroup(ctx, topic, group) {
ps.logger.Errorf("failed to ensure consumer group '%s' for stream '%s'", group, topic)
return fmt.Errorf("%w: group=%s, stream=%s", errFailedToEnsureConsumerGroup, group, topic)
}
consumer := ps.getConsumerName()
ps.storeStreamConsumer(topic, group, consumer)
block := ps.config.PubSubStreamsConfig.Block
if block == 0 {
block = 1 * time.Second // Reduced default for better responsiveness
}
// Consume messages
for {
select {
case <-ctx.Done():
return nil
default:
ps.consumeStreamMessages(ctx, topic, group, consumer, block)
}
}
}
// storeStreamConsumer stores consumer info in the streamConsumers map.
func (ps *PubSub) storeStreamConsumer(topic, group, consumer string) {
ps.mu.Lock()
ps.streamConsumers[topic] = &streamConsumer{
stream: topic,
group: group,
consumer: consumer,
cancel: nil, // handled by subCancel
}
ps.mu.Unlock()
}
// ensureConsumerGroup checks if a consumer group exists and creates it if needed.
func (ps *PubSub) ensureConsumerGroup(ctx context.Context, topic, group string) bool {
groupExists := ps.checkGroupExists(ctx, topic, group)
if groupExists {
return true
}
return ps.createConsumerGroup(ctx, topic, group)
}
// checkGroupExists checks if a consumer group exists for the given stream.
func (ps *PubSub) checkGroupExists(ctx context.Context, topic, group string) bool {
groups, err := ps.client.XInfoGroups(ctx, topic).Result()
if err != nil {
// If XInfoGroups failed (e.g., stream doesn't exist), we'll create it with MKSTREAM
return false
}
// Stream exists, check if group is in the list
for _, g := range groups {
if g.Name == group {
return true
}
}
return false
}
// createConsumerGroup creates a consumer group for the given stream.
func (ps *PubSub) createConsumerGroup(ctx context.Context, topic, group string) bool {
err := ps.client.XGroupCreateMkStream(ctx, topic, group, "$").Err()
if err == nil {
return true
}
// BUSYGROUP means the group already exists (race condition), which is fine
if strings.Contains(err.Error(), "BUSYGROUP") {
return true
}
// Log error and return false to indicate failure
ps.logger.Errorf("failed to create consumer group for stream '%s': %v", topic, err)
return false
}
func (ps *PubSub) consumeStreamMessages(ctx context.Context, topic, group, consumer string, block time.Duration) {
available := ps.getAvailableCapacity(topic)
if available == 0 {
return
}
// Check if we should read from PEL
// pendingRead is false initially and after message drops, true after PEL is read
// This prevents re-reading the same pending messages before they're acknowledged
ps.mu.RLock()
alreadyReadPending := ps.pendingRead[topic]
ps.mu.RUnlock()
// Get PEL ratio from config (default 0.7 = 70% PEL, 30% new)
ratio := 0.7 // Default ratio
if ps.config.PubSubStreamsConfig != nil {
ratio = ps.config.PubSubStreamsConfig.PELRatio
}
// Calculate PEL count based on ratio
pelCount := calculatePELCount(available, ratio)
// Read from PEL if allowed and count > 0
if !alreadyReadPending && pelCount > 0 {
ps.readPendingMessages(ctx, topic, group, consumer, pelCount)
}
// Re-check capacity and fill remaining with new messages
// This ensures remaining capacity is always used, regardless of ratio
available = ps.getAvailableCapacity(topic)
if available > 0 {
// Fill ALL remaining capacity with new messages (not just newCount)
ps.readNewMessages(ctx, topic, group, consumer, int64(available), block)
}
}
// getAvailableCapacity returns the available channel capacity for the given topic.
func (ps *PubSub) getAvailableCapacity(topic string) int {
ps.mu.RLock()
defer ps.mu.RUnlock()
msgChan, exists := ps.receiveChan[topic]
if exists && !ps.chanClosed[topic] {
return cap(msgChan) - len(msgChan)
}
return 0
}
// calculatePELCount calculates how many messages to read from PEL
// based on the configured ratio and available capacity.
func calculatePELCount(available int, ratio float64) int64 {
if available <= 0 {
return 0
}
return int64(float64(available) * ratio)
}
// readPendingMessages reads and processes pending messages. Returns true if messages were processed.
// The count parameter limits how many messages to read from PEL.
func (ps *PubSub) readPendingMessages(ctx context.Context, topic, group, consumer string, count int64) bool {
if count <= 0 {
return false
}
streams, err := ps.client.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: group,
Consumer: consumer,
Streams: []string{topic, "0"},
Count: count,
Block: 0,
NoAck: false,
}).Result()
// Mark that we attempted to read PEL (for tracking purposes)
ps.markPendingRead(topic)
if err != nil && errors.Is(err, redis.Nil) {
return false // No pending messages
}
if err != nil {
ps.logger.Debugf("error reading pending messages for stream '%s': %v, will try new messages", topic, err)
return false
}
if !ps.hasMessages(streams) {
return false // Empty result
}
// Process pending messages
ps.processStreamMessages(ctx, topic, streams, group)
return true
}
// markPendingRead marks pending messages as read.
func (ps *PubSub) markPendingRead(topic string) {
ps.mu.Lock()
ps.pendingRead[topic] = true
ps.mu.Unlock()
}
// hasMessages checks if streams contain any messages.
func (*PubSub) hasMessages(streams []redis.XStream) bool {
for _, stream := range streams {
if len(stream.Messages) > 0 {
return true
}
}
return false
}
// processStreamMessages processes messages from streams.
func (ps *PubSub) processStreamMessages(ctx context.Context, topic string, streams []redis.XStream, group string) {
for _, stream := range streams {
for _, msg := range stream.Messages {
ps.handleStreamMessage(ctx, topic, &msg, group)
}
}
}
// readNewMessages reads and processes new messages from the stream.
// The count parameter limits how many messages to read.
func (ps *PubSub) readNewMessages(ctx context.Context, topic, group, consumer string, count int64, block time.Duration) {
if count <= 0 {
return
}
streams, err := ps.client.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: group,
Consumer: consumer,
Streams: []string{topic, ">"},
Count: count,
Block: block,
NoAck: false,
}).Result()
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, redis.Nil) {
return
}
ps.logger.Errorf("failed to read from stream '%s': %v", topic, err)
time.Sleep(defaultRetryTimeout)
return
}
ps.processStreamMessages(ctx, topic, streams, group)
}
// getConsumerName returns the configured consumer name or generates one.
func (ps *PubSub) getConsumerName() string {
if ps.config.PubSubStreamsConfig != nil && ps.config.PubSubStreamsConfig.ConsumerName != "" {
return ps.config.PubSubStreamsConfig.ConsumerName
}
hostname, _ := os.Hostname()
pid := os.Getpid()
return fmt.Sprintf("consumer-%s-%d-%d", hostname, pid, time.Now().UnixNano())
}
// processMessages processes messages from the Redis channel.
func (ps *PubSub) processMessages(ctx context.Context, topic string, ch <-chan *redis.Message) {
for {
select {
case <-ctx.Done():
return
case msg, ok := <-ch:
if !ok {
ps.logger.Debugf("Redis subscription channel closed for topic '%s'", topic)
return
}
if msg == nil {
continue
}
ps.handleMessage(ctx, topic, msg)
}
}
}
// handleMessage handles a single message from Redis.
func (ps *PubSub) handleMessage(ctx context.Context, topic string, msg *redis.Message) {
m := pubsub.NewMessage(ctx)
m.Topic = topic
m.Value = []byte(msg.Payload)
m.Committer = newPubSubMessage(msg)
ps.dispatchMessage(ctx, topic, m)
}
// handleStreamMessage handles a single message from Redis Stream.
func (ps *PubSub) handleStreamMessage(ctx context.Context, topic string, msg *redis.XMessage, group string) {
m := pubsub.NewMessage(ctx)
m.Topic = topic
m.Committer = newStreamMessage(ps.client, topic, group, msg.ID, ps.logger)
// Extract payload
if val, ok := msg.Values["payload"]; ok {
switch v := val.(type) {
case string:
m.Value = []byte(v)
case []byte:
m.Value = v
}
} else {
ps.logger.Debugf("received stream message without 'payload' key on topic '%s'", topic)
}
ps.dispatchMessage(ctx, topic, m)
}
// dispatchMessage sends the message to the receive channel.
func (ps *PubSub) dispatchMessage(ctx context.Context, topic string, m *pubsub.Message) {
ps.mu.RLock()
msgChan, exists := ps.receiveChan[topic]
closed := ps.chanClosed[topic]
ps.mu.RUnlock()
if !exists || closed {
return
}
// Use recover to handle closed channel gracefully
func() {
defer func() {
if r := recover(); r != nil {
ps.logger.Debugf("channel closed for topic '%s' during dispatch", topic)
}
}()
select {
case msgChan <- m:
case <-ctx.Done():
return
default:
// Channel full - drop message
ps.logger.Errorf("message channel full for topic '%s', dropping message", topic)
// Reset pendingRead for Streams mode so PEL is checked again
// This ensures dropped messages (which stay in PEL) are retried
if m.Committer != nil {
// Check if this is a stream message by type assertion
// Only reset for Streams mode, not PubSub mode
if _, isStreamMessage := m.Committer.(*streamMessage); isStreamMessage {
// Lock is necessary: map writes are not thread-safe
// Setting to false is idempotent, so safe to do without check
ps.mu.Lock()
ps.pendingRead[topic] = false
ps.mu.Unlock()
}
}
}
}()
}
// CreateTopic is a no-op for Redis PubSub (channels are created on first publish/subscribe).
// For Redis Streams, it creates the stream and consumer group.
func (ps *PubSub) CreateTopic(ctx context.Context, name string) error {
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
if mode == modeStreams {
return ps.createStreamTopic(ctx, name)
}
// Redis channels are created automatically on first publish/subscribe
return nil
}
// createStreamTopic creates a stream topic with consumer group.
func (ps *PubSub) createStreamTopic(ctx context.Context, name string) error {
if ps.config.PubSubStreamsConfig == nil || ps.config.PubSubStreamsConfig.ConsumerGroup == "" {
return errConsumerGroupNotProvided
}
group := ps.config.PubSubStreamsConfig.ConsumerGroup
groupExists := ps.checkGroupExists(ctx, name, group)
if groupExists {
return nil
}
err := ps.client.XGroupCreateMkStream(ctx, name, group, "$").Err()
if err != nil && !strings.Contains(err.Error(), "BUSYGROUP") {
return err
}
return nil
}
// DeleteTopic unsubscribes all active subscriptions for the given topic/channel.
func (ps *PubSub) DeleteTopic(ctx context.Context, topic string) error {
if topic == "" {
return nil
}
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
if mode == modeStreams {
if !ps.isConnected() {
return errClientNotConnected
}
ps.cleanupStreamConsumers(topic)
return ps.client.Del(ctx, topic).Err()
}
// Check if there are any active subscriptions for this topic
ps.mu.RLock()
_, hasActiveSub := ps.subStarted[topic]
ps.mu.RUnlock()
if !hasActiveSub {
return nil
}
// Unsubscribe from the topic (this will clean up all resources)
return ps.unsubscribe(topic)
}
// unsubscribe unsubscribes from a Redis channel or stream.
func (ps *PubSub) unsubscribe(topic string) error {
if topic == "" {
return errEmptyTopicName
}
ps.mu.Lock()
_, exists := ps.subStarted[topic]
ps.mu.Unlock()
if !exists {
return nil
}
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
if mode == modeStreams {
ps.cleanupStreamConsumers(topic)
return nil
}
// Unsubscribe from Redis first, then set chanClosed flag to avoid race condition
ps.unsubscribeFromRedis(topic)
ps.cancelSubscription(topic)
ps.waitForGoroutine(topic)
// Set chanClosed after unsubscribe to ensure messages in flight are handled
ps.mu.Lock()
ps.chanClosed[topic] = true
ps.mu.Unlock()
ps.cleanupSubscription(topic)
return nil
}
// unsubscribeFromRedis unsubscribes from the Redis channel.
func (ps *PubSub) unsubscribeFromRedis(topic string) {
ps.mu.RLock()
pubSub, ok := ps.subPubSub[topic]
ps.mu.RUnlock()
if !ok || pubSub == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), unsubscribeOpTimeout)
defer cancel()
if err := pubSub.Unsubscribe(ctx, topic); err != nil {
ps.logger.Errorf("failed to unsubscribe from Redis channel '%s': %v", topic, err)
}
}
// cancelSubscription cancels the subscription context.
func (ps *PubSub) cancelSubscription(topic string) {
ps.mu.Lock()
defer ps.mu.Unlock()
if cancel, ok := ps.subCancel[topic]; ok {
cancel()
delete(ps.subCancel, topic)
}
}
// waitForGoroutine waits for the subscription goroutine to finish.
func (ps *PubSub) waitForGoroutine(topic string) {
ps.mu.RLock()
wg, ok := ps.subWg[topic]
ps.mu.RUnlock()
if !ok {
return
}
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(goroutineWaitTimeout):
ps.logger.Debugf("timeout waiting for subscription goroutine for topic '%s'", topic)
}
ps.mu.Lock()
delete(ps.subWg, topic)
ps.mu.Unlock()
}
// cleanupSubscription cleans up subscription resources.
func (ps *PubSub) cleanupSubscription(topic string) {
ps.mu.Lock()
ch, chExists := ps.receiveChan[topic]
closeOnce, onceExists := ps.closeOnce[topic]
ps.mu.Unlock()
if chExists && onceExists {
ps.chanClosed[topic] = true
closeOnce.Do(func() {
close(ch)
})
}
ps.mu.Lock()
delete(ps.receiveChan, topic)
delete(ps.closeOnce, topic)
delete(ps.subStarted, topic)
delete(ps.chanClosed, topic)
delete(ps.pendingRead, topic)
ps.mu.Unlock()
}
// cleanupStreamConsumers cleans up stream consumer resources.
func (ps *PubSub) cleanupStreamConsumers(topic string) {
ps.mu.Lock()
defer ps.mu.Unlock()
if c, ok := ps.streamConsumers[topic]; ok {
if c.cancel != nil {
c.cancel()
}
delete(ps.streamConsumers, topic)
}
if ch, ok := ps.receiveChan[topic]; ok {
if closeOnce, onceExists := ps.closeOnce[topic]; onceExists {
ps.chanClosed[topic] = true
closeOnce.Do(func() {
close(ch)
})
}
delete(ps.receiveChan, topic)
delete(ps.closeOnce, topic)
}
delete(ps.subStarted, topic)
delete(ps.chanClosed, topic)
delete(ps.pendingRead, topic)
}
// Query retrieves messages from a Redis channel or stream.
func (ps *PubSub) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
if !ps.isConnected() {
return nil, errClientNotConnected
}
if query == "" {
return nil, errEmptyTopicName
}
mode := ps.config.PubSubMode
if mode == "" {
mode = modeStreams
}
if mode == modeStreams {
return ps.queryStream(ctx, query, args...)
}
return ps.queryChannel(ctx, query, args...)
}
// queryChannel retrieves messages from a Redis PubSub channel.
func (ps *PubSub) queryChannel(ctx context.Context, query string, args ...any) ([]byte, error) {
timeout, limit := ps.parseQueryArgs(args...)
queryCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
redisPubSub := ps.client.Subscribe(queryCtx, query)
if redisPubSub == nil {
return nil, errPubSubConnectionFailed
}
defer func() {
if redisPubSub != nil {
// Explicitly unsubscribe before closing to clean up Redis subscription
unsubCtx, unsubCancel := context.WithTimeout(context.Background(), unsubscribeOpTimeout)
_ = redisPubSub.Unsubscribe(unsubCtx, query)
unsubCancel()
redisPubSub.Close()
}
}()
ch := redisPubSub.Channel()
if ch == nil {
return nil, errPubSubChannelFailed
}
return ps.collectMessages(queryCtx, ch, limit), nil
}
// queryStream retrieves messages from a Redis stream.
func (ps *PubSub) queryStream(ctx context.Context, stream string, args ...any) ([]byte, error) {
timeout, limit := ps.parseQueryArgs(args...)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Use XRANGE to get messages from the stream
vals, err := ps.client.XRangeN(ctx, stream, "-", "+", int64(limit)).Result()
if err != nil {
return nil, err
}
var result []byte
for _, msg := range vals {
var payload []byte
if val, ok := msg.Values["payload"]; ok {
switch v := val.(type) {
case string:
payload = []byte(v)
case []byte:
payload = v
}
}
if len(payload) > 0 {
if len(result) > 0 {
result = append(result, '\n')
}
result = append(result, payload...)
}
}
return result, nil
}
// collectMessages collects messages from the channel up to the limit.
func (*PubSub) collectMessages(ctx context.Context, ch <-chan *redis.Message, limit int) []byte {
var result []byte
collected := 0
for collected < limit {
// Check context first before attempting to receive from channel
select {
case <-ctx.Done():
return result
default:
}
// Now try to receive from channel, but also check context
select {
case <-ctx.Done():
return result
case msg, ok := <-ch:
if !ok {
return result
}
if msg != nil {
if len(result) > 0 {
result = append(result, '\n')
}
result = append(result, []byte(msg.Payload)...)
collected++
}
}
}
return result
}
// parseQueryArgs parses query arguments (timeout, limit).
// It uses config defaults if not provided, but allows override via args.
func (ps *PubSub) parseQueryArgs(args ...any) (timeout time.Duration, limit int) {
// Get defaults from config
timeout = ps.config.PubSubQueryTimeout
if timeout == 0 {
timeout = defaultPubSubQueryTimeout // fallback default
}
limit = ps.config.PubSubQueryLimit
if limit == 0 {
limit = defaultPubSubQueryLimit // fallback default
}
// Override with provided args
if len(args) > 0 {
if t, ok := args[0].(time.Duration); ok {
timeout = t
}
}
if len(args) > 1 {
if l, ok := args[1].(int); ok {
limit = l
}
}
return timeout, limit
}
// Helper methods
// isConnected checks if the Redis client is connected.
func (ps *PubSub) isConnected() bool {
ctx, cancel := context.WithTimeout(context.Background(), redisPingTimeout)
defer cancel()
return ps.client.Ping(ctx).Err() == nil
}
// Close closes all active subscriptions and cleans up resources.
func (ps *PubSub) Close() error {
ps.mu.Lock()
defer ps.mu.Unlock()
// Cancel all subscriptions
for topic, cancel := range ps.subCancel {
cancel()
delete(ps.subCancel, topic)
}
// Close all PubSub connections
for topic, pubSub := range ps.subPubSub {
if pubSub != nil {
pubSub.Close()
}
delete(ps.subPubSub, topic)
}
// Wait for all goroutines
ps.waitForAllGoroutines()
// Close all channels
for topic, ch := range ps.receiveChan {
if closeOnce, ok := ps.closeOnce[topic]; ok {
closeOnce.Do(func() {
close(ch)
})
}
delete(ps.receiveChan, topic)
delete(ps.closeOnce, topic)
}
// Clean up stream consumers
for topic, consumer := range ps.streamConsumers {
if consumer.cancel != nil {
consumer.cancel()
}
delete(ps.streamConsumers, topic)
}
// Clear all maps
ps.subStarted = make(map[string]struct{})
ps.chanClosed = make(map[string]bool)
ps.closeOnce = make(map[string]*sync.Once)
if ps.cancel != nil {
ps.cancel() // Stop monitorConnection
}
return nil
}
func (ps *PubSub) waitForAllGoroutines() {
for topic, wg := range ps.subWg {
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
case <-time.After(goroutineWaitTimeout):
ps.logger.Debugf("timeout waiting for subscription goroutine for topic '%s'", topic)
}
delete(ps.subWg, topic)
}
}
// monitorConnection periodically checks the connection status and triggers resubscription if connection is restored.
func (ps *PubSub) monitorConnection(ctx context.Context) {
ticker := time.NewTicker(defaultRetryTimeout)
defer ticker.Stop()
wasConnected := ps.isConnected()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
connected := ps.isConnected()
if !connected && wasConnected {
ps.logger.Errorf("Redis connection lost")
wasConnected = false
} else if connected && !wasConnected {
ps.logger.Infof("Redis connection restored")
wasConnected = true
ps.resubscribeAll()
}
}
}
}
// resubscribeAll triggers resubscription by canceling existing subscription contexts.
// Subscription goroutines will detect cancellation and restart, reconnecting to Redis.
func (ps *PubSub) resubscribeAll() {
ps.mu.Lock()
defer ps.mu.Unlock()
if len(ps.subStarted) == 0 {
return
}
ps.logger.Infof("Triggering resubscription for %d topics after reconnection", len(ps.subStarted))
// Cancel all subscription contexts to trigger restart
// This will cause subscription goroutines to restart and reconnect
// Note: We don't remove from subStarted - the goroutines will restart automatically
for topic, cancel := range ps.subCancel {
if cancel != nil {
// Cancel the old context
cancel()
// Create new context for restart
_, newCancel := context.WithCancel(context.Background())
ps.subCancel[topic] = newCancel
// Reset pendingRead so pending messages are read again
ps.pendingRead[topic] = false
}
}
}
================================================
FILE: pkg/gofr/datasource/redis/pubsub_test.go
================================================
package redis
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redismock/v9"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource/pubsub"
"gofr.dev/pkg/gofr/logging"
)
func TestNewPubSub_UsesRedisPubSubDB(t *testing.T) {
s, err := miniredis.Run()
require.NoError(t, err)
t.Cleanup(s.Close)
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.DEBUG)
ps := NewPubSub(config.NewMockConfig(map[string]string{
"PUBSUB_BACKEND": "REDIS",
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
"REDIS_DB": "0",
"REDIS_PUBSUB_DB": "1",
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "gofr",
}), mockLogger, mockMetrics)
require.NotNil(t, ps)
err = ps.CreateTopic(context.Background(), "db-partition-topic")
require.NoError(t, err)
rc0 := redis.NewClient(&redis.Options{Addr: s.Addr(), DB: 0})
t.Cleanup(func() { _ = rc0.Close() })
t0, err := rc0.Type(context.Background(), "db-partition-topic").Result()
require.NoError(t, err)
require.Equal(t, "none", t0)
rc1 := redis.NewClient(&redis.Options{Addr: s.Addr(), DB: 1})
t.Cleanup(func() { _ = rc1.Close() })
t1, err := rc1.Type(context.Background(), "db-partition-topic").Result()
require.NoError(t, err)
require.Equal(t, "stream", t1)
}
func TestPubSub_Query_Channel(t *testing.T) {
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "query-channel"
type queryResult struct {
msgs []byte
err error
}
resChan := make(chan queryResult)
go func() {
msgs, err := client.PubSub.Query(ctx, topic, 2*time.Second, 2)
resChan <- queryResult{msgs, err}
}()
time.Sleep(200 * time.Millisecond)
msgs := []string{"chan-msg1", "chan-msg2"}
for _, m := range msgs {
err := client.PubSub.Publish(ctx, topic, []byte(m))
require.NoError(t, err)
time.Sleep(50 * time.Millisecond)
}
select {
case res := <-resChan:
require.NoError(t, res.err)
expected := strings.Join(msgs, "\n")
assert.Equal(t, expected, string(res.msgs))
case <-time.After(3 * time.Second):
t.Fatal("Query timed out")
}
}
var (
errMockPing = errors.New("mock ping error")
errMockPublish = errors.New("mock publish error")
errMockXAdd = errors.New("mock xadd error")
errMockGroup = errors.New("mock group error")
errMockGroupCreate = errors.New("mock group create error")
errMockXRange = errors.New("mock xrange error")
errMockDel = errors.New("mock del error")
errBusyGroup = errors.New("BUSYGROUP Consumer Group name already exists")
)
type testRedisClient struct {
*Redis
PubSub *PubSub
}
func setupTest(t *testing.T, conf map[string]string) (*testRedisClient, *miniredis.Miniredis) {
t.Helper()
s, err := miniredis.Run()
require.NoError(t, err)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.DEBUG)
if conf == nil {
conf = make(map[string]string)
}
conf["REDIS_HOST"] = s.Host()
conf["REDIS_PORT"] = s.Port()
conf["PUBSUB_BACKEND"] = "REDIS"
client := NewClient(config.NewMockConfig(conf), mockLogger, mockMetrics)
ps := NewPubSub(config.NewMockConfig(conf), mockLogger, mockMetrics)
require.NotNil(t, ps)
psClient, ok := ps.(*PubSub)
require.True(t, ok)
return &testRedisClient{Redis: client, PubSub: psClient}, s
}
func setupMockTest(t *testing.T, conf map[string]string) (*testRedisClient, redismock.ClientMock) {
t.Helper()
db, mock := redismock.NewClientMock()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.DEBUG)
if conf == nil {
conf = make(map[string]string)
}
// Add required config to trigger PubSub initialization
conf["PUBSUB_BACKEND"] = "REDIS"
conf["REDIS_HOST"] = "localhost"
redisConfig := getRedisConfig(config.NewMockConfig(conf), mockLogger)
r := &Redis{
Client: db,
config: redisConfig,
logger: mockLogger,
stopSignal: make(chan struct{}),
}
ps := newPubSub(db, redisConfig, mockLogger, mockMetrics)
return &testRedisClient{Redis: r, PubSub: ps}, mock
}
func TestPubSub_Operations(t *testing.T) {
tests := getPubSubTestCases()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, s := setupTest(t, tt.config)
defer s.Close()
defer client.Close()
tt.actions(t, client, s)
})
}
}
func getPubSubTestCases() []struct {
name string
config map[string]string
actions func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis)
} {
return append(
getBasicTestCases(),
getQueryTestCases()...,
)
}
func getBasicTestCases() []struct {
name string
config map[string]string
actions func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis)
} {
return []struct {
name string
config map[string]string
actions func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis)
}{
{
name: "Channel Publish Subscribe",
config: map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testChannelPublishSubscribe(t, client)
},
},
{
name: "Stream Publish Subscribe",
config: map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "grp",
"REDIS_STREAMS_BLOCK_TIMEOUT": "100ms",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testStreamPublishSubscribe(t, client)
},
},
{
name: "Delete Topic Channel",
config: map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testDeleteTopicChannel(t, client)
},
},
{
name: "Delete Topic Stream",
config: map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "dgrp",
},
actions: func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis) {
t.Helper()
testDeleteTopicStream(t, client, s)
},
},
{
name: "Health Check",
config: map[string]string{},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testHealthCheck(t, client)
},
},
{
name: "Stream Config Error - Missing Group",
config: map[string]string{
"REDIS_PUBSUB_MODE": "streams",
// Missing REDIS_STREAMS_CONSUMER_GROUP
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testStreamConfigError(t, client)
},
},
{
name: "Stream MaxLen",
config: map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "maxlen-grp",
"REDIS_STREAMS_MAXLEN": "5",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testStreamMaxLen(t, client)
},
},
}
}
func getQueryTestCases() []struct {
name string
config map[string]string
actions func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis)
} {
return []struct {
name string
config map[string]string
actions func(t *testing.T, client *testRedisClient, s *miniredis.Miniredis)
}{
{
name: "Channel Query",
config: map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testChannelQuery(t, client)
},
},
{
name: "Stream Query",
config: map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "qgrp",
},
actions: func(t *testing.T, client *testRedisClient, _ *miniredis.Miniredis) {
t.Helper()
testStreamQuery(t, client)
},
},
}
}
func testChannelPublishSubscribe(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "test-chan"
msg := []byte("hello")
ch := make(chan *pubsub.Message)
errCh := make(chan error)
go func() {
m, err := client.PubSub.Subscribe(ctx, topic)
if err != nil {
errCh <- err
return
}
ch <- m
}()
time.Sleep(100 * time.Millisecond)
err := client.PubSub.Publish(ctx, topic, msg)
require.NoError(t, err)
select {
case err := <-errCh:
require.NoError(t, err)
case m := <-ch:
assert.Equal(t, string(msg), string(m.Value))
case <-time.After(2 * time.Second):
t.Fatal("timeout")
}
}
func testStreamPublishSubscribe(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "test-stream"
msg := []byte("hello stream")
ch := make(chan *pubsub.Message)
errCh := make(chan error)
go func() {
m, err := client.PubSub.Subscribe(ctx, topic)
if err != nil {
errCh <- err
return
}
ch <- m
}()
time.Sleep(500 * time.Millisecond)
err := client.PubSub.Publish(ctx, topic, msg)
require.NoError(t, err)
select {
case err := <-errCh:
require.NoError(t, err)
case m := <-ch:
assert.Equal(t, string(msg), string(m.Value))
m.Committer.Commit()
case <-time.After(2 * time.Second):
t.Fatal("timeout")
}
}
func testChannelQuery(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "query-chan"
ch := make(chan []byte)
errCh := make(chan error)
go func() {
res, err := client.PubSub.Query(ctx, topic, 1*time.Second, 2)
if err != nil {
errCh <- err
return
}
ch <- res
}()
time.Sleep(100 * time.Millisecond)
_ = client.PubSub.Publish(ctx, topic, []byte("m1"))
_ = client.PubSub.Publish(ctx, topic, []byte("m2"))
select {
case err := <-errCh:
require.NoError(t, err)
case res := <-ch:
assert.Contains(t, string(res), "m1")
assert.Contains(t, string(res), "m2")
case <-time.After(2 * time.Second):
t.Fatal("timeout")
}
}
func testStreamQuery(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "query-stream"
_ = client.PubSub.Publish(ctx, topic, []byte("sm1"))
_ = client.PubSub.Publish(ctx, topic, []byte("sm2"))
res, err := client.PubSub.Query(ctx, topic, 1*time.Second, 10)
require.NoError(t, err)
require.NotEmpty(t, res, "Query should return results (miniredis limitation may cause empty results)")
assert.Contains(t, string(res), "sm1")
assert.Contains(t, string(res), "sm2")
}
func TestPubSub_Query_Stream_Success(t *testing.T) {
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "query-group",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "query-stream-success"
msgs := []string{"stream-msg1", "stream-msg2", "stream-msg3"}
for _, m := range msgs {
err := client.PubSub.Publish(ctx, topic, []byte(m))
require.NoError(t, err)
}
// Query messages
results, err := client.PubSub.Query(ctx, topic, 1*time.Second, 10)
require.NoError(t, err)
require.NotEmpty(t, results, "Query should return results for successful case")
expected := strings.Join(msgs, "\n")
assert.Equal(t, expected, string(results))
}
func TestPubSub_Query_Stream_MiniredisCompatibility(t *testing.T) {
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "query-group",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "query-stream-compat"
msgs := []string{"stream-msg1", "stream-msg2"}
for _, m := range msgs {
err := client.PubSub.Publish(ctx, topic, []byte(m))
require.NoError(t, err)
}
// Query messages
results, err := client.PubSub.Query(ctx, topic, 1*time.Second, 10)
require.NoError(t, err)
// Miniredis XRANGE may return empty results - this is acceptable
// The test passes regardless of whether results are empty or not
// This documents the known miniredis limitation
t.Logf("Query returned %d bytes (miniredis may return empty for XRANGE)", len(results))
}
func testDeleteTopicChannel(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "del-chan"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
err := client.PubSub.DeleteTopic(ctx, topic)
require.NoError(t, err)
}
func testDeleteTopicStream(t *testing.T, client *testRedisClient, s *miniredis.Miniredis) {
t.Helper()
ctx := context.Background()
topic := "del-stream"
_ = client.PubSub.CreateTopic(ctx, topic)
err := client.PubSub.DeleteTopic(ctx, topic)
require.NoError(t, err)
// Verify deleted
exists := s.Exists(topic)
assert.False(t, exists)
}
func testStreamConfigError(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "err-stream"
err := client.PubSub.CreateTopic(ctx, topic)
assert.Equal(t, errConsumerGroupNotProvided, err)
ch := client.PubSub.ensureSubscription(ctx, topic)
assert.NotNil(t, ch)
}
func testStreamMaxLen(t *testing.T, client *testRedisClient) {
t.Helper()
ctx := context.Background()
topic := "maxlen-stream"
msg := []byte("payload")
err := client.PubSub.Publish(ctx, topic, msg)
assert.NoError(t, err)
}
func TestPubSub_Errors(t *testing.T) {
// Setup Redis
s, err := miniredis.Run()
require.NoError(t, err)
host := s.Host()
port := s.Port()
s.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.ERROR)
conf := map[string]string{
"REDIS_HOST": host,
"REDIS_PORT": port,
"PUBSUB_BACKEND": "REDIS",
}
client := NewClient(config.NewMockConfig(conf), mockLogger, mockMetrics)
ps := NewPubSub(config.NewMockConfig(conf), mockLogger, mockMetrics)
require.NotNil(t, ps)
psClient, ok := ps.(*PubSub)
require.True(t, ok)
defer client.Close()
defer psClient.Close()
ctx := context.Background()
topic := "err-topic"
err = psClient.Publish(ctx, topic, []byte("msg"))
require.Error(t, err)
assert.Equal(t, errClientNotConnected, err)
ctxCancel, cancel := context.WithCancel(context.Background())
cancel()
msg, err := psClient.Subscribe(ctxCancel, topic)
require.NoError(t, err)
assert.Nil(t, msg)
}
func TestPubSub_MockErrors(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub != nil && client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "mock-err-topic"
mock.ExpectPing().SetVal("PONG")
mock.ExpectPublish(topic, []byte("msg")).SetErr(errMockPublish)
err := client.PubSub.Publish(ctx, topic, []byte("msg"))
require.Error(t, err)
assert.Contains(t, err.Error(), errMockPublish.Error())
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_StreamMockErrors(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mock-grp",
})
defer client.Close()
if client.PubSub != nil && client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "mock-stream-err"
mock.ExpectPing().SetVal("PONG")
mock.ExpectXAdd(&redis.XAddArgs{
Stream: topic,
Values: map[string]any{"payload": []byte("msg")},
}).SetErr(errMockXAdd)
err := client.PubSub.Publish(ctx, topic, []byte("msg"))
require.Error(t, err)
assert.Contains(t, err.Error(), errMockXAdd.Error())
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{})
mock.ExpectXGroupCreateMkStream(topic, "mock-grp", "$").SetErr(errMockGroup)
err = client.PubSub.CreateTopic(ctx, topic)
require.Error(t, err)
assert.Contains(t, err.Error(), errMockGroup.Error())
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_StreamSubscribeErrors(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mock-sub-grp",
})
defer client.Close()
// Stop monitorConnection to avoid extra Ping calls
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
topic := "mock-sub-err"
// Expect Ping for isConnected() check in Subscribe()
mock.ExpectPing().SetVal("PONG")
// Expect XInfoGroups for checkGroupExists() in ensureConsumerGroup()
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{})
// Expect XGroupCreateMkStream which will return an error
mock.ExpectXGroupCreateMkStream(topic, "mock-sub-grp", "$").SetErr(errMockGroupCreate)
msg, err := client.PubSub.Subscribe(ctx, topic)
require.NoError(t, err)
assert.Nil(t, msg)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_MockQueryDeleteErrors(t *testing.T) {
// Stream Mode
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "mock-grp",
})
defer client.Close()
// Stop monitorConnection to avoid extra Ping calls
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "mock-query-err"
mock.ExpectPing().SetVal("PONG")
mock.ExpectXRangeN(topic, "-", "+", int64(10)).SetErr(errMockXRange)
res, err := client.PubSub.Query(ctx, topic, 1*time.Second, 10)
require.Error(t, err)
assert.Nil(t, res)
assert.Contains(t, err.Error(), errMockXRange.Error())
mock.ExpectPing().SetVal("PONG")
mock.ExpectDel(topic).SetErr(errMockDel)
err = client.PubSub.DeleteTopic(ctx, topic)
require.Error(t, err)
assert.Contains(t, err.Error(), errMockDel.Error())
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_Unsubscribe(t *testing.T) {
client, s := setupTest(t, nil)
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "unsub-topic"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
err := client.PubSub.unsubscribe(topic)
require.NoError(t, err)
}
func TestPubSub_MonitorConnection(t *testing.T) {
s, err := miniredis.Run()
require.NoError(t, err)
_ = s.Addr()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.DEBUG)
conf := map[string]string{
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
"PUBSUB_BACKEND": "REDIS",
}
client := NewClient(config.NewMockConfig(conf), mockLogger, mockMetrics)
ps := NewPubSub(config.NewMockConfig(conf), mockLogger, mockMetrics)
require.NotNil(t, ps)
defer client.Close()
defer ps.Close()
psClient, ok := ps.(*PubSub)
require.True(t, ok)
assert.True(t, psClient.isConnected())
topic := "monitor-topic"
go func() {
_, _ = psClient.Subscribe(context.Background(), topic)
}()
time.Sleep(100 * time.Millisecond)
s.Close()
assert.False(t, psClient.isConnected())
s.Close()
}
func TestPubSub_ResubscribeAll(t *testing.T) {
t.Parallel()
client, s := setupTest(t, nil)
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "resub-topic"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
psClient := client.PubSub
psClient.resubscribeAll()
psClient.mu.RLock()
_, exists := psClient.subStarted[topic]
psClient.mu.RUnlock()
assert.True(t, exists)
}
func TestPubSub_ResubscribeAll_NoSubscriptions(t *testing.T) {
t.Parallel()
client, s := setupTest(t, nil)
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.resubscribeAll()
psClient.mu.RLock()
count := len(psClient.subStarted)
psClient.mu.RUnlock()
assert.Equal(t, 0, count)
}
func TestPubSub_CleanupSubscription(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "cleanup-topic"
// Subscribe to create subscription
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
psClient := client.PubSub
psClient.mu.Lock()
psClient.chanClosed[topic] = true
psClient.mu.Unlock()
psClient.cleanupSubscription(topic)
psClient.mu.RLock()
_, exists := psClient.receiveChan[topic]
_, started := psClient.subStarted[topic]
psClient.mu.RUnlock()
assert.False(t, exists)
assert.False(t, started)
}
func TestPubSub_CleanupSubscription_NotClosed(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "cleanup-not-closed-topic"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
psClient := client.PubSub
psClient.cleanupSubscription(topic)
psClient.mu.RLock()
_, exists := psClient.receiveChan[topic]
closed := psClient.chanClosed[topic]
psClient.mu.RUnlock()
assert.False(t, exists)
assert.False(t, closed, "chanClosed is deleted from map after cleanup")
}
func TestPubSub_CleanupStreamConsumers(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "cleanup-grp",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "cleanup-stream-topic"
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(200 * time.Millisecond)
psClient := client.PubSub
psClient.cleanupStreamConsumers(topic)
psClient.mu.RLock()
_, exists := psClient.streamConsumers[topic]
_, started := psClient.subStarted[topic]
psClient.mu.RUnlock()
assert.False(t, exists)
assert.False(t, started)
}
func TestPubSub_CleanupStreamConsumers_WithCancel(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "cleanup-cancel-grp",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "cleanup-stream-cancel-topic"
// Create topic
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
// Subscribe
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(200 * time.Millisecond)
// Manually set up a cancel function
psClient := client.PubSub
psClient.mu.Lock()
ctxCancel, cancel := context.WithCancel(context.Background())
psClient.streamConsumers[topic] = &streamConsumer{
stream: topic,
group: "cleanup-cancel-grp",
consumer: "test-consumer",
cancel: cancel,
}
psClient.mu.Unlock()
// Cleanup should call cancel
psClient.cleanupStreamConsumers(topic)
// Verify context was canceled
assert.Error(t, ctxCancel.Err())
}
func TestPubSub_DispatchMessage_ChannelFull(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
"PUBSUB_BUFFER_SIZE": "1", // Small buffer to test full channel
"REDIS_PUBSUB_BUFFER_SIZE": "1",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "dispatch-full-topic"
msgChan := client.PubSub.ensureSubscription(ctx, topic)
msgChan <- pubsub.NewMessage(ctx)
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte("dropped")
psClient := client.PubSub
psClient.dispatchMessage(ctx, topic, msg)
assert.Len(t, msgChan, 1)
}
func TestPubSub_DispatchMessage_ChannelFull_Streams_ResetsPendingRead(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_PUBSUB_BUFFER_SIZE": "1",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "dispatch-stream-full-topic"
msgChan := client.PubSub.ensureSubscription(ctx, topic)
// Fill channel
msgChan <- pubsub.NewMessage(ctx)
// Create stream message with Committer
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte("dropped-stream-message")
msg.Committer = newStreamMessage(client.PubSub.client, topic, "test-group", "123-0", client.PubSub.logger)
psClient := client.PubSub
// Set pendingRead to true (simulating that PEL was already checked)
psClient.mu.Lock()
psClient.pendingRead[topic] = true
psClient.mu.Unlock()
psClient.dispatchMessage(ctx, topic, msg)
// Verify pendingRead was reset to false
psClient.mu.RLock()
pendingRead := psClient.pendingRead[topic]
psClient.mu.RUnlock()
assert.False(t, pendingRead, "pendingRead should be reset to false when stream message is dropped")
assert.Len(t, msgChan, 1, "channel should still have 1 message")
}
func TestPubSub_DispatchMessage_ChannelFull_PubSub_DoesNotResetPendingRead(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
"REDIS_PUBSUB_BUFFER_SIZE": "1",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "dispatch-pubsub-full-topic"
msgChan := client.PubSub.ensureSubscription(ctx, topic)
// Fill channel
msgChan <- pubsub.NewMessage(ctx)
// Create pubsub message (not stream message)
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte("dropped-pubsub-message")
// PubSub messages also have Committer, but it's *pubSubMessage, not *streamMessage
redisMsg := &redis.Message{Payload: "test"}
msg.Committer = newPubSubMessage(redisMsg)
psClient := client.PubSub
// Set pendingRead to true (though it shouldn't matter for PubSub)
psClient.mu.Lock()
psClient.pendingRead[topic] = true
psClient.mu.Unlock()
psClient.dispatchMessage(ctx, topic, msg)
// Verify pendingRead was NOT reset (should remain true for PubSub)
psClient.mu.RLock()
pendingRead := psClient.pendingRead[topic]
psClient.mu.RUnlock()
assert.True(t, pendingRead, "pendingRead should NOT be reset for PubSub messages")
assert.Len(t, msgChan, 1, "channel should still have 1 message")
}
func TestPubSub_DispatchMessage_ChannelClosed(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "dispatch-closed-topic"
msgChan := client.PubSub.ensureSubscription(ctx, topic)
psClient := client.PubSub
psClient.mu.Lock()
psClient.chanClosed[topic] = true
psClient.mu.Unlock()
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte("ignored")
psClient.dispatchMessage(ctx, topic, msg)
assert.NotNil(t, msgChan)
}
func TestPubSub_DispatchMessage_TopicNotExists(t *testing.T) {
t.Parallel()
client, s := setupTest(t, nil)
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "non-existent-topic"
msg := pubsub.NewMessage(ctx)
msg.Topic = topic
msg.Value = []byte("test")
psClient := client.PubSub
psClient.dispatchMessage(ctx, topic, msg)
assert.NotNil(t, psClient)
}
func TestPubSub_CalculatePELCount_Ratio_0_0(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.0",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.config.PubSubStreamsConfig.PELRatio = 0.0
pelCount := calculatePELCount(100, 0.0)
newCount := int64(100) - pelCount
assert.Equal(t, int64(0), pelCount, "PEL count should be 0 for ratio 0.0")
assert.Equal(t, int64(100), newCount, "New count should be 100 for ratio 0.0")
}
func TestPubSub_CalculatePELCount_Ratio_1_0(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "1.0",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.config.PubSubStreamsConfig.PELRatio = 1.0
pelCount := calculatePELCount(100, 1.0)
newCount := int64(100) - pelCount
assert.Equal(t, int64(100), pelCount, "PEL count should be 100 for ratio 1.0")
assert.Equal(t, int64(0), newCount, "New count should be 0 for ratio 1.0")
}
func TestPubSub_CalculatePELCount_Ratio_0_5(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.5",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.config.PubSubStreamsConfig.PELRatio = 0.5
pelCount := calculatePELCount(100, 0.5)
newCount := int64(100) - pelCount
assert.Equal(t, int64(50), pelCount, "PEL count should be 50 for ratio 0.5")
assert.Equal(t, int64(50), newCount, "New count should be 50 for ratio 0.5")
}
func TestPubSub_CalculatePELCount_Ratio_0_7(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.7",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.config.PubSubStreamsConfig.PELRatio = 0.7
pelCount := calculatePELCount(100, 0.7)
newCount := int64(100) - pelCount
assert.Equal(t, int64(70), pelCount, "PEL count should be 70 for ratio 0.7")
assert.Equal(t, int64(30), newCount, "New count should be 30 for ratio 0.7")
}
func TestPubSub_CalculatePELCount_SmallCapacity(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.7",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
psClient.config.PubSubStreamsConfig.PELRatio = 0.7
// Test with small capacity (10)
pelCount := calculatePELCount(10, 0.7)
newCount := int64(10) - pelCount
assert.Equal(t, int64(7), pelCount, "PEL count should be 7 for capacity 10 with ratio 0.7")
assert.Equal(t, int64(3), newCount, "New count should be 3 for capacity 10 with ratio 0.7")
assert.Equal(t, int64(10), pelCount+newCount, "Total should equal capacity")
}
func TestPubSub_CalculatePELCount_ZeroCapacity(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer s.Close()
defer client.Close()
pelCount := calculatePELCount(0, 0.7)
newCount := int64(0) - pelCount
assert.Equal(t, int64(0), pelCount, "PEL count should be 0 for zero capacity")
assert.Equal(t, int64(0), newCount, "New count should be 0 for zero capacity")
}
func TestPubSub_ConsumeStreamMessages_RatioBasedMixing(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.5",
"REDIS_PUBSUB_BUFFER_SIZE": "10",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "ratio-mixing-topic"
group := "test-group"
// Create topic and consumer group
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
// Ensure subscription to create channel
msgChan := client.PubSub.ensureSubscription(ctx, topic)
require.NotNil(t, msgChan)
// Verify ratio is set correctly
psClient := client.PubSub
require.NotNil(t, psClient.config.PubSubStreamsConfig)
assert.InEpsilon(t, 0.5, psClient.config.PubSubStreamsConfig.PELRatio, 0.001, "PEL ratio should be 0.5")
// Test that consumeStreamMessages can be called (it will check capacity and return if 0)
// Since channel is empty, available should be 10
available := psClient.getAvailableCapacity(topic)
assert.Positive(t, available, "Channel should have available capacity")
// Call consumeStreamMessages - it should attempt to read based on ratio
// This is a basic smoke test to ensure the function doesn't panic
psClient.consumeStreamMessages(ctx, topic, group, "test-consumer", 1*time.Second)
}
func TestPubSub_ConsumeStreamMessages_Ratio_0_0_FillsWithNew(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "0.0",
"REDIS_PUBSUB_BUFFER_SIZE": "10",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "ratio-0-topic"
group := "test-group"
// Create topic and consumer group
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
// Ensure subscription to create channel
msgChan := client.PubSub.ensureSubscription(ctx, topic)
require.NotNil(t, msgChan)
// Verify ratio is set correctly
psClient := client.PubSub
require.NotNil(t, psClient.config.PubSubStreamsConfig)
assert.Zero(t, psClient.config.PubSubStreamsConfig.PELRatio, "PEL ratio should be 0.0")
// With ratio 0.0, pelCount should be 0, so PEL won't be read (unless first iteration)
// But remaining capacity should be filled with new messages
available := psClient.getAvailableCapacity(topic)
assert.Positive(t, available, "Channel should have available capacity")
// Mark as already read pending to skip first iteration logic
psClient.mu.Lock()
psClient.pendingRead[topic] = true
psClient.mu.Unlock()
// Call consumeStreamMessages - with ratio 0.0 and pendingRead=true,
// it should skip PEL and fill all capacity with new messages
psClient.consumeStreamMessages(ctx, topic, group, "test-consumer", 1*time.Second)
// Verify that new messages would be read (capacity should be used)
// This is a smoke test - actual message reading depends on Redis state
}
func TestPubSub_ConsumeStreamMessages_Ratio_1_0_FillsRemainingWithNew(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
"REDIS_STREAMS_PEL_RATIO": "1.0",
"REDIS_PUBSUB_BUFFER_SIZE": "10",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "ratio-1-topic"
group := "test-group"
// Create topic and consumer group
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
// Ensure subscription to create channel
msgChan := client.PubSub.ensureSubscription(ctx, topic)
require.NotNil(t, msgChan)
// Verify ratio is set correctly
psClient := client.PubSub
require.NotNil(t, psClient.config.PubSubStreamsConfig)
assert.InEpsilon(t, 1.0, psClient.config.PubSubStreamsConfig.PELRatio, 0.001, "PEL ratio should be 1.0")
// With ratio 1.0, pelCount should equal available capacity
// After reading PEL, remaining capacity should be filled with new messages
available := psClient.getAvailableCapacity(topic)
assert.Positive(t, available, "Channel should have available capacity")
// Mark as not read pending to allow PEL read
psClient.mu.Lock()
psClient.pendingRead[topic] = false
psClient.mu.Unlock()
// Call consumeStreamMessages - with ratio 1.0,
// it should read PEL first, then fill remaining with new messages
psClient.consumeStreamMessages(ctx, topic, group, "test-consumer", 1*time.Second)
// After PEL read, remaining capacity should be filled with new
// This is a smoke test - actual behavior depends on Redis state
}
func TestPubSub_CheckGroupExists_GroupExists(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "check-grp",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "check-group-topic"
group := "check-grp"
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
psClient := client.PubSub
exists := psClient.checkGroupExists(ctx, topic, group)
assert.True(t, exists)
}
func TestPubSub_CheckGroupExists_GroupNotExists(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "check-group-not-exists-topic"
group := "non-existent-group"
_, err := s.XAdd(topic, "0-1", []string{"payload", "data"})
require.NoError(t, err)
psClient := client.PubSub
exists := psClient.checkGroupExists(ctx, topic, group)
assert.False(t, exists)
}
func TestPubSub_CheckGroupExists_StreamNotExists(t *testing.T) {
t.Parallel()
client, s := setupTest(t, nil)
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "non-existent-stream"
group := "test-group"
psClient := client.PubSub
exists := psClient.checkGroupExists(ctx, topic, group)
assert.False(t, exists)
}
func TestPubSub_EnsureConsumerGroup_CreateNew(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "ensure-new-grp",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "ensure-group-new-topic"
group := "ensure-new-grp"
_, err := s.XAdd(topic, "0-1", []string{"payload", "data"})
require.NoError(t, err)
psClient := client.PubSub
result := psClient.ensureConsumerGroup(ctx, topic, group)
assert.True(t, result)
}
func TestPubSub_GetConsumerName_WithConfig(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-grp",
"REDIS_STREAMS_CONSUMER_NAME": "custom-consumer",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
name := psClient.getConsumerName()
assert.Equal(t, "custom-consumer", name)
}
func TestPubSub_GetConsumerName_WithoutConfig(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-grp",
})
defer s.Close()
defer client.Close()
psClient := client.PubSub
name := psClient.getConsumerName()
assert.Contains(t, name, "consumer-")
assert.NotEmpty(t, name)
}
func TestPubSub_UnsubscribeFromRedis_NotExists(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
topic := "unsub-not-exists-topic"
psClient := client.PubSub
psClient.unsubscribeFromRedis(topic)
assert.NotNil(t, psClient)
}
func TestPubSub_Close_WithSubscriptions(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
ctx := context.Background()
topic := "close-sub-topic"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
go func() {
_, _ = client.PubSub.Subscribe(ctx, "close-sub-topic-2")
}()
time.Sleep(200 * time.Millisecond)
err := client.PubSub.Close()
require.NoError(t, err)
psClient := client.PubSub
psClient.mu.RLock()
subCount := len(psClient.subStarted)
chanCount := len(psClient.receiveChan)
psClient.mu.RUnlock()
assert.Equal(t, 0, subCount)
assert.Equal(t, 0, chanCount)
}
func TestPubSub_Close_NoSubscriptions(t *testing.T) {
t.Parallel()
client, s := setupTest(t, nil)
defer s.Close()
err := client.PubSub.Close()
require.NoError(t, err)
assert.NotNil(t, client.PubSub)
}
func TestPubSub_Close_WithStreamConsumers(t *testing.T) {
t.Parallel()
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "close-grp",
})
defer s.Close()
ctx := context.Background()
topic := "close-stream-topic"
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err)
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(200 * time.Millisecond)
err = client.PubSub.Close()
require.NoError(t, err)
psClient := client.PubSub
psClient.mu.RLock()
consumerCount := len(psClient.streamConsumers)
psClient.mu.RUnlock()
assert.Equal(t, 0, consumerCount)
}
func TestPubSub_SubscribeToChannel_NilPubSub(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
// Stop monitorConnection
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cancel()
done := make(chan struct{})
go func() {
_ = client.PubSub.subscribeToChannel(ctx, "test-topic")
close(done)
}()
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("subscribeToChannel did not return")
}
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_SubscribeToChannel_NilChannel(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan *redis.Message)
close(ch)
done := make(chan struct{})
go func() {
client.PubSub.processMessages(ctx, "test-topic", ch)
close(done)
}()
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("processMessages did not return")
}
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_EnsureConsumerGroup_GroupNotExists(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "test-stream"
group := "test-group"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{})
mock.ExpectXGroupCreateMkStream(topic, group, "$").SetVal("OK")
result := client.PubSub.ensureConsumerGroup(ctx, topic, group)
assert.True(t, result, "should return true after creating group")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_EnsureConsumerGroup_CreateFails(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "test-stream"
group := "test-group"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{})
mock.ExpectXGroupCreateMkStream(topic, group, "$").SetErr(errMockGroup)
result := client.PubSub.ensureConsumerGroup(ctx, topic, group)
assert.False(t, result, "should return false when group creation fails")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CheckGroupExists_Error(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "test-stream"
group := "test-group"
mock.ExpectXInfoGroups(topic).SetErr(errMockGroup)
result := client.PubSub.checkGroupExists(ctx, topic, group)
assert.False(t, result, "should return false on error")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CheckGroupExists_GroupFound(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
group := "test-group"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{
{Name: "other-group"},
{Name: "test-group"},
{Name: "another-group"},
})
result := client.PubSub.checkGroupExists(ctx, topic, group)
assert.True(t, result, "should return true when group found")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CheckGroupExists_GroupNotFound(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "test-stream"
group := "test-group"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{
{Name: "other-group"},
{Name: "another-group"},
})
result := client.PubSub.checkGroupExists(ctx, topic, group)
assert.False(t, result, "should return false when group not found")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CleanupSubscription_ChannelNotClosed(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
topic := "test-topic"
client.PubSub.mu.Lock()
ch := make(chan *pubsub.Message, 1)
client.PubSub.receiveChan[topic] = ch
client.PubSub.chanClosed[topic] = false
client.PubSub.subStarted[topic] = struct{}{}
client.PubSub.mu.Unlock()
client.PubSub.cleanupSubscription(topic)
client.PubSub.mu.Lock()
_, exists := client.PubSub.receiveChan[topic]
_, startedExists := client.PubSub.subStarted[topic]
_, closedExists := client.PubSub.chanClosed[topic]
client.PubSub.mu.Unlock()
assert.False(t, exists, "receiveChan should be deleted")
assert.False(t, startedExists, "subStarted should be deleted")
assert.False(t, closedExists, "chanClosed should be deleted")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CleanupSubscription_ChannelAlreadyClosed(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
topic := "test-topic"
client.PubSub.mu.Lock()
ch := make(chan *pubsub.Message)
close(ch)
client.PubSub.receiveChan[topic] = ch
client.PubSub.chanClosed[topic] = true
client.PubSub.subStarted[topic] = struct{}{}
client.PubSub.mu.Unlock()
client.PubSub.cleanupSubscription(topic)
client.PubSub.mu.Lock()
_, exists := client.PubSub.receiveChan[topic]
_, startedExists := client.PubSub.subStarted[topic]
_, closedExists := client.PubSub.chanClosed[topic]
client.PubSub.mu.Unlock()
assert.False(t, exists, "receiveChan should be deleted")
assert.False(t, startedExists, "subStarted should be deleted")
assert.False(t, closedExists, "chanClosed should be deleted")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CleanupSubscription_NoChannel(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
topic := "test-topic"
client.PubSub.mu.Lock()
client.PubSub.subStarted[topic] = struct{}{}
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
client.PubSub.cleanupSubscription(topic)
client.PubSub.mu.Lock()
_, startedExists := client.PubSub.subStarted[topic]
_, closedExists := client.PubSub.chanClosed[topic]
client.PubSub.mu.Unlock()
assert.False(t, startedExists, "subStarted should be deleted")
assert.False(t, closedExists, "chanClosed should be deleted")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_DeleteTopic_EmptyTopic(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
ctx := context.Background()
err := client.PubSub.DeleteTopic(ctx, "")
require.NoError(t, err, "should return nil for empty topic")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_DeleteTopic_PubSubMode_NoActiveSubscription(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx := context.Background()
topic := "test-topic"
err := client.PubSub.DeleteTopic(ctx, topic)
require.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_DeleteTopic_PubSubMode_WithActiveSubscription(t *testing.T) {
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
ctx := context.Background()
topic := "test-topic"
go func() {
_, _ = client.PubSub.Subscribe(ctx, topic)
}()
time.Sleep(100 * time.Millisecond)
err := client.PubSub.DeleteTopic(ctx, topic)
require.NoError(t, err)
}
func TestPubSub_DeleteTopic_StreamMode(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
mock.ExpectPing().SetVal("PONG")
mock.ExpectDel(topic).SetVal(1)
err := client.PubSub.DeleteTopic(ctx, topic)
require.NoError(t, err)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_UnsubscribeFromRedis_NoPubSub(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
topic := "test-topic"
client.PubSub.unsubscribeFromRedis(topic)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_UnsubscribeFromRedis_NilPubSub(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
topic := "test-topic"
client.PubSub.mu.Lock()
client.PubSub.subPubSub[topic] = nil
client.PubSub.mu.Unlock()
client.PubSub.unsubscribeFromRedis(topic)
client.PubSub.mu.Lock()
delete(client.PubSub.subPubSub, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_UnsubscribeFromRedis_Error(t *testing.T) {
client, s := setupTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer s.Close()
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
topic := "test-topic"
ctx := context.Background()
redisPubSub := client.Redis.Client.Subscribe(ctx, topic)
client.PubSub.mu.Lock()
client.PubSub.subPubSub[topic] = redisPubSub
client.PubSub.mu.Unlock()
s.Close()
client.PubSub.unsubscribeFromRedis(topic)
client.PubSub.mu.Lock()
delete(client.PubSub.subPubSub, topic)
client.PubSub.mu.Unlock()
}
func TestPubSub_CreateTopic_PubSubMode(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
ctx := context.Background()
topic := "test-channel"
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err, "should return nil for pubsub mode")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CreateTopic_StreamMode_GroupExists(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{
{Name: "test-group"},
})
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err, "should return nil when group exists")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CreateTopic_StreamMode_BusyGroup(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
mock.ExpectXInfoGroups(topic).SetVal([]redis.XInfoGroup{})
mock.ExpectXGroupCreateMkStream(topic, "test-group", "$").SetErr(errBusyGroup)
err := client.PubSub.CreateTopic(ctx, topic)
require.NoError(t, err, "should return nil for BUSYGROUP error")
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_CollectMessages_ContextDone(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan *redis.Message, 1)
ch <- &redis.Message{Payload: "msg1"}
// Cancel context immediately
cancel()
ps := &PubSub{}
result := ps.collectMessages(ctx, ch, 10)
assert.Empty(t, result, "should return empty when context done")
}
func TestPubSub_CollectMessages_ChannelClosed(t *testing.T) {
ctx := context.Background()
ch := make(chan *redis.Message)
close(ch)
ps := &PubSub{}
result := ps.collectMessages(ctx, ch, 10)
assert.Empty(t, result, "should return empty when channel closed")
}
func TestPubSub_CollectMessages_NilMessage(t *testing.T) {
ctx := context.Background()
ch := make(chan *redis.Message, 2)
ch <- nil
ch <- &redis.Message{Payload: "msg1"}
close(ch)
ps := &PubSub{}
result := ps.collectMessages(ctx, ch, 10)
assert.Equal(t, []byte("msg1"), result, "should skip nil messages")
}
func TestPubSub_CollectMessages_ReachesLimit(t *testing.T) {
ctx := context.Background()
ch := make(chan *redis.Message, 3)
ch <- &redis.Message{Payload: "msg1"}
ch <- &redis.Message{Payload: "msg2"}
ch <- &redis.Message{Payload: "msg3"}
close(ch)
ps := &PubSub{}
result := ps.collectMessages(ctx, ch, 2)
expected := []byte("msg1\nmsg2")
assert.Equal(t, expected, result, "should stop at limit")
}
func TestPubSub_ProcessMessages_ChannelClosed(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan *redis.Message)
close(ch)
done := make(chan struct{})
go func() {
client.PubSub.processMessages(ctx, "test-topic", ch)
close(done)
}()
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("processMessages did not return when channel closed")
}
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_ProcessMessages_NilMessage(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
ch := make(chan *redis.Message, 1)
ch <- nil
go func() {
time.Sleep(50 * time.Millisecond)
ch <- &redis.Message{Channel: "test", Payload: "data"}
time.Sleep(50 * time.Millisecond)
cancel()
}()
client.PubSub.processMessages(ctx, "test-topic", ch)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_ProcessMessages_ContextDone(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "pubsub",
})
defer client.Close()
if client.PubSub.cancel != nil {
client.PubSub.cancel()
time.Sleep(10 * time.Millisecond)
}
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan *redis.Message)
done := make(chan struct{})
go func() {
client.PubSub.processMessages(ctx, "test-topic", ch)
close(done)
}()
cancel()
select {
case <-done:
case <-time.After(1 * time.Second):
t.Fatal("processMessages did not return when context canceled")
}
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_DispatchMessage_ChannelNotExists(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
ctx := context.Background()
topic := "non-existent-topic"
msg := pubsub.NewMessage(ctx)
client.PubSub.dispatchMessage(ctx, topic, msg)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_DispatchMessage_ContextDone(t *testing.T) {
client, mock := setupMockTest(t, nil)
defer client.Close()
ctx, cancel := context.WithCancel(context.Background())
topic := "test-topic"
client.PubSub.mu.Lock()
client.PubSub.receiveChan[topic] = make(chan *pubsub.Message, 1)
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
msg1 := pubsub.NewMessage(ctx)
client.PubSub.receiveChan[topic] <- msg1
cancel()
msg2 := pubsub.NewMessage(ctx)
client.PubSub.dispatchMessage(ctx, topic, msg2)
client.PubSub.mu.Lock()
close(client.PubSub.receiveChan[topic])
delete(client.PubSub.receiveChan, topic)
delete(client.PubSub.chanClosed, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HandleStreamMessage_StringPayload(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
group := "test-group"
msg := &redis.XMessage{
ID: "123-0",
Values: map[string]any{
"payload": "string-payload",
},
}
client.PubSub.mu.Lock()
client.PubSub.receiveChan[topic] = make(chan *pubsub.Message, 1)
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
client.PubSub.handleStreamMessage(ctx, topic, msg, group)
select {
case received := <-client.PubSub.receiveChan[topic]:
require.NotNil(t, received)
assert.Equal(t, topic, received.Topic)
assert.Equal(t, []byte("string-payload"), received.Value)
assert.NotNil(t, received.Committer)
case <-time.After(100 * time.Millisecond):
t.Fatal("message was not dispatched")
}
client.PubSub.mu.Lock()
close(client.PubSub.receiveChan[topic])
delete(client.PubSub.receiveChan, topic)
delete(client.PubSub.chanClosed, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HandleStreamMessage_BytePayload(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
group := "test-group"
msg := &redis.XMessage{
ID: "123-0",
Values: map[string]any{
"payload": []byte("byte-payload"),
},
}
client.PubSub.mu.Lock()
client.PubSub.receiveChan[topic] = make(chan *pubsub.Message, 1)
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
client.PubSub.handleStreamMessage(ctx, topic, msg, group)
select {
case received := <-client.PubSub.receiveChan[topic]:
require.NotNil(t, received)
assert.Equal(t, topic, received.Topic)
assert.Equal(t, []byte("byte-payload"), received.Value)
case <-time.After(100 * time.Millisecond):
t.Fatal("message was not dispatched")
}
client.PubSub.mu.Lock()
close(client.PubSub.receiveChan[topic])
delete(client.PubSub.receiveChan, topic)
delete(client.PubSub.chanClosed, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HandleStreamMessage_MissingPayload(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
group := "test-group"
msg := &redis.XMessage{
ID: "123-0",
Values: map[string]any{},
}
client.PubSub.mu.Lock()
client.PubSub.receiveChan[topic] = make(chan *pubsub.Message, 1)
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
client.PubSub.handleStreamMessage(ctx, topic, msg, group)
select {
case received := <-client.PubSub.receiveChan[topic]:
require.NotNil(t, received)
assert.Equal(t, topic, received.Topic)
assert.Nil(t, received.Value)
case <-time.After(100 * time.Millisecond):
t.Fatal("message was not dispatched")
}
client.PubSub.mu.Lock()
close(client.PubSub.receiveChan[topic])
delete(client.PubSub.receiveChan, topic)
delete(client.PubSub.chanClosed, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestPubSub_HandleStreamMessage_UnsupportedPayloadType(t *testing.T) {
client, mock := setupMockTest(t, map[string]string{
"REDIS_PUBSUB_MODE": "streams",
"REDIS_STREAMS_CONSUMER_GROUP": "test-group",
})
defer client.Close()
ctx := context.Background()
topic := "test-stream"
group := "test-group"
msg := &redis.XMessage{
ID: "123-0",
Values: map[string]any{
"payload": 12345,
},
}
client.PubSub.mu.Lock()
client.PubSub.receiveChan[topic] = make(chan *pubsub.Message, 1)
client.PubSub.chanClosed[topic] = false
client.PubSub.mu.Unlock()
client.PubSub.handleStreamMessage(ctx, topic, msg, group)
select {
case received := <-client.PubSub.receiveChan[topic]:
require.NotNil(t, received)
assert.Equal(t, topic, received.Topic)
assert.Nil(t, received.Value)
case <-time.After(100 * time.Millisecond):
t.Fatal("message was not dispatched")
}
client.PubSub.mu.Lock()
close(client.PubSub.receiveChan[topic])
delete(client.PubSub.receiveChan, topic)
delete(client.PubSub.chanClosed, topic)
client.PubSub.mu.Unlock()
assert.NoError(t, mock.ExpectationsWereMet())
}
================================================
FILE: pkg/gofr/datasource/redis/redis.go
================================================
package redis
import (
"context"
"crypto/tls"
"errors"
"strconv"
"sync"
"time"
otel "github.com/redis/go-redis/extra/redisotel/v9"
"github.com/redis/go-redis/v9"
otelglobal "go.opentelemetry.io/otel"
oteltrace "go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/datasource/pubsub"
)
const (
redisPingTimeout = 5 * time.Second
// PubSub constants.
defaultRetryTimeout = 10 * time.Second
subscribeRetryInterval = 100 * time.Millisecond // Shorter interval for connection checks in Subscribe
unsubscribeOpTimeout = 2 * time.Second
goroutineWaitTimeout = 5 * time.Second
)
var (
// PubSub errors.
errClientNotConnected = errors.New("redis client not connected")
errEmptyTopicName = errors.New("topic name cannot be empty")
errPubSubConnectionFailed = errors.New("failed to create PubSub connection for query")
errPubSubChannelFailed = errors.New("failed to get channel from PubSub for query")
errConsumerGroupNotProvided = errors.New("consumer group must be provided for streams mode")
errPubSubConnectionFailedForTopic = errors.New("failed to create PubSub connection for topic")
errPubSubChannelFailedForTopic = errors.New("failed to get channel from PubSub for topic")
errConsumerGroupNotConfigured = errors.New("consumer group not configured for stream")
errFailedToEnsureConsumerGroup = errors.New("failed to ensure consumer group for stream")
)
type Config struct {
HostName string
Username string
Password string
Port int
DB int
Options *redis.Options
TLS *tls.Config
// PubSub configuration
PubSubMode string // "pubsub" or "streams"
PubSubStreamsConfig *StreamsConfig
PubSubBufferSize int // Message buffer size for channels (default: 100)
PubSubQueryTimeout time.Duration // Default query timeout (default: 5s)
PubSubQueryLimit int // Default query message limit (default: 10)
}
// StreamsConfig holds configuration for Redis Streams.
type StreamsConfig struct {
// ConsumerGroup is the name of the consumer group (required for Streams)
ConsumerGroup string
// ConsumerName is the name of the consumer (optional, auto-generated if empty)
ConsumerName string
// MaxLen is the maximum length of the stream (optional)
// If > 0, the stream will be trimmed to this length on publish
MaxLen int64
// Block is the blocking duration for XREADGROUP (optional)
// If > 0, calls will block for this duration waiting for new messages
Block time.Duration
// PELRatio is the ratio of PEL (pending) messages to read (0.0-1.0)
// Default: 0.7 (70% PEL, 30% new messages)
// 0.0 = only new messages, 1.0 = only PEL messages
PELRatio float64
}
type Redis struct {
*redis.Client
logger datasource.Logger
config *Config
stopSignal chan struct{}
closeOnce sync.Once
}
// PubSub handles Redis PubSub operations.
type PubSub struct {
// Reference to Redis client connection
client *redis.Client
// Configuration, logger, and metrics
config *Config
logger datasource.Logger
metrics Metrics
// Tracer for OpenTelemetry distributed tracing
tracer oteltrace.Tracer
// Subscription management
receiveChan map[string]chan *pubsub.Message
subStarted map[string]struct{}
subCancel map[string]context.CancelFunc
subPubSub map[string]*redis.PubSub // Track active PubSub connections for unsubscribe
subWg map[string]*sync.WaitGroup
chanClosed map[string]bool
closeOnce map[string]*sync.Once // Ensure channels are closed only once
streamConsumers map[string]*streamConsumer
pendingRead map[string]bool // Track if pending messages have been read for streams
mu sync.RWMutex
ctx context.Context
cancel context.CancelFunc
}
// streamConsumer represents a consumer in a Redis Stream consumer group.
type streamConsumer struct {
stream string
group string
consumer string
cancel context.CancelFunc
}
// NewClient returns a [Redis] client if connection is successful based on [Config].
// Supports both plain and TLS connections. TLS is configured via REDIS_TLS_ENABLED and related environment variables.
// In case of error, it returns an error as second parameter.
func NewClient(c config.Config, logger datasource.Logger, metrics Metrics) *Redis {
redisConfig := getRedisConfig(c, logger)
// if Hostname is not provided, we won't try to connect to Redis
if redisConfig.HostName == "" {
return nil
}
rc := redis.NewClient(redisConfig.Options)
rc.AddHook(&redisHook{config: redisConfig, logger: logger, metrics: metrics})
ctx, cancel := context.WithTimeout(context.TODO(), redisPingTimeout)
defer cancel()
stopSignal := make(chan struct{})
if err := rc.Ping(ctx).Err(); err == nil {
if err = otel.InstrumentTracing(rc); err != nil {
logger.Errorf("could not add tracing instrumentation, error: %s", err)
}
logger.Infof("connected to redis at %s:%d on database %d", redisConfig.HostName, redisConfig.Port, redisConfig.DB)
} else {
logger.Errorf("could not connect to redis at '%s:%d' , error: %s", redisConfig.HostName, redisConfig.Port, err)
go retryConnect(rc, logger, stopSignal)
}
r := &Redis{
Client: rc,
config: redisConfig,
logger: logger,
stopSignal: stopSignal,
}
return r
}
// retryConnect handles the retry mechanism for connecting to Redis.
func retryConnect(client *redis.Client, logger datasource.Logger, stopSignal <-chan struct{}) {
for {
select {
case <-stopSignal:
return
case <-time.After(defaultRetryTimeout):
}
ctx, cancel := context.WithTimeout(context.Background(), redisPingTimeout)
err := client.Ping(ctx).Err()
cancel()
if err == nil {
if err = otel.InstrumentTracing(client); err != nil {
logger.Errorf("could not add tracing instrumentation, error: %s", err)
}
logger.Info("connected to redis successfully")
return
}
logger.Errorf("could not connect to redis, error: %s", err)
}
}
// Close shuts down the Redis client, ensuring the current dataset is saved before exiting.
func (r *Redis) Close() error {
r.closeOnce.Do(func() {
close(r.stopSignal)
})
if r.Client != nil {
return r.Client.Close()
}
return nil
}
// NewPubSub creates a new PubSub client that implements pubsub.Client interface.
// This allows Redis PubSub to be initialized directly without type assertion,
// aligning with the pattern used by Kafka, MQTT, and Google PubSub implementations.
func NewPubSub(conf config.Config, logger datasource.Logger, metrics Metrics) pubsub.Client {
redisConfig := getRedisConfig(conf, logger)
// if Hostname is not provided, we won't try to connect to Redis
if redisConfig.HostName == "" {
return nil
}
// Allow PubSub to use a different Redis logical DB than the primary Redis datasource.
// This prevents keyspace collisions (e.g., HASH vs STREAM on `gofr_migrations`) when Redis is used for both
// migrations and PubSub (streams mode).
//
// If not set or invalid, we default to database 15 (highest default Redis database)
// to avoid collisions with the main Redis database (typically 0).
setPubSubDB(conf, redisConfig)
rc := redis.NewClient(redisConfig.Options)
rc.AddHook(&redisHook{config: redisConfig, logger: logger, metrics: metrics})
ps := newPubSub(rc, redisConfig, logger, metrics)
ctx, cancel := context.WithTimeout(context.TODO(), redisPingTimeout)
defer cancel()
if err := rc.Ping(ctx).Err(); err == nil {
if err = otel.InstrumentTracing(rc); err != nil {
logger.Errorf("could not add tracing instrumentation, error: %s", err)
}
logger.Infof("connected to redis at %s:%d on database %d", redisConfig.HostName, redisConfig.Port, redisConfig.DB)
} else {
logger.Errorf("could not connect to redis at '%s:%d' , error: %s", redisConfig.HostName, redisConfig.Port, err)
go retryConnect(rc, logger, ps.ctx.Done())
}
return ps
}
// setPubSubDB sets the PubSub database number from config or defaults to 15.
func setPubSubDB(conf config.Config, redisConfig *Config) {
dbStr := conf.GetOrDefault("REDIS_PUBSUB_DB", strconv.Itoa(defaultPubSubDB))
db, err := strconv.Atoi(dbStr)
if err != nil || db < 0 {
// Invalid value, use default
redisConfig.DB = defaultPubSubDB
if redisConfig.Options != nil {
redisConfig.Options.DB = defaultPubSubDB
}
return
}
// Valid value, use it
redisConfig.DB = db
if redisConfig.Options != nil {
redisConfig.Options.DB = db
}
}
// newPubSub creates a new PubSub instance.
func newPubSub(client *redis.Client, redisCfg *Config, logger datasource.Logger, metrics Metrics) *PubSub {
ps := &PubSub{
client: client,
config: redisCfg,
logger: logger,
metrics: metrics,
tracer: otelglobal.GetTracerProvider().Tracer("gofr"),
receiveChan: make(map[string]chan *pubsub.Message),
subStarted: make(map[string]struct{}),
subCancel: make(map[string]context.CancelFunc),
subPubSub: make(map[string]*redis.PubSub),
subWg: make(map[string]*sync.WaitGroup),
chanClosed: make(map[string]bool),
closeOnce: make(map[string]*sync.Once),
streamConsumers: make(map[string]*streamConsumer),
pendingRead: make(map[string]bool),
}
ps.ctx, ps.cancel = context.WithCancel(context.Background())
go ps.monitorConnection(ps.ctx)
return ps
}
// TODO - if we make Redis an interface and expose from container we can avoid c.Redis(c, command) using methods on c and still pass c.
// type Redis interface {
// Get(string) (string, error)
// }
================================================
FILE: pkg/gofr/datasource/redis/redis_test.go
================================================
package redis
import (
"crypto/tls"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func Test_NewClient_HostNameMissing(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
mockConfig := config.NewMockConfig(map[string]string{"REDIS_HOST": ""})
client := NewClient(mockConfig, mockLogger, mockMetrics)
assert.Nil(t, client, "Test_NewClient_HostNameMissing Failed! Expected redis client to be nil")
}
func Test_NewClient_InvalidPort(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
mockConfig := config.NewMockConfig(map[string]string{"REDIS_HOST": "localhost", "REDIS_PORT": "&&^%%^&*"})
// The go-redis library may send multiple commands during initialization (hello, client, ping, etc.)
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(), "app_redis_stats", gomock.Any(), "hostname", gomock.Any(), "type", gomock.Any(),
).AnyTimes()
client := NewClient(mockConfig, mockLogger, mockMetrics)
assert.NotNil(t, client.Client, "Test_NewClient_InvalidPort Failed! Expected redis client not to be nil")
}
func TestRedis_QueryLogging(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock Redis server setup
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(),
"hostname", gomock.Any(), "type", gomock.Any()).AnyTimes()
result := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := NewClient(config.NewMockConfig(map[string]string{
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
"REDIS_DB": "1",
}), mockLogger, mockMetric)
require.NoError(t, err)
result, err := client.Set(t.Context(), "key", "value", 1*time.Minute).Result()
require.NoError(t, err)
assert.Equal(t, "OK", result)
})
// Assertions
assert.Contains(t, result, "set")
assert.Contains(t, result, "key")
assert.Contains(t, result, "value")
assert.Contains(t, result, "ex 60")
}
func TestRedis_PipelineQueryLogging(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock Redis server setup
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(),
"hostname", gomock.Any(), "type", gomock.Any()).AnyTimes()
// Execute Redis pipeline
result := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := NewClient(config.NewMockConfig(map[string]string{
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
}), mockLogger, mockMetric)
require.NoError(t, err)
// Pipeline execution
pipe := client.Pipeline()
setCmd := pipe.Set(t.Context(), "key1", "value1", 1*time.Minute)
getCmd := pipe.Get(t.Context(), "key1")
// Pipeline Exec should return a non-nil error
_, err = pipe.Exec(t.Context())
require.NoError(t, err)
// Retrieve results
setResult, err := setCmd.Result()
require.NoError(t, err)
assert.Equal(t, "OK", setResult)
getResult, err := getCmd.Result()
require.NoError(t, err)
assert.Equal(t, "value1", getResult)
})
// Assertions
// All Redis commands are now logged, including pipeline operations
assert.Contains(t, result, "connected to redis")
}
func TestRedis_Close(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Mock Redis server setup
s, err := miniredis.Run()
require.NoError(t, err)
defer s.Close()
// Mock metrics setup
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), "app_redis_stats", gomock.Any(), "hostname",
gomock.Any(), "type", gomock.Any()).AnyTimes()
mockLogger := logging.NewMockLogger(logging.DEBUG)
client := NewClient(config.NewMockConfig(map[string]string{
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
}), mockLogger, mockMetric)
err = client.Close()
require.NoError(t, err)
}
func Test_TLSConfig(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := logging.NewMockLogger(logging.ERROR)
mockConfig := config.NewMockConfig(map[string]string{
"REDIS_HOST": "localhost",
"REDIS_TLS_ENABLED": "true",
})
conf := getRedisConfig(mockConfig, mockLogger)
assert.NotNil(t, conf.TLS, "Expected TLS config to be set")
assert.EqualValues(t, tls.VersionTLS12, conf.TLS.MinVersion, "Expected TLS 1.2")
}
func Test_TLSConfigWithDummyPEM(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockPEM := getMockPEM()
mockKey := getMockKey()
mockLogger := logging.NewMockLogger(logging.ERROR)
mockConfig := config.NewMockConfig(map[string]string{
"REDIS_HOST": "localhost",
"REDIS_TLS_ENABLED": "true",
"REDIS_TLS_CA_CERT": mockPEM,
"REDIS_TLS_CERT": mockPEM,
"REDIS_TLS_KEY": mockKey,
})
conf := getRedisConfig(mockConfig, mockLogger)
assert.NotNil(t, conf.TLS, "Expected TLS config to be set")
assert.EqualValues(t, tls.VersionTLS12, conf.TLS.MinVersion, "Expected TLS 1.2")
}
func getMockPEM() string {
const mockPEM = `-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzQw\n-----END CERTIFICATE-----`
return mockPEM
}
func getMockKey() string {
//nolint:gosec // dummy private key for test only, not used in production
const mockKey = `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAnzQw\n-----END RSA PRIVATE KEY-----`
return mockKey
}
================================================
FILE: pkg/gofr/datasource/scylladb/errors.go
================================================
package scylladb
import (
"errors"
"fmt"
)
var (
errUnsupportedBatchType = errors.New("batch type not supported")
errDestinationIsNotPointer = errors.New("destination is not pointer")
errBatchNotInitialized = errors.New("batch not initialized")
errUnexpectedMap = errors.New("a map was not expected")
)
type errUnexpectedPointer struct {
target string
}
func (d errUnexpectedPointer) Error() string {
return fmt.Sprintf("a pointer to %v was not expected.", d.target)
}
type errUnexpectedSlice struct {
target string
}
func (d errUnexpectedSlice) Error() string {
return fmt.Sprintf("a slice of %v was not expected.", d.target)
}
================================================
FILE: pkg/gofr/datasource/scylladb/errors_test.go
================================================
package scylladb
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_UnexpectedPointer_Error(t *testing.T) {
expected := "a pointer to int was not expected."
err := errUnexpectedPointer{target: "int"}
require.ErrorContains(t, err, expected)
}
func Test_UnexpectedSlice_Error(t *testing.T) {
expected := "a slice of int was not expected."
err := errUnexpectedSlice{target: "int"}
require.ErrorContains(t, err, expected)
}
================================================
FILE: pkg/gofr/datasource/scylladb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/scylladb
go 1.25.0
require (
github.com/gocql/gocql v1.7.0
github.com/stoewer/go-strcase v1.3.1
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/scylladb/go.sum
================================================
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932 h1:mXoPYz/Ul5HYEDvkta6I8/rnYM5gSdSV2tJ6XbZuEtY=
github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus=
github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs=
github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/scylladb/interface.go
================================================
package scylladb
import (
"github.com/gocql/gocql"
)
// clusterConfig defines methods for interacting with a ScyllaDB clusterConfig.
type clusterConfig interface {
createSession() (session, error)
}
// iterator defines methods for interacting with a ScyllaDB iterator.
type iterator interface {
Columns() []gocql.ColumnInfo
Scan(dest ...any) bool
NumRows() int
}
// query defines methods for interacting with a ScyllaDB query.
type query interface {
Exec() error
Iter() iterator
MapScanCAS(dest map[string]any) (applied bool, err error)
ScanCAS(dest ...any) (applied bool, err error)
}
// batch defines methods for interacting with a ScyllaDB batch.
type batch interface {
Query(stmt string, args ...any)
getBatch() *gocql.Batch
}
// session defines methods for interacting with a ScyllaDB session.
type session interface {
Query(stmt string, values ...any) query
newBatch(batchType gocql.BatchType) batch
executeBatch(batch batch) error
executeBatchCAS(batch batch, dest ...any) (bool, error)
}
================================================
FILE: pkg/gofr/datasource/scylladb/internal.go
================================================
package scylladb
import (
"context"
"fmt"
"reflect"
"strings"
"time"
"github.com/gocql/gocql"
"github.com/stoewer/go-strcase"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// scylladbIterator implements iterator interface.
type scylladbIterator struct {
iter *gocql.Iter
}
// Columns gets the Column information. This method wraps the `Columns` method of the underlying `iter` object.
func (s *scylladbIterator) Columns() []gocql.ColumnInfo {
return s.iter.Columns()
}
// Scan gets the next row from the ScyllaDB iterator and fills in the provided arguments.
func (s *scylladbIterator) Scan(dest ...any) bool {
return s.iter.Scan(dest...)
}
// NumRows returns a number of rows.
func (s *scylladbIterator) NumRows() int {
return s.iter.NumRows()
}
// scylladbQuery implements query interface.
type scylladbQuery struct {
query *gocql.Query
}
// Exec performs a ScyllaDB Query Exec.
func (s *scylladbQuery) Exec() error {
return s.query.Exec()
}
// Iter returns a ScyllaDB iterator.
func (s *scylladbQuery) Iter() iterator {
iter := scylladbIterator{iter: s.query.Iter()}
return &iter
}
// MapScanCAS checks a ScyllaDB query an IF clause and scans the existing data into map[string]any (if any).
// This method wraps the `MapScanCAS` method of the underlying `query` object.
func (s *scylladbQuery) MapScanCAS(dest map[string]any) (applied bool, err error) {
return s.query.MapScanCAS(dest)
}
// ScanCAS checks a ScyllaDB query with a IF clause and scans the existing data.
// This method wraps the `ScanCAS` method of the underlying `query` object.
func (s *scylladbQuery) ScanCAS(dest ...any) (applied bool, err error) {
return s.query.ScanCAS(dest)
}
// scyllaClusterConfig implements clusterConfig.
type scyllaClusterConfig struct {
clusterConfig *gocql.ClusterConfig
}
func newClusterConfig(config *Config) clusterConfig {
var s scyllaClusterConfig
config.Host = strings.TrimSuffix(strings.TrimSpace(config.Host), ",")
hosts := strings.Split(config.Host, ",")
s.clusterConfig = gocql.NewCluster(hosts...)
s.clusterConfig.Keyspace = config.Keyspace
s.clusterConfig.Port = config.Port
s.clusterConfig.Authenticator = gocql.PasswordAuthenticator{Username: config.Username, Password: config.Password}
return &s
}
// createSession creates a ScyllaDB session based on the provided configuration.
func (s *scyllaClusterConfig) createSession() (session, error) {
sess, err := s.clusterConfig.CreateSession()
if err != nil {
return nil, err
}
return &scyllaSession{session: sess}, nil
}
// scyllaSession implements session.
type scyllaSession struct {
session *gocql.Session
}
// Query creates a ScyllaDB query.
func (s *scyllaSession) Query(stmt string, values ...any) query {
return &scylladbQuery{query: s.session.Query(stmt, values...)}
}
// newBatch creates a `gocql.BatchType`.
func (s *scyllaSession) newBatch(batchType gocql.BatchType) batch {
return &scyllaBatch{batch: s.session.NewBatch(batchType)}
}
// executeBatch executes a batch operation.
func (s *scyllaSession) executeBatch(b batch) error {
gocqlBatch := b.getBatch()
return s.session.ExecuteBatch(gocqlBatch)
}
// executeBatchCAS executes a batch operation and returns true if successful.
func (s *scyllaSession) executeBatchCAS(batch batch, dest ...any) (bool, error) {
gocqlBatch := batch.getBatch()
applied, _, err := s.session.ExecuteBatchCAS(gocqlBatch, dest...)
return applied, err
}
// scyllaBatch implements batch.
type scyllaBatch struct {
batch *gocql.Batch
}
// Query adds the query to the batch operation.
func (s *scyllaBatch) Query(stmt string, args ...any) {
s.batch.Query(stmt, args...)
}
// getBatch returns the underlying `gocql.Batch`.
func (s *scyllaBatch) getBatch() *gocql.Batch {
return s.batch
}
// getFields returns a slice of field pointers from the struct, mapping columns to their corresponding fields.
func (Client) getFields(columns []string, fieldNameIndex map[string]int, v reflect.Value) []any {
fields := make([]any, len(columns))
for i, column := range columns {
if index, ok := fieldNameIndex[column]; ok {
fields[i] = v.Field(index).Addr().Interface()
} else {
fields[i] = new(any)
}
}
return fields
}
// addTrace starts a new trace span for the specified method and query.
func (c *Client) addTrace(ctx context.Context, method, query string) trace.Span {
if c.tracer == nil {
return nil
}
_, span := c.tracer.Start(ctx, fmt.Sprintf("scylladb-%v", method))
span.SetAttributes(
attribute.String("scylladb.query", query),
attribute.String("scylladb.keyspace", c.config.Keyspace),
)
return span
}
// getColumnsFromColumnsInfo Extracts and returns a slice of column names from the provided gocql.ColumnInfo slice.
func (*Client) getColumnsFromColumnsInfo(columns []gocql.ColumnInfo) []string {
cols := make([]string, 0)
for _, column := range columns {
cols = append(cols, column.Name)
}
return cols
}
// rowsToStruct Scans the iterator row data and maps it to the fields of the provided struct.
func (c *Client) rowsToStruct(iter iterator, vo reflect.Value) {
v := vo
if vo.Kind() == reflect.Ptr {
v = vo.Elem()
}
columns := c.getColumnsFromColumnsInfo(iter.Columns())
fieldNameIndex := c.getFieldNameIndex(v)
fields := c.getFields(columns, fieldNameIndex, v)
_ = iter.Scan(fields...)
if vo.CanSet() {
vo.Set(v)
}
}
// getFieldNameIndex Returns a map of field names from struct convert toSnakeCase to their index positions in the struct.
func (*Client) getFieldNameIndex(v reflect.Value) map[string]int {
fieldNameIndex := map[string]int{}
for i := 0; i < v.Type().NumField(); i++ {
var name string
f := v.Type().Field(i)
tag := f.Tag.Get("db")
if tag != "" {
name = tag
} else {
name = strcase.SnakeCase(f.Name)
}
fieldNameIndex[name] = i
}
return fieldNameIndex
}
// sendOperationStats Logs query duration and stats, records metrics, and ends the trace span if present.
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time, method string, span trace.Span) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
if span != nil {
defer span.End()
span.SetAttributes(attribute.Int64(fmt.Sprintf("scylla.%v.duration", method), duration))
}
c.metrics.RecordHistogram(context.Background(), "app_scylla_stats", float64(duration), "hostname",
c.config.Host, "keyspace", c.config.Keyspace)
c.scylla.query = nil
}
// rowsToStructCAS Scans a CAS query result into a struct, setting fields based on column names and types,
// and returns if the update was applied.
func (c *Client) rowsToStructCAS(query query, vo reflect.Value) (bool, error) {
v := vo
if vo.Kind() == reflect.Ptr {
v = vo.Elem()
}
row := make(map[string]any)
applied, err := query.MapScanCAS(row)
if err != nil {
return false, err
}
fieldNameIndex := c.getFieldNameIndex(v)
for col, value := range row {
if i, ok := fieldNameIndex[col]; ok {
field := v.Field(i)
if reflect.TypeOf(value) == field.Type() {
field.Set(reflect.ValueOf(value))
}
}
}
if vo.CanSet() {
vo.Set(v)
}
return applied, nil
}
================================================
FILE: pkg/gofr/datasource/scylladb/logger.go
================================================
package scylladb
import (
"context"
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Error(args ...any)
Infof(pattern string, args ...any)
Errorf(format string, args ...any)
Log(args ...any)
Logf(pattern string, args ...any)
}
type QueryLog struct {
Operation string `json:"operation"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Keyspace string `json:"keyspace,omitempty"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s \u001B[38;5;8m%-32s\u001B[0m\n",
clean(ql.Operation), "SCYLDB", ql.Duration, clean(ql.Keyspace), clean(ql.Query))
}
var matchSpaces = regexp.MustCompile(`\s+`)
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
query = matchSpaces.ReplaceAllString(query, " ")
query = strings.TrimSpace(query)
return query
}
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/scylladb/logger_test.go
================================================
package scylladb
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_PrettyPrint(t *testing.T) {
queryLog := QueryLog{
Query: "sample query",
Duration: 12345,
}
expected := "sample query"
var buf bytes.Buffer
queryLog.PrettyPrint(&buf)
assert.Contains(t, buf.String(), expected)
}
func Test_Clean(t *testing.T) {
testCases := []struct {
desc string
input string
expected string
}{
{"multiple spaces", " multiple spaces ", "multiple spaces"},
{"leading and trailing", "leading and trailing ", "leading and trailing"},
{"mixed white spaces", " mixed\twhite\nspaces", "mixed white spaces"},
{"single word", "singleword", "singleword"},
{"empty string", "", ""},
{"empty string with spaces", " ", ""},
}
for i, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := clean(tc.input)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
================================================
FILE: pkg/gofr/datasource/scylladb/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=scylladb
//
// Package scylladb is a generated GoMock package.
package scylladb
import (
reflect "reflect"
gocql "github.com/gocql/gocql"
gomock "go.uber.org/mock/gomock"
)
// MockclusterConfig is a mock of clusterConfig interface.
type MockclusterConfig struct {
ctrl *gomock.Controller
recorder *MockclusterConfigMockRecorder
isgomock struct{}
}
// MockclusterConfigMockRecorder is the mock recorder for MockclusterConfig.
type MockclusterConfigMockRecorder struct {
mock *MockclusterConfig
}
// NewMockclusterConfig creates a new mock instance.
func NewMockclusterConfig(ctrl *gomock.Controller) *MockclusterConfig {
mock := &MockclusterConfig{ctrl: ctrl}
mock.recorder = &MockclusterConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockclusterConfig) EXPECT() *MockclusterConfigMockRecorder {
return m.recorder
}
// createSession mocks base method.
func (m *MockclusterConfig) createSession() (session, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "createSession")
ret0, _ := ret[0].(session)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// createSession indicates an expected call of createSession.
func (mr *MockclusterConfigMockRecorder) createSession() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "createSession", reflect.TypeOf((*MockclusterConfig)(nil).createSession))
}
// Mockiterator is a mock of iterator interface.
type Mockiterator struct {
ctrl *gomock.Controller
recorder *MockiteratorMockRecorder
isgomock struct{}
}
// MockiteratorMockRecorder is the mock recorder for Mockiterator.
type MockiteratorMockRecorder struct {
mock *Mockiterator
}
// NewMockiterator creates a new mock instance.
func NewMockiterator(ctrl *gomock.Controller) *Mockiterator {
mock := &Mockiterator{ctrl: ctrl}
mock.recorder = &MockiteratorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockiterator) EXPECT() *MockiteratorMockRecorder {
return m.recorder
}
// Columns mocks base method.
func (m *Mockiterator) Columns() []gocql.ColumnInfo {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Columns")
ret0, _ := ret[0].([]gocql.ColumnInfo)
return ret0
}
// Columns indicates an expected call of Columns.
func (mr *MockiteratorMockRecorder) Columns() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Columns", reflect.TypeOf((*Mockiterator)(nil).Columns))
}
// NumRows mocks base method.
func (m *Mockiterator) NumRows() int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NumRows")
ret0, _ := ret[0].(int)
return ret0
}
// NumRows indicates an expected call of NumRows.
func (mr *MockiteratorMockRecorder) NumRows() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NumRows", reflect.TypeOf((*Mockiterator)(nil).NumRows))
}
// Scan mocks base method.
func (m *Mockiterator) Scan(dest ...any) bool {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Scan", varargs...)
ret0, _ := ret[0].(bool)
return ret0
}
// Scan indicates an expected call of Scan.
func (mr *MockiteratorMockRecorder) Scan(dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*Mockiterator)(nil).Scan), dest...)
}
// Mockquery is a mock of query interface.
type Mockquery struct {
ctrl *gomock.Controller
recorder *MockqueryMockRecorder
isgomock struct{}
}
// MockqueryMockRecorder is the mock recorder for Mockquery.
type MockqueryMockRecorder struct {
mock *Mockquery
}
// NewMockquery creates a new mock instance.
func NewMockquery(ctrl *gomock.Controller) *Mockquery {
mock := &Mockquery{ctrl: ctrl}
mock.recorder = &MockqueryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockquery) EXPECT() *MockqueryMockRecorder {
return m.recorder
}
// Exec mocks base method.
func (m *Mockquery) Exec() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Exec")
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockqueryMockRecorder) Exec() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*Mockquery)(nil).Exec))
}
// Iter mocks base method.
func (m *Mockquery) Iter() iterator {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Iter")
ret0, _ := ret[0].(iterator)
return ret0
}
// Iter indicates an expected call of Iter.
func (mr *MockqueryMockRecorder) Iter() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Iter", reflect.TypeOf((*Mockquery)(nil).Iter))
}
// MapScanCAS mocks base method.
func (m *Mockquery) MapScanCAS(dest map[string]any) (bool, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MapScanCAS", dest)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MapScanCAS indicates an expected call of MapScanCAS.
func (mr *MockqueryMockRecorder) MapScanCAS(dest any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MapScanCAS", reflect.TypeOf((*Mockquery)(nil).MapScanCAS), dest)
}
// ScanCAS mocks base method.
func (m *Mockquery) ScanCAS(dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ScanCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ScanCAS indicates an expected call of ScanCAS.
func (mr *MockqueryMockRecorder) ScanCAS(dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanCAS", reflect.TypeOf((*Mockquery)(nil).ScanCAS), dest...)
}
// Mockbatch is a mock of batch interface.
type Mockbatch struct {
ctrl *gomock.Controller
recorder *MockbatchMockRecorder
isgomock struct{}
}
// MockbatchMockRecorder is the mock recorder for Mockbatch.
type MockbatchMockRecorder struct {
mock *Mockbatch
}
// NewMockbatch creates a new mock instance.
func NewMockbatch(ctrl *gomock.Controller) *Mockbatch {
mock := &Mockbatch{ctrl: ctrl}
mock.recorder = &MockbatchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockbatch) EXPECT() *MockbatchMockRecorder {
return m.recorder
}
// Query mocks base method.
func (m *Mockbatch) Query(stmt string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Query", varargs...)
}
// Query indicates an expected call of Query.
func (mr *MockbatchMockRecorder) Query(stmt any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*Mockbatch)(nil).Query), varargs...)
}
// getBatch mocks base method.
func (m *Mockbatch) getBatch() *gocql.Batch {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "getBatch")
ret0, _ := ret[0].(*gocql.Batch)
return ret0
}
// getBatch indicates an expected call of getBatch.
func (mr *MockbatchMockRecorder) getBatch() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getBatch", reflect.TypeOf((*Mockbatch)(nil).getBatch))
}
// Mocksession is a mock of session interface.
type Mocksession struct {
ctrl *gomock.Controller
recorder *MocksessionMockRecorder
isgomock struct{}
}
// MocksessionMockRecorder is the mock recorder for Mocksession.
type MocksessionMockRecorder struct {
mock *Mocksession
}
// NewMocksession creates a new mock instance.
func NewMocksession(ctrl *gomock.Controller) *Mocksession {
mock := &Mocksession{ctrl: ctrl}
mock.recorder = &MocksessionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mocksession) EXPECT() *MocksessionMockRecorder {
return m.recorder
}
// Query mocks base method.
func (m *Mocksession) Query(stmt string, values ...any) query {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(query)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MocksessionMockRecorder) Query(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*Mocksession)(nil).Query), varargs...)
}
// executeBatch mocks base method.
func (m *Mocksession) executeBatch(batch batch) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "executeBatch", batch)
ret0, _ := ret[0].(error)
return ret0
}
// executeBatch indicates an expected call of executeBatch.
func (mr *MocksessionMockRecorder) executeBatch(batch any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeBatch", reflect.TypeOf((*Mocksession)(nil).executeBatch), batch)
}
// executeBatchCAS mocks base method.
func (m *Mocksession) executeBatchCAS(batch batch, dest ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{batch}
for _, a := range dest {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "executeBatchCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// executeBatchCAS indicates an expected call of executeBatchCAS.
func (mr *MocksessionMockRecorder) executeBatchCAS(batch any, dest ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{batch}, dest...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "executeBatchCAS", reflect.TypeOf((*Mocksession)(nil).executeBatchCAS), varargs...)
}
// newBatch mocks base method.
func (m *Mocksession) newBatch(batchType gocql.BatchType) batch {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "newBatch", batchType)
ret0, _ := ret[0].(batch)
return ret0
}
// newBatch indicates an expected call of newBatch.
func (mr *MocksessionMockRecorder) newBatch(batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "newBatch", reflect.TypeOf((*Mocksession)(nil).newBatch), batchType)
}
================================================
FILE: pkg/gofr/datasource/scylladb/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=scylladb
//
// Package scylladb is a generated GoMock package.
package scylladb
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(format string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{format}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(format any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{format}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
// Log mocks base method.
func (m *MockLogger) Log(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Log", varargs...)
}
// Log indicates an expected call of Log.
func (mr *MockLoggerMockRecorder) Log(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Log", reflect.TypeOf((*MockLogger)(nil).Log), args...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/scylladb/scylladb.go
================================================
package scylladb
import (
"context"
"errors"
"reflect"
"time"
"github.com/gocql/gocql"
"go.opentelemetry.io/otel/trace"
)
const (
LoggedBatch = iota
UnLoggedBatch
CounterBatch
)
var errStatusDown = errors.New("status down")
// Config holds the configuration settings for connecting to a ScyllaDB cluster,
// including host addresses, keyspace, port, and authentication credentials.
type Config struct {
Host string
Keyspace string
Port int
Username string
Password string
}
// ScyllaDB represents the connection and operations context for interacting with a ScyllaDB cluster,
// including configuration, active session, query handling, and initialized batches.
type ScyllaDB struct {
clusterConfig clusterConfig
session session
query query
batches map[string]batch
}
// Client is the main interface for interacting with a ScyllaDB cluster,
// managing configuration, ScyllaDB operations, logging, metrics, and tracing.
type Client struct {
config *Config
scylla *ScyllaDB
logger Logger
metrics Metrics
tracer trace.Tracer
}
// Health represents the health status of the ScyllaDB cluster,
// including the overall status (e.g., "UP" or "DOWN") and additional details.
type Health struct {
Status string `json:" status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
// New initializes ScyllaDB driver with the provided configuration.
func New(conf Config) *Client {
scylla := &ScyllaDB{clusterConfig: newClusterConfig(&conf)}
return &Client{config: &conf, scylla: scylla}
}
// Connect establishes a connection to Scylladb.
func (c *Client) Connect() {
c.logger.Debugf("connecting to ScyllaDB at %v on port %v to keyspace %v", c.config.Host, c.config.Port, c.config.Keyspace)
sess, err := c.scylla.clusterConfig.createSession()
if err != nil {
c.logger.Error("failed to connect to ScyllaDB:", err)
return
}
scyllaBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_scylla_stats", "Response time of ScyllaDB queries in microseconds", scyllaBuckets...)
c.logger.Logf("connected to '%s' keyspace at host '%s' and port '%d'", c.config.Keyspace, c.config.Host, c.config.Port)
c.scylla.session = sess
}
// UseLogger sets the logger for the scylladb client.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the scylladb client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for the scylladb client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// Query is the original method without context.
// It internally delegates to QueryWithCtx using context.Background() as the default context.
func (c *Client) Query(dest any, stmt string, values ...any) error {
return c.QueryWithCtx(context.Background(), dest, stmt, values...)
}
// Exec executes a CQL (Cassandra Query Language) statement on a ScyllaDB cluster
// with the provided values, using the default context (context.Background()).
func (c *Client) Exec(stmt string, values ...any) error {
return c.ExecWithCtx(context.Background(), stmt, values...)
}
// ExecWithCtx executes a CQL statement by using the context,statement,values and returns error.
func (c *Client) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
span := c.addTrace(ctx, "exec", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "ExecWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "exec", span)
return c.scylla.session.Query(stmt, values...).Exec()
}
// ExecCAS performs Compare and Set operation on ScyllaDB cluster.
func (c *Client) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
return c.ExecCASWithCtx(context.Background(), dest, stmt, values)
}
// ExecCASWithCtx takes default context,destination,statement,values and return bool and error.
//
//nolint:exhaustive // We just want to take care of slice and struct in this case.
func (c *Client) ExecCASWithCtx(ctx context.Context, dest any, stmt string, values ...any) (bool, error) {
var (
applied bool
err error
)
span := c.addTrace(ctx, "exec-cas", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "ExecCASWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "exec-cas", span)
rvo := reflect.ValueOf(dest)
if rvo.Kind() != reflect.Ptr {
c.logger.Errorf("we did not get a pointer. data is not settable.")
return false, errDestinationIsNotPointer
}
rv := rvo.Elem()
q := c.scylla.session.Query(stmt, values...)
switch rv.Kind() {
case reflect.Struct:
applied, err = c.rowsToStructCAS(q, rv)
case reflect.Slice:
c.logger.Debugf("a slice of %v was not expected.", reflect.SliceOf(reflect.TypeOf(dest)).String())
return false, errUnexpectedSlice{target: reflect.SliceOf(reflect.TypeOf(dest)).String()}
case reflect.Map:
c.logger.Debugf("a map was not expected.")
return false, errUnexpectedMap
default:
applied = true
}
return applied, err
}
// QueryWithCtx takes context ,destination,statement,values and returns error.
//
//nolint:exhaustive // We just want to take care of slice and struct in this case
func (c *Client) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
span := c.addTrace(ctx, "query", stmt)
defer c.sendOperationStats(&QueryLog{Operation: "QueryWithCtx", Query: stmt, Keyspace: c.config.Keyspace}, time.Now(), "query", span)
rvo := reflect.ValueOf(dest)
if rvo.Kind() != reflect.Ptr {
c.logger.Debug("we did not get a pointer. data is not settable.")
return errDestinationIsNotPointer
}
rv := rvo.Elem()
iter := c.scylla.session.Query(stmt, values...).Iter()
switch rv.Kind() {
case reflect.Slice:
numRows := iter.NumRows()
for numRows > 0 {
val := reflect.New(rv.Type().Elem())
if rv.Type().Elem().Kind() == reflect.Struct {
c.rowsToStruct(iter, val)
} else {
_ = iter.Scan(val.Interface())
}
rv = reflect.Append(rv, val.Elem())
numRows--
}
if rvo.Elem().CanSet() {
rvo.Elem().Set(rv)
}
case reflect.Struct:
c.rowsToStruct(iter, rv)
default:
c.logger.Debugf("a pointer to %v was not expected.", rv.Kind().String())
return errUnexpectedPointer{target: rv.Kind().String()}
}
return nil
}
// NewBatch creates a new batch operation for a ScyllaDB cluster with the provided
// batch name and batch type, using the default context (context.Background()).
func (c *Client) NewBatch(name string, batchType int) error {
return c.NewBatchWithCtx(context.Background(), name, batchType)
}
// NewBatchWithCtx uses context ,batch name,and batch type, and returns error.
func (c *Client) NewBatchWithCtx(_ context.Context, name string, batchType int) error {
switch batchType {
case LoggedBatch, UnLoggedBatch, CounterBatch:
if len(c.scylla.batches) == 0 {
c.scylla.batches = make(map[string]batch)
}
c.scylla.batches[name] = c.scylla.session.newBatch(gocql.BatchType(batchType))
return nil
default:
return errUnsupportedBatchType
}
}
// BatchQuery executes a batched query in a ScyllaDB cluster with the provided
// batch name, statement, and values, using the default context (context.Background()).
func (c *Client) BatchQuery(name, stmt string, values ...any) error {
return c.BatchQueryWithCtx(context.Background(), name, stmt, values...)
}
// BatchQueryWithCtx executes Query with the provided context,batch name, statement and values.
func (c *Client) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
span := c.addTrace(ctx, "batch-query", stmt)
defer c.sendOperationStats(&QueryLog{
Operation: "BatchQueryWithCtx",
Query: stmt,
Keyspace: c.config.Keyspace,
}, time.Now(), "batch-query", span)
b, ok := c.scylla.batches[name]
if !ok {
return errBatchNotInitialized
}
b.Query(stmt, values...)
return nil
}
// ExecuteBatchWithCtx executes batch with provided context, batch name and returns err.
func (c *Client) ExecuteBatchWithCtx(ctx context.Context, name string) error {
span := c.addTrace(ctx, "execute-batch", "batch")
defer c.sendOperationStats(&QueryLog{
Operation: "ExecuteBatchWithCtx",
Query: "batch",
Keyspace: c.config.Keyspace,
}, time.Now(), "execute-batch", span)
b, ok := c.scylla.batches[name]
if !ok {
return errBatchNotInitialized
}
return c.scylla.session.executeBatch(b)
}
// ExecuteBatchCAS executes a Compare and set operation on ScyllaDB cluster using the provided batch name.
func (c *Client) ExecuteBatchCAS(name string, dest ...any) (bool, error) {
return c.ExecuteBatchCASWithCtx(context.Background(), name, dest)
}
// ExecuteBatchCASWithCtx takes default context, batch name and destination returns bool and error.
func (c *Client) ExecuteBatchCASWithCtx(ctx context.Context, name string, dest ...any) (bool, error) {
span := c.addTrace(ctx, "execute-batch-cas", "batch")
defer c.sendOperationStats(&QueryLog{
Operation: "ExecuteBatchCASWithCtx",
Query: "batch",
Keyspace: c.config.Keyspace,
}, time.Now(), "execute-batch-cas", span)
b, ok := c.scylla.batches[name]
if !ok {
return false, errBatchNotInitialized
}
return c.scylla.session.executeBatchCAS(b, dest...)
}
// ExecuteBatch executes a previously initialized batch operation in a ScyllaDB cluster
// using the provided batch name, with the default context (context.Background()).
func (c *Client) ExecuteBatch(name string) error {
return c.ExecuteBatchWithCtx(context.Background(), name)
}
// HealthCheck performs a health check on the ScyllaDB cluster by querying.
func (c *Client) HealthCheck(context.Context) (any, error) {
const (
statusDown = "DOWN"
statusUp = "UP"
)
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = c.config.Host
h.Details["keyspace"] = c.config.Keyspace
if c.scylla.session == nil {
h.Status = statusDown
h.Details["message"] = "ScyllaDB not connected"
return &h, errStatusDown
}
err := c.scylla.session.Query("SELECT now() FROM system.local").Exec()
if err != nil {
h.Status = statusDown
h.Details["message"] = err.Error()
return &h, errStatusDown
}
h.Status = statusUp
return &h, nil
}
================================================
FILE: pkg/gofr/datasource/scylladb/scylladb_test.go
================================================
package scylladb
import (
"context"
"errors"
"testing"
"github.com/gocql/gocql"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
const mockBatchName = "mockBatch"
var (
errConnFail = errors.New("connection failed")
errMock = errors.New("test error")
)
type mockDependencies struct {
mockSession *Mocksession
mockQuery *Mockquery
mockBatch *Mockbatch
mockIter *Mockiterator
mockLogger *MockLogger
}
func initTest(t *testing.T) (*Client, *mockDependencies) {
t.Helper()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockSession := NewMocksession(ctrl)
mockQuery := NewMockquery(ctrl)
mockBatch := NewMockbatch(ctrl)
mockiter := NewMockiterator(ctrl)
config := Config{
Host: "host1",
Port: 9042,
Keyspace: "my_Keyspace",
}
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.scylla.session = mockSession
client.scylla.batches = map[string]batch{mockBatchName: mockBatch}
mockMetrics.EXPECT().RecordHistogram(gomock.AssignableToTypeOf(context.Background()), "app_scylla_stats",
gomock.AssignableToTypeOf(float64(0)), "hostname", client.config.Host, "keyspace", client.config.Keyspace).
AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf("we did not get a pointer. data is not settable.").AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
return client, &mockDependencies{mockSession: mockSession, mockQuery: mockQuery, mockBatch: mockBatch,
mockIter: mockiter, mockLogger: mockLogger}
}
func TestScyllaDB_Connect(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockClusterConfig := NewMockclusterConfig(ctrl)
config := Config{
Host: "host1",
Port: 9042,
Keyspace: "my_keyspace",
}
scylladbBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
testCases := []struct {
desc string
mockCall func()
expSession session
}{
{"Successful connection", func() {
mockClusterConfig.EXPECT().createSession().Return(&scyllaSession{}, nil).Times(1)
mockMetrics.EXPECT().NewHistogram("app_scylla_stats", "Response time of ScyllaDB queries in microseconds",
scylladbBuckets).Times(1)
mockLogger.EXPECT().Debugf("connecting to ScyllaDB at %v on port %v to keyspace %v", "host1", 9042, "my_keyspace")
mockLogger.EXPECT().Logf("connected to '%s' keyspace at host '%s' and port '%d'", "my_keyspace", "host1", 9042)
}, &scyllaSession{}},
{
"Connection failed", func() {
mockLogger.EXPECT().Debugf("connecting to ScyllaDB at %v on port %v to keyspace %v", "host1", 9042, "my_keyspace")
mockClusterConfig.EXPECT().createSession().Return(nil, errConnFail).Times(1)
mockLogger.EXPECT().Error("failed to connect to ScyllaDB:")
}, nil,
},
}
for i, tc := range testCases {
tc.mockCall()
client := New(config)
client.UseLogger(mockLogger)
client.UseMetrics(mockMetrics)
client.scylla.clusterConfig = mockClusterConfig
client.Connect()
assert.Equal(t, tc.expSession, client.scylla.session, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_Query(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
const query = "SELECT id, name FROM Users"
type Users struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStructSlice := make([]Users, 0)
mockIntSlice := make([]int, 0)
mockStruct := Users{}
mockInt := 0
client, mockDeps := initTest(t)
testCases := []struct {
desc string
dest any
mockCall func()
expRes any
expErr error
}{
{"success case: struct slice", &mockStructSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().Iter().Return(mockDeps.mockIter).AnyTimes()
mockDeps.mockIter.EXPECT().NumRows().Return(1).AnyTimes()
mockDeps.mockIter.EXPECT().Columns().Return([]gocql.ColumnInfo{{Name: "id"}, {Name: "name"}}).AnyTimes()
mockDeps.mockIter.EXPECT().Scan(gomock.Any()).Times(1)
}, &mockStructSlice, nil},
{"success case: int slice", &mockIntSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().Iter().Return(mockDeps.mockIter).AnyTimes()
mockDeps.mockIter.EXPECT().NumRows().Return(1).AnyTimes()
mockDeps.mockIter.EXPECT().Scan(gomock.Any()).AnyTimes()
}, &mockIntSlice, nil},
{"success case: struct", &mockStruct, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().Iter().Return(mockDeps.mockIter).AnyTimes()
mockDeps.mockIter.EXPECT().Columns().Return([]gocql.ColumnInfo{{Name: "id"}, {Name: "name"}}).AnyTimes()
mockDeps.mockIter.EXPECT().Scan(gomock.Any()).AnyTimes()
}, &mockStruct, nil},
{"failure case: dest is not pointer", mockStructSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
}, mockStructSlice,
nil},
{"failure case: dest is int", &mockInt, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().Iter().Return(mockDeps.mockIter).AnyTimes()
}, &mockInt, nil},
{"failure case: dest is int", &mockInt, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().Iter().Return(mockDeps.mockIter).Times(1)
}, &mockInt, nil},
}
for i, tc := range testCases {
tc.mockCall()
err := client.Query(&mockStructSlice, query)
assert.Equalf(t, tc.expRes, tc.dest, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_Exec(t *testing.T) {
const query = "INSERT INTO Users (id, name) VALUES(1, 'Test')"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"success case", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().Exec().Return(nil).Times(1)
}, nil},
{"failure case", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().Exec().Return(errMock).Times(1)
}, errMock},
}
for i, tc := range testCases {
tc.mockCall()
err := client.Exec(query)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_NewBatch(t *testing.T) {
const batchName = "testBatch"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
batchType int
mockCall func()
expErr error
}{
{"valid log type", LoggedBatch, func() {
mockDeps.mockSession.EXPECT().newBatch(gocql.BatchType(LoggedBatch)).Return(&scyllaBatch{}).Times(1)
}, nil},
{"valid log type, empty batches", LoggedBatch, func() {
client.scylla.batches = nil
mockDeps.mockSession.EXPECT().newBatch(gocql.BatchType(LoggedBatch)).Return(&scyllaBatch{}).Times(1)
}, nil},
{"invalid log type", -1, func() {}, errUnsupportedBatchType},
}
for i, tc := range testCases {
tc.mockCall()
err := client.NewBatch(batchName, tc.batchType)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
if tc.expErr != nil {
_, ok := client.scylla.batches[batchName]
assert.Truef(t, ok, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
}
func Test_HealthCheck(t *testing.T) {
const query = "SELECT now() FROM system.local"
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expHealth *Health
err error
}{
{"success case", func() {
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().Exec().Return(nil).Times(1)
}, &Health{
Status: "UP",
Details: map[string]any{"host": client.config.Host, "keyspace": client.config.Keyspace},
}, nil},
{"failure case: exec error", func() {
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().Exec().Return(errMock).Times(1)
}, &Health{
Status: "DOWN",
Details: map[string]any{"host": client.config.Host, "keyspace": client.config.Keyspace,
"message": errMock.Error()},
}, errStatusDown},
{"failure case: ScyllaDB not initialized", func() {
client.scylla.session = nil
mockDeps.mockSession.EXPECT().Query(query).Return(mockDeps.mockQuery).Times(1)
mockDeps.mockQuery.EXPECT().Exec().Return(nil).Times(1)
}, &Health{
Status: "DOWN",
Details: map[string]any{"host": client.config.Host, "keyspace": client.config.Keyspace,
"message": "ScyllaDB not connected"},
}, errStatusDown},
}
for i, tc := range testCases {
tc.mockCall()
health, err := client.HealthCheck(context.Background())
assert.Equal(t, tc.err, err)
assert.Equalf(t, tc.expHealth, health, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_BatchQuery(t *testing.T) {
client, mockDeps := initTest(t)
const stmt = "INSERT INTO users (id, name) VALUES(?, ?)"
values := []any{1, "Test"}
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"batch is initialized", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockBatch.EXPECT().Query(stmt, values...)
}, nil},
{"batch is not initialized", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
client.scylla.batches = nil
}, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
err := client.BatchQuery(mockBatchName, stmt, values...)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_ExecuteBatchCAS(t *testing.T) {
client, mockDeps := initTest(t)
type testStruct struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStructSlice := make([]testStruct, 0)
testCases := []struct {
desc string
dest any
mockCall func()
expRes any
expErr error
}{
{"success case: struct slice", &mockStructSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatchCAS(mockDeps.mockBatch, gomock.Any()).Return(true, nil).Times(1)
}, &mockStructSlice, nil},
{"failure case: executeBatchCAS returns error", &mockStructSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatchCAS(mockDeps.mockBatch, gomock.Any()).Return(false, assert.AnError).Times(1)
}, &mockStructSlice, assert.AnError},
{"failure case: batch not initialized", &mockStructSlice, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
client.scylla.batches = nil
}, &mockStructSlice, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
applied, err := client.ExecuteBatchCAS(mockBatchName, tc.dest)
assert.Equalf(t, tc.expRes, tc.dest, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, applied, tc.expErr == nil, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_ExecCAS(t *testing.T) {
const query = "INSERT INTO users (id, name) VALUES(1, 'Test') IF NOT EXISTS"
type users struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStruct := users{}
mockInt := 0
client, mockDeps := initTest(t)
testCases := []struct {
desc string
dest any
mockCall func()
expApplied bool
expErr error
}{
{"success case: struct dest, applied true", &mockStruct, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{})).AnyTimes()
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().MapScanCAS(gomock.AssignableToTypeOf(map[string]any{})).Return(true, nil).AnyTimes()
}, true, nil},
{"success case: int dest, applied true", &mockInt, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{})).AnyTimes()
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().ScanCAS(gomock.Any()).Return(true, nil).AnyTimes()
}, true, nil},
{"failure case: struct dest, error", &mockStruct, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().MapScanCAS(gomock.AssignableToTypeOf(map[string]any{})).Return(false, errMock).AnyTimes()
}, true, nil},
{"failure case: int dest, error", &mockInt, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
mockDeps.mockQuery.EXPECT().ScanCAS(gomock.Any()).Return(false, errMock).AnyTimes()
}, true, nil},
{"failure case: dest is not pointer", mockInt, func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
}, false, errDestinationIsNotPointer},
{"failure case: dest is slice", &[]int{}, func() {
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
}, false, errUnexpectedSlice{target: "[]*[]int"}},
{"failure case: dest is map", &map[string]any{}, func() {
mockDeps.mockSession.EXPECT().Query(query, nil).Return(mockDeps.mockQuery).AnyTimes()
}, false, errUnexpectedMap},
}
for i, tc := range testCases {
tc.mockCall()
applied, err := client.ExecCAS(tc.dest, query)
assert.Equalf(t, tc.expApplied, applied, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestClient_ExecuteBatchCASWithCtx(t *testing.T) {
client, mockDeps := initTest(t)
type testStruct struct {
ID int `json:"id"`
Name string `json:"name"`
}
mockStructSlice := make([]testStruct, 0)
testCases := []struct {
desc string
batchName string
dest any
mockCall func()
expRes any
expErr error
}{
{
desc: "success case: batch found and executed",
batchName: "test-batch",
dest: &mockStructSlice,
mockCall: func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(nil).Times(1)
},
expRes: &mockStructSlice,
expErr: errBatchNotInitialized,
},
{
desc: "failure case: executeBatch returns error",
batchName: "test-batch",
dest: &mockStructSlice,
mockCall: func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(assert.AnError).Times(1)
},
expRes: &mockStructSlice,
expErr: errBatchNotInitialized,
},
{
desc: "failure case: batch not initialized",
batchName: "non-existent-batch",
dest: &mockStructSlice,
mockCall: func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
client.scylla.batches = nil
},
expRes: &mockStructSlice,
expErr: errBatchNotInitialized,
},
}
for i, tc := range testCases {
tc.mockCall()
ctx := context.Background()
err := client.ExecuteBatchWithCtx(ctx, tc.batchName)
assert.Equalf(t, tc.expRes, tc.dest, "TEST[%d], Failed: %s", i, tc.desc)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed: %s", i, tc.desc)
}
}
func Test_ExecuteBatch(t *testing.T) {
client, mockDeps := initTest(t)
testCases := []struct {
desc string
mockCall func()
expErr error
}{
{"execute batch success", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(nil).Times(1)
}, nil},
{"execute batch failure", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
mockDeps.mockSession.EXPECT().executeBatch(mockDeps.mockBatch).Return(errMock).Times(1)
}, errMock},
{"batch not initialized", func() {
mockDeps.mockLogger.EXPECT().Debug(gomock.AssignableToTypeOf(&QueryLog{}))
client.scylla.batches = nil
}, errBatchNotInitialized},
}
for i, tc := range testCases {
tc.mockCall()
err := client.ExecuteBatch(mockBatchName)
assert.Equalf(t, tc.expErr, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/datasource/solr/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/solr
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/solr/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0 h1:c9r/G1CSw4dPI1jaNNG9RnQP+q4SvZnHciDQJVIvchU=
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.67.0/go.mod h1:gO9smoZe9KnZcJCqcB0lMmQ4Z5VEifYmjMTpnwtTSuQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/solr/logger.go
================================================
package solr
import (
"fmt"
"io"
"regexp"
"strings"
)
type Logger interface {
Debug(args ...any)
Debugf(pattern string, args ...any)
Info(args ...any)
Infof(pattern string, args ...any)
Error(args ...any)
Errorf(pattern string, args ...any)
}
type QueryLog struct {
Type string `json:"type"`
URL string `json:"Url"`
Duration int64 `json:"duration"`
}
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n",
clean(ql.URL), "SOLR", ql.Duration, clean(ql.Type))
}
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
================================================
FILE: pkg/gofr/datasource/solr/metrics.go
================================================
package solr
import "context"
type Metrics interface {
NewHistogram(name, desc string, buckets ...float64)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/solr/mock_logger.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: logger.go
//
// Generated by this command:
//
// mockgen -source=logger.go -destination=mock_logger.go -package=solr
//
// Package solr is a generated GoMock package.
package solr
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Error mocks base method.
func (m *MockLogger) Error(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Error", varargs...)
}
// Error indicates an expected call of Error.
func (mr *MockLoggerMockRecorder) Error(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockLogger)(nil).Error), args...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Info mocks base method.
func (m *MockLogger) Info(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Info", varargs...)
}
// Info indicates an expected call of Info.
func (mr *MockLoggerMockRecorder) Info(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockLogger)(nil).Info), args...)
}
// Infof mocks base method.
func (m *MockLogger) Infof(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Infof", varargs...)
}
// Infof indicates an expected call of Infof.
func (mr *MockLoggerMockRecorder) Infof(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Infof", reflect.TypeOf((*MockLogger)(nil).Infof), varargs...)
}
================================================
FILE: pkg/gofr/datasource/solr/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=solr
//
// Package solr is a generated GoMock package.
package solr
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
================================================
FILE: pkg/gofr/datasource/solr/solr.go
================================================
package solr
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type Config struct {
Host string
Port string
}
type Client struct {
url string
logger Logger
metrics Metrics
tracer trace.Tracer
client *http.Client
}
// New initializes Solr driver with the provided configuration.
// The Connect method must be called to establish a connection to Solr.
// Usage:
// client := New(config)
// client.UseLogger(loggerInstance)
// client.UseMetrics(metricsInstance)
// client.Connect().
func New(conf Config) *Client {
s := &Client{}
s.url = "http://" + conf.Host + ":" + conf.Port + "/solr"
s.client = &http.Client{}
return s
}
// UseLogger sets the logger for the Solr client which asserts the Logger interface.
func (c *Client) UseLogger(logger any) {
if l, ok := logger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets the metrics for the Solr client which asserts the Metrics interface.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets the tracer for Solr client.
func (c *Client) UseTracer(tracer any) {
if tracer, ok := tracer.(trace.Tracer); ok {
c.tracer = tracer
}
}
// Connect establishes a connection to Solr and registers metrics using the provided configuration when the client was Created.
func (c *Client) Connect() {
c.logger.Debugf("connecting to Solr at %v", c.url)
solrBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_solr_stats", "Response time of Solr operations in milliseconds.", solrBuckets...)
_, err := c.HealthCheck(context.Background())
if err != nil {
c.logger.Errorf("error while connecting to Solr: %v", err)
return
}
c.logger.Infof("connected to Solr at %v", c.url)
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
url := c.url + "/admin/info/system?wt=json"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodGet, url, nil, nil)
defer c.sendOperationStats(ctx, &QueryLog{Type: "HealthCheck", URL: url}, startTime, "healthcheck", span)
return resp, err
}
// Search searches documents in the given collections based on the parameters specified.
// This can be used for making any queries to Solr.
func (c *Client) Search(ctx context.Context, collection string, params map[string]any) (any, error) {
url := c.url + "/" + collection + "/select"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodGet, url, params, nil)
c.sendOperationStats(ctx, &QueryLog{Type: "Search", URL: url}, startTime, "search", span)
return resp, err
}
// Create makes documents in the specified collection. params can be used to send parameters like commit=true.
func (c *Client) Create(ctx context.Context, collection string, document *bytes.Buffer,
params map[string]any) (any, error) {
url := c.url + "/" + collection + "/update"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, params, document)
c.sendOperationStats(ctx, &QueryLog{Type: "Create", URL: url}, startTime, "create", span)
return resp, err
}
// Update updates documents in the specified collection. params can be used to send parameters like commit=true.
func (c *Client) Update(ctx context.Context, collection string, document *bytes.Buffer,
params map[string]any) (any, error) {
url := c.url + "/" + collection + "/update"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, params, document)
c.sendOperationStats(ctx, &QueryLog{Type: "Update", URL: url}, startTime, "update", span)
return resp, err
}
// Delete deletes documents in the specified collection. params can be used to send parameters like commit=true.
func (c *Client) Delete(ctx context.Context, collection string, document *bytes.Buffer,
params map[string]any) (any, error) {
url := c.url + "/" + collection + "/update"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, params, document)
c.sendOperationStats(ctx, &QueryLog{Type: "Delete", URL: url}, startTime, "delete", span)
return resp, err
}
// ListFields retrieves all the fields in the schema for the specified collection.
// params can be used to send query parameters like wt, fl, includeDynamic etc.
func (c *Client) ListFields(ctx context.Context, collection string, params map[string]any) (any, error) {
url := c.url + "/" + collection + "/schema/fields"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodGet, url, params, nil)
c.sendOperationStats(ctx, &QueryLog{Type: "ListFields", URL: url}, startTime, "list-fields", span)
return resp, err
}
// Retrieve retrieves the entire schema that includes all the fields,field types,dynamic rules and copy field rules.
// params can be used to specify the format of response.
func (c *Client) Retrieve(ctx context.Context, collection string, params map[string]any) (any, error) {
url := c.url + "/" + collection + "/schema"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodGet, url, params, nil)
c.sendOperationStats(ctx, &QueryLog{Type: "Retrieve", URL: url}, startTime, "retrieve", span)
return resp, err
}
// AddField adds Field in the schema for the specified collection.
func (c *Client) AddField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
url := c.url + "/" + collection + "/schema"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, nil, document)
c.sendOperationStats(ctx, &QueryLog{Type: "AddField", URL: url}, startTime, "add-field", span)
return resp, err
}
// UpdateField updates the field definitions in the schema for the specified collection.
func (c *Client) UpdateField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
url := c.url + "/" + collection + "/schema"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, nil, document)
c.sendOperationStats(ctx, &QueryLog{Type: "UpdateField", URL: url}, startTime, "update-field", span)
return resp, err
}
// DeleteField deletes the field definitions in the schema for the specified collection.
func (c *Client) DeleteField(ctx context.Context, collection string, document *bytes.Buffer) (any, error) {
url := c.url + "/" + collection + "/schema"
startTime := time.Now()
resp, span, err := c.call(ctx, http.MethodPost, url, nil, document)
c.sendOperationStats(ctx, &QueryLog{Type: "DeleteField", URL: url}, startTime, "delete-field", span)
return resp, err
}
// Response stores the response from Solr.
type Response struct {
Code int
Data any
}
// call forms the http request and makes a call to solr and populates the solr response.
func (c *Client) call(ctx context.Context, method, url string, params map[string]any, body io.Reader) (any, trace.Span, error) {
var span trace.Span
if c.tracer != nil {
ctx, span = c.tracer.Start(ctx, fmt.Sprintf("Solr %s", method),
trace.WithAttributes(
attribute.String("solr.url", url),
),
)
}
ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
req, err := c.createRequest(ctx, method, url, params, body)
if err != nil {
return nil, nil, err
}
resp, err := c.client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
var respBody any
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, nil, err
}
err = json.Unmarshal(b, &respBody)
if err != nil {
return nil, nil, err
}
if span != nil {
span.SetAttributes(
attribute.Int("http.status_code", resp.StatusCode),
)
}
return Response{resp.StatusCode, respBody}, span, nil
}
func (*Client) createRequest(ctx context.Context, method, url string, params map[string]any, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, method, url, body)
if err != nil {
return nil, err
}
if method != http.MethodGet {
req.Header.Add("Content-Type", "application/json")
}
q := req.URL.Query()
for k, val := range params {
switch v := val.(type) {
case []string:
for _, val := range v {
q.Add(k, val)
}
default:
q.Add(k, fmt.Sprint(val))
}
}
req.URL.RawQuery = q.Encode()
return req, nil
}
func (c *Client) sendOperationStats(ctx context.Context, ql *QueryLog, startTime time.Time, method string, span trace.Span) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
c.metrics.RecordHistogram(ctx, "app_solr_stats", float64(duration),
"type", ql.Type)
if span != nil {
defer span.End()
span.SetAttributes(
attribute.String("solr.type", ql.Type),
attribute.Int64(fmt.Sprintf("solr.%v.duration", method), duration))
}
}
================================================
FILE: pkg/gofr/datasource/solr/solr_test.go
================================================
package solr
import (
"bytes"
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func Test_InvalidRequest(t *testing.T) {
client := New(Config{})
_, _, err := client.call(context.Background(), "GET", ":/localhost:", nil, nil)
require.Error(t, err, "TEST Failed.\n")
}
func Test_InvalidJSONBody(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`Not a JSON`))
}))
defer ts.Close()
client := New(Config{})
_, _, err := client.call(context.Background(), "GET", ts.URL, nil, nil)
require.Error(t, err, "TEST Failed.\n")
}
func Test_ErrorResponse(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "some error", http.StatusLocked)
}))
ts.Close()
client := New(Config{})
_, _, err := client.call(context.Background(), "GET", ts.URL, nil, nil)
require.Error(t, err, "TEST Failed.\n")
}
func setupClient(t *testing.T) *Client {
t.Helper()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{
"responseHeader": {
"rf": 1,
"status": 0}}`))
}))
t.Cleanup(func() {
ts.Close()
})
a := ts.Listener.Addr().String()
addr := strings.Split(a, ":")
ctrl := gomock.NewController(t)
mockLogger := NewMockLogger(ctrl)
mockMetrics := NewMockMetrics(ctrl)
mockLogger.EXPECT().Debug(gomock.Any())
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_solr_stats", gomock.Any(), "type", gomock.Any())
s := New(Config{Host: addr[0], Port: addr[1]})
s.metrics = mockMetrics
s.logger = mockLogger
return s
}
func Test_ClientSearch(t *testing.T) {
s := setupClient(t)
resp, err := s.Search(context.Background(), "test", map[string]any{"id": []string{"1234"}})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientCreate(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{
"id": "1234567",
"cat": [
"Book"
],
"genere_s": "Hello There"}`)
resp, err := s.Create(context.Background(), "test", body, map[string]any{"commit": "true"})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientUpdate(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{
"id": "1234567",
"cat": [
"Book"
]}`)
resp, err := s.Update(context.Background(), "test", body, map[string]any{"commit": "true"})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientDelete(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{"delete":[
"1234",
"12345"
]}`)
resp, err := s.Delete(context.Background(), "test", body, map[string]any{"commit": "true"})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientRetrieve(t *testing.T) {
s := setupClient(t)
resp, err := s.Retrieve(context.Background(), "test", map[string]any{"wt": "xml"})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientListFields(t *testing.T) {
s := setupClient(t)
resp, err := s.ListFields(context.Background(), "test", map[string]any{"includeDynamic": true})
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientAddField(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{"add-field":{
"name":"merchant",
"type":"string",
"stored":true }}`)
resp, err := s.AddField(context.Background(), "test", body)
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientUpdateField(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{"replace-field":{
"name":"merchant",
"type":"text_general"}}`)
resp, err := s.UpdateField(context.Background(), "test", body)
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
func Test_ClientDeleteField(t *testing.T) {
s := setupClient(t)
body := bytes.NewBufferString(`{"delete-field":{
"name":"merchant",
"type":"text_general"}}`)
resp, err := s.DeleteField(context.Background(), "test", body)
require.NoError(t, err, "TEST Failed.\n")
require.NotNil(t, resp, "TEST Failed.\n")
}
================================================
FILE: pkg/gofr/datasource/sql/bind.go
================================================
package sql
import (
"fmt"
)
const (
dialectMysql = "mysql"
dialectPostgres = "postgres"
quoteBack = "`"
quoteDouble = `"`
)
// BindVarType represents different type of bindvars in SQL queries.
type BindVarType uint
const (
UNKNOWN BindVarType = iota + 1
QUESTION
DOLLAR
)
func bindType(dialect string) BindVarType {
switch dialect {
case dialectMysql:
return QUESTION
case dialectPostgres:
return DOLLAR
default:
return UNKNOWN
}
}
func bindVar(dialect string, position int) string {
if DOLLAR == bindType(dialect) {
return fmt.Sprintf("$%v", position)
}
return "?"
}
func quote(dialect string) string {
if dialectPostgres == dialect {
return quoteDouble
}
return quoteBack
}
func quotedString(q, s string) string {
return fmt.Sprintf("%s%s%s", q, s, q)
}
================================================
FILE: pkg/gofr/datasource/sql/bind_test.go
================================================
package sql
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_BindType(t *testing.T) {
tests := []struct {
dialect string
expected BindVarType
}{
{
dialect: "mysql",
expected: QUESTION,
},
{
dialect: "postgres",
expected: DOLLAR,
},
{
dialect: "any-other-dialect",
expected: UNKNOWN,
},
}
for i, tc := range tests {
t.Run(tc.dialect+" bind type", func(t *testing.T) {
actual := bindType(tc.dialect)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.dialect+" bind type")
})
}
}
func Test_BindVar(t *testing.T) {
tests := []struct {
name string
dialect string
position int
expected string
}{
{
name: "Postgres bind var",
dialect: dialectPostgres,
position: 1,
expected: "$1",
},
{
name: "MySQL bind var",
dialect: dialectMysql,
position: 1,
expected: "?",
},
{
name: "Unknown dialect bind var",
dialect: "unknown",
position: 1,
expected: "?",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := bindVar(tc.dialect, tc.position)
assert.Equal(t, tc.expected, actual)
})
}
}
func Test_Quote(t *testing.T) {
tests := []struct {
name string
dialect string
expected string
}{
{
name: "Postgres quote",
dialect: dialectPostgres,
expected: quoteDouble,
},
{
name: "MySQL quote",
dialect: dialectMysql,
expected: quoteBack,
},
{
name: "Unknown dialect quote",
dialect: "unknown",
expected: quoteBack,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := quote(tc.dialect)
assert.Equal(t, tc.expected, actual)
})
}
}
func Test_QuotedString(t *testing.T) {
tests := []struct {
name string
q string
s string
expected string
}{
{
name: "Double quote",
q: quoteDouble,
s: "test",
expected: `"test"`,
},
{
name: "Back quote",
q: quoteBack,
s: "test",
expected: "`test`",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual := quotedString(tc.q, tc.s)
assert.Equal(t, tc.expected, actual)
})
}
}
================================================
FILE: pkg/gofr/datasource/sql/db.go
================================================
// Package sql provides functionalities to interact with SQL databases using the database/sql package.This package
// includes a wrapper around sql.DB and sql.Tx to provide additional features such as query logging, metrics recording,
// and error handling.
package sql
import (
"context"
"database/sql"
"fmt"
"io"
"reflect"
"regexp"
"strings"
"sync"
"time"
"gofr.dev/pkg/gofr/datasource"
)
// DB is a wrapper around sql.DB which provides some more features.
type DB struct {
// contains unexported or private fields
*sql.DB
logger datasource.Logger
config *DBConfig
metrics Metrics
stopSignal chan struct{}
closeOnce sync.Once
}
type Log struct {
Type string `json:"type"`
Query string `json:"query"`
Duration int64 `json:"duration"`
Args []any `json:"args,omitempty"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;24m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s\n",
l.Type, "SQL", l.Duration, clean(l.Query))
}
func clean(query string) string {
query = regexp.MustCompile(`\s+`).ReplaceAllString(query, " ")
query = strings.TrimSpace(query)
return query
}
func sendStats(logger datasource.Logger, metrics Metrics, config *DBConfig, start time.Time, queryType, query string, args ...any) {
duration := time.Since(start).Milliseconds()
if logger != nil {
logger.Debug(&Log{
Type: queryType,
Query: query,
Duration: duration,
Args: args,
})
}
// This contains the fix for the nil pointer dereference
if metrics != nil {
metrics.RecordHistogram(context.Background(), "app_sql_stats", float64(duration), "hostname", config.HostName,
"database", config.Database, "type", getOperationType(query))
}
}
func (d *DB) sendOperationStats(start time.Time, queryType, query string, args ...any) {
sendStats(d.logger, d.metrics, d.config, start, queryType, query, args...)
}
func getOperationType(query string) string {
query = strings.TrimSpace(query)
words := strings.Split(query, " ")
return strings.ToUpper(words[0])
}
func (d *DB) Query(query string, args ...any) (*sql.Rows, error) {
defer d.sendOperationStats(time.Now(), "Query", query, args...)
return d.DB.QueryContext(context.Background(), query, args...)
}
func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
defer d.sendOperationStats(time.Now(), "QueryContext", query, args...)
return d.DB.QueryContext(ctx, query, args...)
}
func (d *DB) Dialect() string {
return d.config.Dialect
}
func (d *DB) QueryRow(query string, args ...any) *sql.Row {
defer d.sendOperationStats(time.Now(), "QueryRow", query, args...)
return d.DB.QueryRowContext(context.Background(), query, args...)
}
func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
defer d.sendOperationStats(time.Now(), "QueryRowContext", query, args...)
return d.DB.QueryRowContext(ctx, query, args...)
}
func (d *DB) Exec(query string, args ...any) (sql.Result, error) {
defer d.sendOperationStats(time.Now(), "Exec", query, args...)
return d.DB.ExecContext(context.Background(), query, args...)
}
func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
defer d.sendOperationStats(time.Now(), "ExecContext", query, args...)
return d.DB.ExecContext(ctx, query, args...)
}
func (d *DB) Prepare(query string) (*sql.Stmt, error) {
defer d.sendOperationStats(time.Now(), "Prepare", query)
return d.DB.PrepareContext(context.Background(), query)
}
func (d *DB) Begin() (*Tx, error) {
tx, err := d.DB.BeginTx(context.Background(), nil)
if err != nil {
return nil, err
}
return &Tx{Tx: tx, config: d.config, logger: d.logger, metrics: d.metrics}, nil
}
func (d *DB) Close() error {
d.closeOnce.Do(func() {
close(d.stopSignal)
})
if d.DB != nil {
return d.DB.Close()
}
return nil
}
type Tx struct {
*sql.Tx
config *DBConfig
logger datasource.Logger
metrics Metrics
}
func (t *Tx) sendOperationStats(start time.Time, queryType, query string, args ...any) {
sendStats(t.logger, t.metrics, t.config, start, queryType, query, args...)
}
func (t *Tx) Query(query string, args ...any) (*sql.Rows, error) {
defer t.sendOperationStats(time.Now(), "TxQuery", query, args...)
return t.Tx.QueryContext(context.Background(), query, args...)
}
func (t *Tx) QueryRow(query string, args ...any) *sql.Row {
defer t.sendOperationStats(time.Now(), "TxQueryRow", query, args...)
return t.Tx.QueryRowContext(context.Background(), query, args...)
}
func (t *Tx) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
defer t.sendOperationStats(time.Now(), "TxQueryRowContext", query, args...)
return t.Tx.QueryRowContext(ctx, query, args...)
}
func (t *Tx) Exec(query string, args ...any) (sql.Result, error) {
defer t.sendOperationStats(time.Now(), "TxExec", query, args...)
return t.Tx.ExecContext(context.Background(), query, args...)
}
func (t *Tx) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
defer t.sendOperationStats(time.Now(), "TxExecContext", query, args...)
return t.Tx.ExecContext(ctx, query, args...)
}
func (t *Tx) Prepare(query string) (*sql.Stmt, error) {
defer t.sendOperationStats(time.Now(), "TxPrepare", query)
return t.Tx.PrepareContext(context.Background(), query)
}
func (t *Tx) Commit() error {
defer t.sendOperationStats(time.Now(), "TxCommit", "COMMIT")
return t.Tx.Commit()
}
func (t *Tx) Rollback() error {
defer t.sendOperationStats(time.Now(), "TxRollback", "ROLLBACK")
return t.Tx.Rollback()
}
// Select runs a query with args and binds the result of the query to the data.
// data should be a point to a slice, struct or any type. Slice will return multiple
// objects whereas struct will return a single object.
//
// Example Usages:
//
// 1. Get multiple rows with only one column
// ids := make([]int, 0)
// db.Select(ctx, &ids, "select id from users")
//
// 2. Get a single object from database
// type user struct {
// Name string
// ID int
// Image string
// }
// u := user{}
// db.Select(ctx, &u, "select * from users where id=?", 1)
//
// 3. Get array of objects from multiple rows
// type user struct {
// Name string
// ID int
// Image string `db:"image_url"`
// }
// users := []user{}
// db.Select(ctx, &users, "select * from users")
//
//nolint:exhaustive // We just want to take care of slice and struct in this case.
func (d *DB) Select(ctx context.Context, data any, query string, args ...any) {
// If context is done, it is not needed
if ctx.Err() != nil {
return
}
// First confirm that what we got in v is a pointer else it won't be settable
rvo := reflect.ValueOf(data)
if rvo.Kind() != reflect.Ptr {
d.logger.Error("we did not get a pointer. data is not settable.")
return
}
// Deference the pointer to the underlying element, if the underlying element is a slice, multiple rows are expected.
// If the underlying element is a struct, one row is expected.
rv := rvo.Elem()
switch rv.Kind() {
case reflect.Slice:
d.selectSlice(ctx, query, args, rvo, rv)
case reflect.Struct:
d.selectStruct(ctx, query, args, rv)
default:
d.logger.Debugf("a pointer to %v was not expected.", rv.Kind().String())
}
}
func (d *DB) selectSlice(ctx context.Context, query string, args []any, rvo, rv reflect.Value) {
rows, err := d.QueryContext(ctx, query, args...)
if err != nil {
d.logger.Errorf("error running query: %v", err)
return
}
for rows.Next() {
val := reflect.New(rv.Type().Elem())
if rv.Type().Elem().Kind() == reflect.Struct {
d.rowsToStruct(rows, val)
} else {
_ = rows.Scan(val.Interface())
}
rv = reflect.Append(rv, val.Elem())
}
if rows.Err() != nil {
d.logger.Errorf("error parsing rows : %v", err)
return
}
if rvo.Elem().CanSet() {
rvo.Elem().Set(rv)
}
}
func (d *DB) selectStruct(ctx context.Context, query string, args []any, rv reflect.Value) {
rows, err := d.QueryContext(ctx, query, args...)
if err != nil {
d.logger.Errorf("error running query: %v", err)
return
}
for rows.Next() {
d.rowsToStruct(rows, rv)
}
if rows.Err() != nil {
d.logger.Errorf("error parsing rows : %v", err)
return
}
}
func (*DB) rowsToStruct(rows *sql.Rows, vo reflect.Value) {
v := vo
if vo.Kind() == reflect.Ptr {
v = vo.Elem()
}
// Map fields and their indexes by normalized name
fieldNameIndex := map[string]int{}
for i := 0; i < v.Type().NumField(); i++ {
var name string
f := v.Type().Field(i)
tag := f.Tag.Get("db")
if tag != "" {
name = tag
} else {
name = ToSnakeCase(f.Name)
}
fieldNameIndex[name] = i
}
fields := []any{}
columns, _ := rows.Columns()
for _, c := range columns {
if i, ok := fieldNameIndex[c]; ok {
fields = append(fields, v.Field(i).Addr().Interface())
} else {
var i any
fields = append(fields, &i)
}
}
_ = rows.Scan(fields...)
if vo.CanSet() {
vo.Set(v)
}
}
var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
func ToSnakeCase(str string) string {
snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}")
snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}")
return strings.ToLower(snake)
}
================================================
FILE: pkg/gofr/datasource/sql/db_test.go
================================================
package sql
import (
"bytes"
"context"
"database/sql"
"sync"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
var (
errDB = testutil.CustomError{ErrorMessage: "DB error"}
errSyntax = testutil.CustomError{ErrorMessage: "syntax error"}
errTx = testutil.CustomError{ErrorMessage: "error starting transaction"}
errbegin = testutil.CustomError{ErrorMessage: "begin failed"}
)
func getDB(t *testing.T, logLevel logging.Level) (*DB, sqlmock.Sqlmock) {
t.Helper()
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual), sqlmock.MonitorPingsOption(true))
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
db := &DB{
DB: mockDB,
logger: logging.NewMockLogger(logLevel),
metrics: nil,
config: nil, // Initializing config to nil as it's set in next line
stopSignal: make(chan struct{}),
closeOnce: sync.Once{},
}
db.config = &DBConfig{}
return db, mock
}
func TestDB_SelectSingleColumnFromIntToString(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow(1).
AddRow(2)
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
ids := make([]string, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []string{"1", "2"}, ids, "TEST Failed.\n")
}
func TestDB_SelectSingleColumnFromStringToString(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow("1").
AddRow("2")
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
ids := make([]string, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []string{"1", "2"}, ids, "TEST Failed.\n")
}
func TestDB_SelectSingleColumnFromIntToInt(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow(1).
AddRow(2)
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
ids := make([]int, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []int{1, 2}, ids, "TEST Failed.\n")
}
func TestDB_SelectSingleColumnFromIntToCustomInt(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow(1).
AddRow(2)
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type CustomInt int
ids := make([]CustomInt, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []CustomInt{1, 2}, ids, "TEST Failed.\n")
}
func TestDB_SelectSingleColumnFromStringToCustomInt(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow("1").
AddRow("2")
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type CustomInt int
ids := make([]CustomInt, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []CustomInt{1, 2}, ids, "TEST Failed.\n")
}
func TestDB_SelectContextError(t *testing.T) {
ctx, cancel := context.WithDeadline(t.Context(), time.Now().Add(time.Microsecond))
time.Sleep(1 * time.Millisecond)
defer cancel()
db, _ := getDB(t, logging.DEBUG)
defer db.DB.Close()
// the query won't run, since context is past deadline and the function will simply return
db.Select(ctx, nil, "select 1")
}
func TestDB_SelectDataPointerError(t *testing.T) {
out := testutil.StderrOutputForFunc(func() {
db, _ := getDB(t, logging.INFO)
defer db.DB.Close()
db.Select(t.Context(), nil, "select 1")
})
assert.Contains(t, out, "we did not get a pointer. data is not settable.", "TEST Failed.\n")
}
func TestDB_SelectSingleColumnFromStringToCustomString(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id"}).
AddRow("1").
AddRow("2")
mock.ExpectQuery("select id from users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type CustomStr string
ids := make([]CustomStr, 0)
db.Select(t.Context(), &ids, "select id from users")
assert.Equal(t, []CustomStr{"1", "2"}, ids, "TEST Failed.\n")
}
func TestDB_SelectSingleRowMultiColumn(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id", "name", "image"}).
AddRow("1", "Vikash", "http://via.placeholder.com/150")
mock.ExpectQuery("select 1 user").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type user struct {
Name string
ID int
Image string
}
u := user{}
db.Select(t.Context(), &u, "select 1 user")
assert.Equal(t, user{
Name: "Vikash",
ID: 1,
Image: "http://via.placeholder.com/150",
}, u, "TEST Failed.\n")
}
func TestDB_SelectSingleRowMultiColumnWithTags(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id", "name", "image_url"}).
AddRow("1", "Vikash", "http://via.placeholder.com/150")
mock.ExpectQuery("select 1 user").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type user struct {
Name string
ID int
Image string `db:"image_url"`
}
u := user{}
db.Select(t.Context(), &u, "select 1 user")
assert.Equal(t, user{
Name: "Vikash",
ID: 1,
Image: "http://via.placeholder.com/150",
}, u, "TEST Failed.\n")
}
func TestDB_SelectMultiRowMultiColumnWithTags(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
rows := sqlmock.NewRows([]string{"id", "name", "image_url"}).
AddRow("1", "Vikash", "http://via.placeholder.com/150").
AddRow("2", "Gofr", "")
mock.ExpectQuery("select users").
WillReturnRows(rows)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
type user struct {
Name string
ID int
Image string `db:"image_url"`
}
users := []user{}
db.Select(t.Context(), &users, "select users")
assert.Equal(t, []user{
{
Name: "Vikash",
ID: 1,
Image: "http://via.placeholder.com/150",
},
{
Name: "Gofr",
ID: 2,
},
}, users, "TEST Failed.\n")
}
func TestDB_SelectSingleColumnError(t *testing.T) {
ids := make([]string, 0)
out := testutil.StderrOutputForFunc(func() {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
mock.ExpectQuery("select id from users").
WillReturnError(errDB)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any())
db.Select(t.Context(), &ids, "select id from users")
})
assert.Contains(t, out, "DB error", "TEST Failed.\n")
assert.Equal(t, []string{}, ids, "TEST Failed.\n")
}
func TestDB_SelectDataPointerNotExpected(t *testing.T) {
m := make(map[int]int)
out := testutil.StdoutOutputForFunc(func() {
db, _ := getDB(t, logging.DEBUG)
defer db.DB.Close()
db.Select(t.Context(), &m, "select id from users")
})
assert.Contains(t, out, "a pointer to map was not expected.", "TEST Failed.\n")
}
func TestDB_Query(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT 1").
WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow("1"))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = db.Query("SELECT 1")
require.NoError(t, err)
require.NoError(t, rows.Err())
assert.NotNil(t, rows)
})
assert.Contains(t, out, "Query SELECT 1")
}
func TestDB_QueryError(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT ").
WillReturnError(errSyntax)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = db.Query("SELECT")
if !assert.Nil(t, rows) {
require.NoError(t, rows.Err())
}
require.Error(t, err)
assert.Equal(t, errSyntax, err)
})
assert.Contains(t, out, "Query SELECT")
}
func TestDB_QueryContext(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT 1").
WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow("1"))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = db.QueryContext(t.Context(), "SELECT 1")
require.NoError(t, err)
require.NoError(t, rows.Err())
assert.NotNil(t, rows)
})
assert.Contains(t, out, "QueryContext SELECT 1")
}
func TestDB_QueryContextError(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT ").
WillReturnError(errSyntax)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = db.QueryContext(t.Context(), "SELECT")
if !assert.Nil(t, rows) {
require.NoError(t, rows.Err())
}
require.Error(t, err)
assert.Equal(t, errSyntax, err)
})
assert.Contains(t, out, "QueryContext SELECT")
}
func TestDB_QueryRow(t *testing.T) {
var (
row *sql.Row
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("jhon"))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
row = db.QueryRow("SELECT name FROM employee WHERE id = ?", 1)
assert.NotNil(t, row)
})
assert.Contains(t, out, "QueryRow SELECT name FROM employee WHERE id = ?")
}
func TestDB_QueryRowContext(t *testing.T) {
var (
row *sql.Row
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
row = db.QueryRowContext(t.Context(), "SELECT name FROM employee WHERE id = ?", 1)
assert.NotNil(t, row)
})
assert.Contains(t, out, "QueryRowContext SELECT name FROM employee WHERE id = ?")
}
func TestDB_Exec(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectExec("INSERT INTO employee VALUES(?, ?)").
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = db.Exec("INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "Exec INSERT INTO employee VALUES(?, ?)")
}
func TestDB_ExecError(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectExec("INSERT INTO employee VALUES(?, ?").
WithArgs(2, "doe").WillReturnError(errSyntax)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = db.Exec("INSERT INTO employee VALUES(?, ?", 2, "doe")
assert.Nil(t, res)
require.Error(t, err)
assert.Equal(t, errSyntax, err)
})
assert.Contains(t, out, "Exec INSERT INTO employee VALUES(?, ?")
}
func TestDB_ExecContext(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`).
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = db.ExecContext(t.Context(), "INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "ExecContext INSERT INTO employee VALUES(?, ?)")
}
func TestDB_ExecContextError(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`).
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = db.ExecContext(t.Context(), "INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "ExecContext INSERT INTO employee VALUES(?, ?)")
}
func TestDB_Prepare(t *testing.T) {
var (
stmt *sql.Stmt
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?")
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
stmt, err = db.Prepare("SELECT name FROM employee WHERE id = ?")
require.NoError(t, err)
assert.NotNil(t, stmt)
})
assert.Contains(t, out, "Prepare SELECT name FROM employee WHERE id = ?")
}
func TestDB_PrepareError(t *testing.T) {
var (
stmt *sql.Stmt
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?")
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
stmt, err = db.Prepare("SELECT name FROM employee WHERE id = ?")
require.NoError(t, err)
assert.NotNil(t, stmt)
})
assert.Contains(t, out, "Prepare SELECT name FROM employee WHERE id = ?")
}
func TestDB_Begin(t *testing.T) {
db, mock := getDB(t, logging.INFO)
mock.ExpectBegin()
tx, err := db.Begin()
assert.NotNil(t, tx)
require.NoError(t, err)
}
func TestDB_BeginError(t *testing.T) {
db, mock := getDB(t, logging.INFO)
mock.ExpectBegin().WillReturnError(errTx)
tx, err := db.Begin()
assert.Nil(t, tx)
require.Error(t, err)
assert.Equal(t, errTx, err)
}
func TestDB_Close(t *testing.T) {
db, mock := getDB(t, logging.INFO)
mock.ExpectClose()
err := db.Close()
require.NoError(t, err)
}
func getTransaction(db *DB, mock sqlmock.Sqlmock) *Tx {
mock.ExpectBegin()
tx, _ := db.Begin()
return tx
}
func TestTx_Query(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
defer db.DB.Close()
mock.ExpectQuery("SELECT 1").
WillReturnRows(sqlmock.NewRows([]string{"1"}).AddRow("1"))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = tx.Query("SELECT 1")
require.NoError(t, err)
assert.NotNil(t, rows)
require.NoError(t, rows.Err())
})
assert.Contains(t, out, "Query SELECT 1")
}
func TestTx_QueryError(t *testing.T) {
var (
rows *sql.Rows
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectQuery("SELECT ").
WillReturnError(errSyntax)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
rows, err = tx.Query("SELECT")
if !assert.Nil(t, rows) {
require.NoError(t, rows.Err())
}
require.Error(t, err)
assert.Equal(t, errSyntax, err)
})
assert.Contains(t, out, "Query SELECT")
}
func TestTx_QueryRow(t *testing.T) {
var (
row *sql.Row
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"name"}).AddRow("jhon"))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
row = tx.QueryRow("SELECT name FROM employee WHERE id = ?", 1)
assert.NotNil(t, row)
})
assert.Contains(t, out, "QueryRow SELECT name FROM employee WHERE id = ?")
}
func TestTx_QueryRowContext(t *testing.T) {
var (
row *sql.Row
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectQuery("SELECT name FROM employee WHERE id = ?").WithArgs(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
row = tx.QueryRowContext(t.Context(), "SELECT name FROM employee WHERE id = ?", 1)
assert.NotNil(t, row)
})
assert.Contains(t, out, "QueryRowContext SELECT name FROM employee WHERE id = ?")
}
func TestTx_Exec(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectExec("INSERT INTO employee VALUES(?, ?)").
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = tx.Exec("INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "TxExec INSERT INTO employee VALUES(?, ?)")
}
func TestTx_ExecError(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectExec("INSERT INTO employee VALUES(?, ?").
WithArgs(2, "doe").WillReturnError(errSyntax)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = tx.Exec("INSERT INTO employee VALUES(?, ?", 2, "doe")
assert.Nil(t, res)
require.Error(t, err)
assert.Equal(t, errSyntax, err)
})
assert.Contains(t, out, "TxExec INSERT INTO employee VALUES(?, ?")
}
func TestTx_ExecContext(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`).
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = tx.ExecContext(t.Context(), "INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "ExecContext INSERT INTO employee VALUES(?, ?)")
}
func TestTx_ExecContextError(t *testing.T) {
var (
res sql.Result
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectExec(`INSERT INTO employee VALUES(?, ?)`).
WithArgs(2, "doe").WillReturnResult(sqlmock.NewResult(1, 1))
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "INSERT")
res, err = tx.ExecContext(t.Context(), "INSERT INTO employee VALUES(?, ?)", 2, "doe")
require.NoError(t, err)
assert.NotNil(t, res)
})
assert.Contains(t, out, "ExecContext INSERT INTO employee VALUES(?, ?)")
}
func TestTx_Prepare(t *testing.T) {
var (
stmt *sql.Stmt
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?")
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
stmt, err = tx.Prepare("SELECT name FROM employee WHERE id = ?")
require.NoError(t, err)
assert.NotNil(t, stmt)
})
assert.Contains(t, out, "Prepare SELECT name FROM employee WHERE id = ?")
}
func TestTx_PrepareError(t *testing.T) {
var (
stmt *sql.Stmt
err error
)
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mock.ExpectPrepare("SELECT name FROM employee WHERE id = ?")
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "SELECT")
stmt, err = tx.Prepare("SELECT name FROM employee WHERE id = ?")
require.NoError(t, err)
assert.NotNil(t, stmt)
})
assert.Contains(t, out, "Prepare SELECT name FROM employee WHERE id = ?")
}
func TestTx_Commit(t *testing.T) {
var err error
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
defer db.DB.Close()
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "COMMIT")
mock.ExpectCommit()
err = tx.Commit()
require.NoError(t, err)
})
assert.Contains(t, out, "TxCommit COMMIT")
}
func TestTx_CommitError(t *testing.T) {
var err error
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
defer db.DB.Close()
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "COMMIT")
mock.ExpectCommit().WillReturnError(errDB)
err = tx.Commit()
require.Error(t, err)
assert.Equal(t, errDB, err)
})
assert.Contains(t, out, "TxCommit COMMIT")
}
func TestTx_RollBack(t *testing.T) {
var err error
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
defer db.DB.Close()
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "ROLLBACK")
mock.ExpectRollback()
err = tx.Rollback()
require.NoError(t, err)
})
assert.Contains(t, out, "TxRollback ROLLBACK")
}
func TestTx_RollbackError(t *testing.T) {
var err error
out := testutil.StdoutOutputForFunc(func() {
db, mock := getDB(t, logging.DEBUG)
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
defer db.DB.Close()
db.metrics = mockMetrics
tx := getTransaction(db, mock)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats",
gomock.Any(), "hostname", gomock.Any(), "database", gomock.Any(), "type", "ROLLBACK")
mock.ExpectRollback().WillReturnError(errDB)
err = tx.Rollback()
require.Error(t, err)
assert.Equal(t, errDB, err)
})
assert.Contains(t, out, "TxRollback ROLLBACK")
}
func TestPrettyPrint(t *testing.T) {
b := make([]byte, 0)
w := bytes.NewBuffer(b)
l := &Log{
Type: "Query",
Query: "SELECT 2 + 2",
Duration: 12912,
}
l.PrettyPrint(w)
assert.Equal(t,
"\u001B[38;5;8mQuery "+
"\u001B[38;5;24mSQL \u001B[0m 12912\u001B[38;5;8mµs\u001B[0m SELECT 2 + 2\n",
w.String())
}
func TestClean(t *testing.T) {
query := ""
out := clean(query)
assert.Empty(t, out)
}
func TestDB_CloseWhenNil(t *testing.T) {
db := &DB{
stopSignal: make(chan struct{}),
}
err := db.Close()
assert.NoError(t, err)
}
func TestDB_BeginTx(t *testing.T) {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
db.metrics = NewMockMetrics(ctrl)
mock.ExpectBegin()
tx, err := db.BeginTx(t.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
require.NoError(t, err)
assert.NotNil(t, tx)
}
func TestDB_PingSuccess(t *testing.T) {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
ctrl := gomock.NewController(t)
db.metrics = NewMockMetrics(ctrl)
mock.ExpectPing()
mock.ExpectPing().WillReturnError(nil)
err := db.PingContext(t.Context())
assert.NoError(t, err)
}
func TestDB_PingFailure(t *testing.T) {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
mock.ExpectPing().WillReturnError(sql.ErrConnDone)
err := db.PingContext(t.Context())
assert.Equal(t, sql.ErrConnDone, err)
}
func TestGetOperationType_EdgeCases(t *testing.T) {
require.Empty(t, getOperationType(""))
require.Empty(t, getOperationType(" "))
require.Equal(t, "SELECT", getOperationType(" SELECT * FROM users"))
}
func TestClean_EmptyString(t *testing.T) {
require.Empty(t, clean(""))
require.Equal(t, "SELECT", clean(" SELECT "))
}
func TestDB_Dialect(t *testing.T) {
db, _ := getDB(t, logging.INFO)
defer db.Close()
db.config.Dialect = "postgresql"
require.Equal(t, "postgresql", db.Dialect())
}
func TestGetOperationType(t *testing.T) {
tests := []struct {
query string
expected string
}{
{"SELECT * FROM users", "SELECT"},
{" INSERT INTO users", "INSERT"},
{"UPDATE users SET name = ?", "UPDATE"},
{"DELETE FROM users", "DELETE"},
{"", ""},
}
for _, test := range tests {
result := getOperationType(test.query)
require.Equal(t, test.expected, result)
}
}
func TestDB_Begin_Error(t *testing.T) {
db, mock := getDB(t, logging.DEBUG)
defer db.DB.Close()
mock.ExpectBegin().WillReturnError(errbegin)
tx, err := db.Begin()
require.Error(t, err)
assert.Nil(t, tx)
}
func TestDB_sendOperationStats_RecordsMilliseconds(t *testing.T) {
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
db := &DB{
logger: logging.NewMockLogger(logging.DEBUG),
config: &DBConfig{HostName: "host", Database: "db"},
metrics: mockMetrics,
stopSignal: make(chan struct{}),
}
start := time.Now().Add(-1500 * time.Millisecond) // 1.5 seconds ago
// Expect RecordHistogram to be called with duration 1500 (milliseconds)
mockMetrics.EXPECT().RecordHistogram(
gomock.Any(), "app_sql_stats", float64(1500),
"hostname", "host", "database", "db", "type", "SELECT",
)
db.sendOperationStats(start, "SELECT", "SELECT * FROM users")
duration := time.Since(start).Milliseconds()
assert.Equal(t, int64(1500), duration)
}
func TestTx_Exec_SafeWithNilMetrics(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
// 2. FORCE nil metrics (Simulating the bug scenario)
db.metrics = nil
// 3. Begin transaction
mock.ExpectBegin()
tx, err := db.Begin()
require.NoError(t, err)
mock.ExpectExec("INSERT INTO users VALUES(.*)").
WillReturnResult(sqlmock.NewResult(1, 1))
assert.NotPanics(t, func() {
_, err = tx.Exec("INSERT INTO users VALUES(.*)", 1, "test")
}, "Tx.Exec should NOT panic when metrics are nil")
assert.NoError(t, err)
}
================================================
FILE: pkg/gofr/datasource/sql/health.go
================================================
package sql
import (
"context"
"time"
"gofr.dev/pkg/gofr/datasource"
)
type DBStats struct {
MaxOpenConnections int `json:"maxOpenConnections"` // Maximum number of open connections to the database.
// Pool Status
OpenConnections int `json:"openConnections"` // The number of established connections both in use and idle.
InUse int `json:"inUse"` // The number of connections currently in use.
Idle int `json:"idle"` // The number of idle connections.
// Counters
WaitCount int64 `json:"waitCount"` // The total number of connections waited for.
WaitDuration time.Duration `json:"waitDuration"` // The total time blocked waiting for a new connection.
MaxIdleClosed int64 `json:"maxIdleClosed"` // The total number of connections closed due to SetMaxIdleConns.
MaxIdleTimeClosed int64 `json:"maxIdleTimeClosed"` // The total number of connections closed due to SetConnMaxIdleTime.
MaxLifetimeClosed int64 `json:"maxLifetimeClosed"` // The total number of connections closed due to SetConnMaxLifetime.
}
func (d *DB) HealthCheck() *datasource.Health {
h := datasource.Health{
Details: make(map[string]any),
}
h.Details["host"] = d.config.HostName + ":" + d.config.Port + "/" + d.config.Database
if d.DB == nil {
h.Status = datasource.StatusDown
return &h
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
err := d.PingContext(ctx)
if err != nil {
h.Status = datasource.StatusDown
return &h
}
h.Status = datasource.StatusUp
dbStats := d.Stats()
h.Details["stats"] = DBStats{
MaxOpenConnections: dbStats.MaxOpenConnections,
OpenConnections: dbStats.OpenConnections,
InUse: dbStats.InUse,
Idle: dbStats.Idle,
WaitCount: dbStats.WaitCount,
WaitDuration: dbStats.WaitDuration,
MaxIdleClosed: dbStats.MaxIdleClosed,
MaxIdleTimeClosed: dbStats.MaxIdleTimeClosed,
MaxLifetimeClosed: dbStats.MaxLifetimeClosed,
}
return &h
}
================================================
FILE: pkg/gofr/datasource/sql/health_test.go
================================================
package sql
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/logging"
)
func TestHealth_HealthCheck(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
mock.ExpectPing()
db.config = &DBConfig{
HostName: "host",
Port: "3306",
Database: "test",
}
expected := &datasource.Health{
Status: "UP",
Details: map[string]any{
"host": "host:3306/test",
"stats": DBStats{
MaxOpenConnections: db.Stats().MaxOpenConnections,
OpenConnections: db.Stats().OpenConnections,
InUse: db.Stats().InUse,
Idle: db.Stats().Idle,
WaitCount: db.Stats().WaitCount,
WaitDuration: db.Stats().WaitDuration,
MaxIdleClosed: db.Stats().MaxIdleClosed,
MaxIdleTimeClosed: db.Stats().MaxIdleTimeClosed,
MaxLifetimeClosed: db.Stats().MaxLifetimeClosed,
},
},
}
out := db.HealthCheck()
assert.Equal(t, expected, out)
}
func TestHealth_HealthCheckDBNotConnected(t *testing.T) {
db := &DB{
config: &DBConfig{
HostName: "host",
Port: "3306",
Database: "test",
},
}
expected := &datasource.Health{
Status: "DOWN",
Details: map[string]any{
"host": "host:3306/test",
},
}
out := db.HealthCheck()
assert.Equal(t, expected, out)
}
func TestHealth_HealthCheckDBPingFailed(t *testing.T) {
db, mock := getDB(t, logging.INFO)
defer db.DB.Close()
mock.ExpectPing().WillReturnError(errDB)
db.config = &DBConfig{
HostName: "host",
Port: "3306",
Database: "test",
}
expected := &datasource.Health{
Status: "DOWN",
Details: map[string]any{
"host": "host:3306/test",
},
}
out := db.HealthCheck()
assert.Equal(t, expected, out)
}
================================================
FILE: pkg/gofr/datasource/sql/metrics.go
================================================
package sql
import "context"
type Metrics interface {
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/sql/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=sql
//
// Package sql is a generated GoMock package.
package sql
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/datasource/sql/query_builder.go
================================================
package sql
import (
"errors"
"fmt"
"reflect"
"strings"
)
var (
errFieldCannotBeEmpty = errors.New("field cannot be empty")
errFieldCannotBeZero = errors.New("field cannot be zero")
errFieldCannotBeNull = errors.New("field cannot be null")
)
type FieldConstraints struct {
AutoIncrement bool
NotNull bool
}
func InsertQuery(dialect, tableName string, fieldNames []string, values []any,
constraints map[string]FieldConstraints) (string, error) {
bindVars := make([]string, 0, len(fieldNames))
columns := make([]string, 0, len(fieldNames))
for i, fieldName := range fieldNames {
if constraints[fieldName].AutoIncrement {
continue
}
if err := validateNotNull(fieldName, values[i], constraints[fieldName].NotNull); err != nil {
return "", err
}
bindVars = append(bindVars, bindVar(dialect, i+1))
columns = append(columns, quotedString(quote(dialect), fieldName))
}
q := quote(dialect)
stmt := fmt.Sprintf(`INSERT INTO %s (%s) VALUES (%s)`,
quotedString(q, tableName),
strings.Join(columns, ", "),
strings.Join(bindVars, ", "),
)
return stmt, nil
}
func SelectQuery(dialect, tableName string) string {
return fmt.Sprintf(`SELECT * FROM %s`, quotedString(quote(dialect), tableName))
}
func SelectByQuery(dialect, tableName, field string) string {
q := quote(dialect)
return fmt.Sprintf(`SELECT * FROM %s WHERE %s=%s`,
quotedString(q, tableName),
quotedString(q, field),
bindVar(dialect, 1))
}
func UpdateByQuery(dialect, tableName string, fieldNames []string, field string) string {
q := quote(dialect)
fieldNamesLength := len(fieldNames)
var paramsList []string
for i := 0; i < fieldNamesLength; i++ {
paramsList = append(paramsList, fmt.Sprintf(`%s=%s`, quotedString(q, fieldNames[i]), bindVar(dialect, i+1)))
}
stmt := fmt.Sprintf(`UPDATE %s SET %s WHERE %s=%s`,
quotedString(q, tableName),
strings.Join(paramsList, ", "),
quotedString(q, field),
bindVar(dialect, fieldNamesLength+1),
)
return stmt
}
func DeleteByQuery(dialect, tableName, field string) string {
q := quote(dialect)
return fmt.Sprintf(`DELETE FROM %s WHERE %s=%s`,
quotedString(q, tableName),
quotedString(q, field),
bindVar(dialect, 1))
}
func validateNotNull(fieldName string, value any, isNotNull bool) error {
if !isNotNull {
return nil
}
switch v := value.(type) {
case string:
return validateStringNotNull(fieldName, v)
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return validateIntNotNull(fieldName, v)
case float32, float64:
return validateFloatNotNull(fieldName, v)
default:
return validateDefaultNotNull(fieldName, value)
}
}
func validateStringNotNull(fieldName, value string) error {
if value == "" {
return fmt.Errorf("%w: %s", errFieldCannotBeEmpty, fieldName)
}
return nil
}
func validateIntNotNull(fieldName string, value any) error {
if reflect.ValueOf(value).Int() == 0 {
return fmt.Errorf("%w: %s", errFieldCannotBeZero, fieldName)
}
return nil
}
func validateFloatNotNull(fieldName string, value any) error {
if reflect.ValueOf(value).Float() == 0.0 {
return fmt.Errorf("%w: %s", errFieldCannotBeZero, fieldName)
}
return nil
}
func validateDefaultNotNull(fieldName string, value any) error {
if reflect.ValueOf(value).IsNil() {
return fmt.Errorf("%w: %s", errFieldCannotBeNull, fieldName)
}
return nil
}
================================================
FILE: pkg/gofr/datasource/sql/query_builder_test.go
================================================
package sql
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_InsertQuery_Success(t *testing.T) {
tests := []struct {
name string
dialect string
tableName string
fieldNames []string
values []any
constraints map[string]FieldConstraints
expected string
}{
{
name: "Basic INSERT (MySQL)",
dialect: "mysql",
tableName: "user",
fieldNames: []string{"name", "age"},
values: []any{"John Doe", 30},
expected: "INSERT INTO `user` (`name`, `age`) VALUES (?, ?)",
},
{
name: "Basic INSERT (Postgres)",
dialect: "postgres",
tableName: "user",
fieldNames: []string{"name", "age"},
values: []any{"John Doe", 30},
expected: `INSERT INTO "user" ("name", "age") VALUES ($1, $2)`,
},
{
name: "Skip Auto-Increment (MySQL)",
dialect: "mysql",
tableName: "user",
fieldNames: []string{"id", "name"},
values: []any{1, "John Doe"},
constraints: map[string]FieldConstraints{
"id": {AutoIncrement: true},
},
expected: "INSERT INTO `user` (`name`) VALUES (?)",
},
{
name: "Skip Auto-Increment (Postgres)",
dialect: "postgres",
tableName: "user",
fieldNames: []string{"id", "name"},
values: []any{1, "John Doe"},
constraints: map[string]FieldConstraints{
"id": {AutoIncrement: true},
},
expected: `INSERT INTO "user" ("name") VALUES ($2)`,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
actual, err := InsertQuery(tc.dialect, tc.tableName, tc.fieldNames, tc.values, tc.constraints)
require.NoError(t, err)
assert.Equal(t, tc.expected, actual)
})
}
}
func Test_InsertQuery_Error(t *testing.T) {
tests := []struct {
name string
dialect string
tableName string
fieldNames []string
values []any
constraints map[string]FieldConstraints
}{
{
name: "NotNull Validation Error (MySQL)",
dialect: "mysql",
tableName: "user",
fieldNames: []string{"name"},
values: []any{""},
constraints: map[string]FieldConstraints{
"name": {NotNull: true},
},
},
{
name: "NotNull Validation Error (Postgres)",
dialect: "postgres",
tableName: "user",
fieldNames: []string{"age"},
values: []any{0},
constraints: map[string]FieldConstraints{
"age": {NotNull: true},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := InsertQuery(tc.dialect, tc.tableName, tc.fieldNames, tc.values, tc.constraints)
require.Error(t, err)
})
}
}
func Test_SelectQuery(t *testing.T) {
tableName := "user"
tests := []struct {
dialect string
expected string
}{
{
dialect: "mysql",
expected: "SELECT * FROM `user`",
},
{
dialect: "postgres",
expected: `SELECT * FROM "user"`,
},
}
for i, tc := range tests {
t.Run(tc.dialect, func(t *testing.T) {
actual := SelectQuery(tc.dialect, tableName)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.dialect)
})
}
}
func Test_SelectByQuery(t *testing.T) {
tableName := "user"
field := "id"
tests := []struct {
dialect string
expected string
}{
{
dialect: "mysql",
expected: "SELECT * FROM `user` WHERE `id`=?",
},
{
dialect: "postgres",
expected: `SELECT * FROM "user" WHERE "id"=$1`,
},
}
for i, tc := range tests {
t.Run(tc.dialect, func(t *testing.T) {
actual := SelectByQuery(tc.dialect, tableName, field)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.dialect)
})
}
}
func Test_UpdateByQuery(t *testing.T) {
tableName := "user"
fieldNames := []string{"name", "age"}
field := "id"
tests := []struct {
dialect string
expected string
}{
{
dialect: "mysql",
expected: "UPDATE `user` SET `name`=?, `age`=? WHERE `id`=?",
},
{
dialect: "postgres",
expected: `UPDATE "user" SET "name"=$1, "age"=$2 WHERE "id"=$3`,
},
}
for i, tc := range tests {
t.Run(tc.dialect, func(t *testing.T) {
actual := UpdateByQuery(tc.dialect, tableName, fieldNames, field)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.dialect)
})
}
}
func Test_DeleteByQuery(t *testing.T) {
tableName := "user"
field := "id"
tests := []struct {
dialect string
expected string
}{
{
dialect: "mysql",
expected: "DELETE FROM `user` WHERE `id`=?",
},
{
dialect: "postgres",
expected: `DELETE FROM "user" WHERE "id"=$1`,
},
}
for i, tc := range tests {
t.Run(tc.dialect, func(t *testing.T) {
actual := DeleteByQuery(tc.dialect, tableName, field)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.dialect)
})
}
}
func Test_validateNotNull_Error(t *testing.T) {
type customType struct{}
tests := []struct {
name string
fieldName string
value any
isNotNull bool
expectedErr string
}{
{
name: "Float null error",
fieldName: "weight",
value: 0.0,
isNotNull: true,
expectedErr: "field cannot be zero: weight",
},
{
name: "Nil channel",
fieldName: "channelField",
value: chan int(nil),
isNotNull: true,
expectedErr: "field cannot be null: channelField",
},
{
name: "Custom type nil",
fieldName: "customField",
value: (*customType)(nil),
isNotNull: true,
expectedErr: "field cannot be null: customField",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateNotNull(tt.fieldName, tt.value, tt.isNotNull)
require.EqualError(t, err, tt.expectedErr)
})
}
}
================================================
FILE: pkg/gofr/datasource/sql/sql.go
================================================
package sql
import (
"context"
"crypto/tls"
"crypto/x509"
"database/sql"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/XSAM/otelsql"
"github.com/go-sql-driver/mysql"
_ "github.com/lib/pq" // used for concrete implementation of the database driver.
_ "modernc.org/sqlite"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource"
)
const (
sqlite = "sqlite"
cockroachDB = "cockroachdb"
defaultDBPort = 3306
requireSSLMode = "require"
tlsSkipVerify = "tls=skip-verify"
)
var (
errUnsupportedDialect = fmt.Errorf(
"unsupported db dialect; supported dialects are - mysql, postgres, supabase, sqlite, %s", cockroachDB)
errFailedCACerts = fmt.Errorf("failed to append CA certificate")
)
// DBConfig has those members which are necessary variables while connecting to database.
type DBConfig struct {
Dialect string
HostName string
User string
Password string
Port string
Database string
SSLMode string
MaxIdleConn int
MaxOpenConn int
Charset string
}
func setupSupabaseDefaults(dbConfig *DBConfig, configs config.Config, logger datasource.Logger) {
if dbConfig.HostName == "" {
projectRef := configs.Get("SUPABASE_PROJECT_REF")
if projectRef != "" {
dbConfig.HostName = fmt.Sprintf("db.%s.supabase.co", projectRef)
}
}
if dbConfig.Database == "" {
dbConfig.Database = dialectPostgres
}
if dbConfig.SSLMode != requireSSLMode {
logger.Warnf("Supabase connections require SSL. Setting DB_SSL_MODE to 'require'")
dbConfig.SSLMode = requireSSLMode // Enforce SSL mode for Supabase
}
if dbConfig.Port == strconv.Itoa(defaultDBPort) {
dbConfig.Port = "5432"
}
}
func NewSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *DB {
dbConfig := getDBConfig(configs)
if dbConfig.Dialect == supabaseDialect {
setupSupabaseDefaults(dbConfig, configs, logger)
}
if dbConfig.Dialect == "" {
return nil
}
// if Hostname is not provided, we won't try to connect to DB
if dbConfig.Dialect != sqlite && dbConfig.HostName == "" {
logger.Errorf("connection to %s failed: host name is empty.", dbConfig.Dialect)
}
// Register MySQL TLS config if needed (BEFORE opening connection)
if err := registerMySQLTLSConfig(dbConfig, logger); err != nil {
if strings.Contains(strings.ToLower(dbConfig.SSLMode), "verify") {
logger.Errorf("failed to register MySQL TLS config: %v", err)
return nil
}
logger.Warnf("failed to register MySQL TLS config: %v", err)
}
logger.Debugf("generating database connection string for '%s'", dbConfig.Dialect)
dbConnectionString, err := getDBConnectionString(dbConfig)
if err != nil {
logger.Error(errUnsupportedDialect)
return nil
}
logger.Debugf("registering sql dialect '%s' for traces", dbConfig.Dialect)
otelRegisteredDialect, err := registerOtel(dbConfig.Dialect, logger)
if err != nil {
logger.Errorf("could not register sql dialect '%s' for traces, error: %s", dbConfig.Dialect, err)
return nil
}
database := &DB{config: dbConfig, logger: logger, metrics: metrics, stopSignal: make(chan struct{})}
printConnectionSuccessLog("connecting", database.config, logger)
database.DB, err = sql.Open(otelRegisteredDialect, dbConnectionString)
if err != nil {
printConnectionFailureLog("open connection with", database.config, database.logger, err)
return database
}
// We are not setting idle connection timeout because we are checking for connection
// every 10 seconds which would need a connection, moreover if connection expires it is
// automatically closed by the database/sql package.
database.DB.SetMaxIdleConns(dbConfig.MaxIdleConn)
// We are not setting max open connection because any connection which is expired,
// it is closed automatically.
database.DB.SetMaxOpenConns(dbConfig.MaxOpenConn)
database = pingToTestConnection(database)
go retryConnection(database)
go pushDBMetrics(database, metrics)
return database
}
func registerOtel(dialect string, logger datasource.Logger) (string, error) {
// Supabase and CockroachDB use the PostgreSQL driver, so we register them as the "postgres" dialect
// to ensure compatibility with OpenTelemetry instrumentation.
otelSupportedDialect := dialect
if dialect == supabaseDialect || dialect == cockroachDB {
logger.Debugf("using '%s' as an alias for '%s' for otel-sql registration", dialectPostgres, dialect)
otelSupportedDialect = dialectPostgres
}
return otelsql.Register(otelSupportedDialect)
}
func pingToTestConnection(database *DB) *DB {
if err := database.DB.PingContext(context.Background()); err != nil {
printConnectionFailureLog("connect", database.config, database.logger, err)
return database
}
printConnectionSuccessLog("connected", database.config, database.logger)
return database
}
func retryConnection(database *DB) {
const connRetryFrequencyInSeconds = 10
retryDuration := connRetryFrequencyInSeconds * time.Second
for {
select {
case <-database.stopSignal:
return
default:
}
if database.DB.PingContext(context.Background()) != nil {
database.logger.Info("retrying SQL database connection")
if !attemptReconnection(database, retryDuration) {
return
}
}
select {
case <-time.After(retryDuration):
case <-database.stopSignal:
return
}
}
}
func attemptReconnection(database *DB, retryDuration time.Duration) bool {
for {
select {
case <-database.stopSignal:
return false
default:
}
err := database.DB.PingContext(context.Background())
if err == nil {
printConnectionSuccessLog("connected", database.config, database.logger)
return true
}
printConnectionFailureLog("connect", database.config, database.logger, err)
select {
case <-time.After(retryDuration):
case <-database.stopSignal:
return false
}
}
}
func getDBConfig(configs config.Config) *DBConfig {
const (
defaultMaxIdleConn = 2
defaultMaxOpenConn = 0
)
// if the value of maxIdleConn is negative or 0, no idle connections are retained.
maxIdleConn, err := strconv.Atoi(configs.Get("DB_MAX_IDLE_CONNECTION"))
if err != nil {
// setting the max open connection as the default which is being provided by default package
maxIdleConn = defaultMaxIdleConn
}
// if the value of maxOpenConn is negative, it is treated as 0 by sql package.
maxOpenConn, err := strconv.Atoi(configs.Get("DB_MAX_OPEN_CONNECTION"))
if err != nil {
// setting the max open connection as the default which is being provided by default
// in this case there will be no limit for number of max open connections.
maxOpenConn = defaultMaxOpenConn
}
return &DBConfig{
Dialect: configs.Get("DB_DIALECT"),
HostName: configs.Get("DB_HOST"),
User: configs.Get("DB_USER"),
Password: configs.Get("DB_PASSWORD"),
Port: configs.GetOrDefault("DB_PORT", strconv.Itoa(defaultDBPort)),
Database: configs.Get("DB_NAME"),
MaxOpenConn: maxOpenConn,
MaxIdleConn: maxIdleConn,
// Supported for postgres, supabase, cockroachdb, and mysql
SSLMode: configs.GetOrDefault("DB_SSL_MODE", "disable"),
Charset: configs.Get("DB_CHARSET"),
}
}
func getDBConnectionString(dbConfig *DBConfig) (string, error) {
switch dbConfig.Dialect {
case "mysql":
if dbConfig.Charset == "" {
dbConfig.Charset = "utf8"
}
connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=True&loc=Local&interpolateParams=true",
dbConfig.User,
dbConfig.Password,
dbConfig.HostName,
dbConfig.Port,
dbConfig.Database,
dbConfig.Charset,
)
if tlsParam := getMySQLTLSParam(dbConfig.SSLMode); tlsParam != "" {
connStr = fmt.Sprintf("%s&%s", connStr, tlsParam)
}
return connStr, nil
case dialectPostgres, supabaseDialect, cockroachDB:
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
dbConfig.HostName, dbConfig.Port, dbConfig.User, dbConfig.Password, dbConfig.Database, dbConfig.SSLMode), nil
case sqlite:
s := strings.TrimSuffix(dbConfig.Database, ".db")
return fmt.Sprintf("file:%s.db", s), nil
default:
return "", errUnsupportedDialect
}
}
func pushDBMetrics(database *DB, metrics Metrics) {
const frequency = 10
for {
select {
case <-database.stopSignal:
return
default:
}
if database.DB != nil {
stats := database.DB.Stats()
metrics.SetGauge("app_sql_open_connections", float64(stats.OpenConnections))
metrics.SetGauge("app_sql_inUse_connections", float64(stats.InUse))
select {
case <-time.After(frequency * time.Second):
case <-database.stopSignal:
return
}
}
}
}
func printConnectionSuccessLog(status string, dbconfig *DBConfig, logger datasource.Logger) {
logFunc := logger.Infof
if status != "connected" {
logFunc = logger.Debugf
}
if dbconfig.Dialect == sqlite {
logFunc("%s to '%s' database", status, dbconfig.Database)
} else {
logFunc("%s to '%s' user to '%s' database at '%s:%s'", status, dbconfig.User, dbconfig.Database, dbconfig.HostName, dbconfig.Port)
}
}
func printConnectionFailureLog(action string, dbconfig *DBConfig, logger datasource.Logger, err error) {
if dbconfig.Dialect == sqlite {
logger.Errorf("could not %s database '%s', error: %v", action, dbconfig.Database, err)
} else {
logger.Errorf("could not %s '%s' user to '%s' database at '%s:%s', error: %v",
action, dbconfig.User, dbconfig.Database, dbconfig.HostName, dbconfig.Port, err)
}
}
// getMySQLTLSParam converts the generic DB_SSL_MODE to MySQL-specific TLS parameter.
// For custom CA certificates, use DB_TLS_CA_CERT environment variable.
func getMySQLTLSParam(sslMode string) string {
switch strings.ToLower(sslMode) {
case "disable", "false":
return "" // No TLS - insecure
case "preferred":
return "tls=preferred" // Try TLS, fallback to plain
case "require", "true":
return tlsSkipVerify // TLS required but no cert validation
case "skip-verify":
return tlsSkipVerify // Explicit skip verification
case "verify-ca", "verify-full":
return "tls=custom" // Use custom TLS config with CA verification
default:
return "" // Default to no TLS
}
}
// registerMySQLTLSConfig registers custom TLS configuration for MySQL if needed.
func registerMySQLTLSConfig(dbConfig *DBConfig, logger datasource.Logger) error {
// Only for MySQL with verify-ca or verify-full
if dbConfig.Dialect != dialectMysql {
return nil
}
if !strings.Contains(strings.ToLower(dbConfig.SSLMode), "verify") {
return nil // skip-verify doesn't need custom config
}
caCertPath := os.Getenv("DB_TLS_CA_CERT")
if caCertPath == "" {
logger.Warn("DB_SSL_MODE=verify-ca requires DB_TLS_CA_CERT. Falling back to system CA pool")
// Use system CA pool
tlsConfig := &tls.Config{
ServerName: getServerName(dbConfig.HostName),
MinVersion: tls.VersionTLS12,
}
return mysql.RegisterTLSConfig("custom", tlsConfig)
}
// Load custom CA certificate
caCert, err := os.ReadFile(caCertPath)
if err != nil {
return fmt.Errorf("failed to read CA certificate from %s: %w", caCertPath, err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return errFailedCACerts
}
tlsConfig := &tls.Config{
RootCAs: caCertPool,
ServerName: dbConfig.HostName,
MinVersion: tls.VersionTLS12,
}
// Optional: Support client certificates (mutual TLS)
clientCertPath := os.Getenv("DB_TLS_CLIENT_CERT")
clientKeyPath := os.Getenv("DB_TLS_CLIENT_KEY")
if clientCertPath != "" && clientKeyPath != "" {
clientCert, err := tls.LoadX509KeyPair(clientCertPath, clientKeyPath)
if err != nil {
return fmt.Errorf("failed to load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{clientCert}
logger.Debug("loaded client certificate for mutual TLS")
}
return mysql.RegisterTLSConfig("custom", tlsConfig)
}
func getServerName(hostname string) string {
// For localhost/127.0.0.1, use "localhost" explicitly
if hostname == "127.0.0.1" || hostname == "::1" {
return "localhost"
}
return hostname
}
================================================
FILE: pkg/gofr/datasource/sql/sql_mock.go
================================================
package sql
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
func NewSQLMocks(t *testing.T) (*DB, sqlmock.Sqlmock, *MockMetrics) {
t.Helper()
return NewSQLMocksWithConfig(t, &DBConfig{})
}
func NewSQLMocksWithConfig(t *testing.T, config *DBConfig) (*DB, sqlmock.Sqlmock, *MockMetrics) {
t.Helper()
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), "app_sql_stats", gomock.Any(),
"hostname", gomock.Any(), "database", gomock.Any(), "type", gomock.Any()).AnyTimes()
return &DB{
DB: db,
logger: logging.NewMockLogger(logging.DEBUG),
config: config,
metrics: mockMetrics,
stopSignal: make(chan struct{}),
}, mock, mockMetrics
}
================================================
FILE: pkg/gofr/datasource/sql/sql_test.go
================================================
package sql
import (
"errors"
"fmt"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestNewSQL_ErrorCase(t *testing.T) {
ctrl := gomock.NewController(t)
expectedLog := "could not connect 'testuser' user to 'testdb' database at 'localhost:3306'"
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "mysql",
"DB_HOST": "localhost",
"DB_USER": "testuser",
"DB_PASSWORD": "testpassword",
"DB_PORT": "3306",
"DB_NAME": "testdb",
"DB_SSL_MODE": "disable",
})
testLogs := testutil.StderrOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
db := NewSQL(mockConfig, mockLogger, mockMetrics)
// Fix: Stop the goroutine by closing the DB connection
if db != nil && db.DB != nil {
db.Close()
}
time.Sleep(10 * time.Millisecond)
})
assert.Containsf(t, testLogs, expectedLog, "TestNewSQL_ErrorCase Failed! Expected error log doesn't match actual.")
}
func TestNewSQL_InvalidDialect(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "abc",
"DB_HOST": "localhost",
})
testLogs := testutil.StderrOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
NewSQL(mockConfig, mockLogger, mockMetrics)
})
assert.Containsf(t, testLogs, errUnsupportedDialect.Error(), "TestNewSQL_ErrorCase Failed! Expected error log doesn't match actual.")
}
func TestNewSQL_GetDBDialect(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "postgres",
"DB_HOST": "localhost",
})
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
// using gomock.Any as we are not actually testing any feature related to metrics
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
db := NewSQL(mockConfig, mockLogger, mockMetrics)
dialect := db.Dialect()
assert.Equal(t, "postgres", dialect)
time.Sleep(100 * time.Millisecond)
}
func TestNewSQL_InvalidConfig(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "",
})
mockLogger := logging.NewMockLogger(logging.ERROR)
mockMetrics := NewMockMetrics(ctrl)
db := NewSQL(mockConfig, mockLogger, mockMetrics)
assert.Nil(t, db, "TestNewSQL_InvalidConfig. expected db to be nil.")
}
func TestSQL_GetDBConfig(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "mysql",
"DB_HOST": "host",
"DB_USER": "user",
"DB_PASSWORD": "password",
"DB_PORT": "3201",
"DB_NAME": "test",
"DB_SSL_MODE": "require",
"DB_MAX_IDLE_CONNECTION": "25",
"DB_MAX_OPEN_CONNECTION": "50",
"DB_CHARSET": "utf8mb4",
})
expectedConfigs := &DBConfig{
Dialect: "mysql",
HostName: "host",
User: "user",
Password: "password",
Port: "3201",
Database: "test",
SSLMode: "require",
MaxIdleConn: 25,
MaxOpenConn: 50,
Charset: "utf8mb4",
}
configs := getDBConfig(mockConfig)
assert.Equal(t, expectedConfigs, configs)
}
func TestSQL_ConfigCases(t *testing.T) {
testCases := []struct {
name string
idleConn string
openConn string
expectedIdle int
expectedOpen int
}{
{
name: "Invalid Max Idle and Open Connections",
idleConn: "abc",
openConn: "def",
expectedIdle: 2,
expectedOpen: 0,
},
{
name: "Negative Max Idle and Open Connections",
idleConn: "-2",
openConn: "-3",
expectedIdle: -2,
expectedOpen: -3,
},
}
for _, tc := range testCases {
mockConfig := config.NewMockConfig(map[string]string{
"DB_MAX_IDLE_CONNECTION": tc.idleConn,
"DB_MAX_OPEN_CONNECTION": tc.openConn,
})
expectedConfig := &DBConfig{
Port: "3306",
MaxIdleConn: tc.expectedIdle,
MaxOpenConn: tc.expectedOpen,
SSLMode: "disable",
}
configs := getDBConfig(mockConfig)
assert.Equal(t, expectedConfig, configs)
}
}
func TestSQL_getDBConnectionString(t *testing.T) {
testCases := []struct {
desc string
configs *DBConfig
expOut string
expErr error
}{
{
desc: "mysql dialect",
configs: &DBConfig{
Dialect: "mysql",
HostName: "host",
User: "user",
Password: "password",
Port: "3201",
Database: "test",
},
expOut: "user:password@tcp(host:3201)/test?charset=utf8&parseTime=True&loc=Local&interpolateParams=true",
},
{
desc: "mysql dialect with Configurable charset",
configs: &DBConfig{
Dialect: "mysql",
HostName: "host",
User: "user",
Password: "password",
Port: "3201",
Database: "test",
Charset: "utf8mb4",
},
expOut: "user:password@tcp(host:3201)/test?charset=utf8mb4&parseTime=True&loc=Local&interpolateParams=true",
},
{
desc: "postgresql dialect",
configs: &DBConfig{
Dialect: "postgres",
HostName: "host",
User: "user",
Password: "password",
Port: "3201",
Database: "test",
SSLMode: "require",
},
expOut: "host=host port=3201 user=user password=password dbname=test sslmode=require",
},
{
desc: "postgresql dialect",
configs: &DBConfig{
Dialect: "postgres",
HostName: "host",
User: "user",
Password: "password",
Port: "3201",
Database: "test",
SSLMode: "disable",
},
expOut: "host=host port=3201 user=user password=password dbname=test sslmode=disable",
},
{
desc: "sqlite dialect",
configs: &DBConfig{
Dialect: "sqlite",
Database: "test.db",
},
expOut: "file:test.db",
},
{
desc: "cockroachdb dialect",
configs: &DBConfig{
Dialect: "cockroachdb",
HostName: "host",
User: "user",
Password: "password",
Port: "26257",
Database: "test",
SSLMode: "require",
},
expOut: "host=host port=26257 user=user password=password dbname=test sslmode=require",
},
{
desc: "unsupported dialect",
configs: &DBConfig{Dialect: "mssql"},
expOut: "",
expErr: errUnsupportedDialect,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
connString, err := getDBConnectionString(tc.configs)
assert.Equal(t, tc.expOut, connString)
assert.Equal(t, tc.expErr, err)
})
}
}
func Test_NewSQLMock(t *testing.T) {
db, mock, mockMetric := NewSQLMocks(t)
assert.NotNil(t, db)
assert.NotNil(t, mock)
assert.NotNil(t, mockMetric)
}
func Test_NewSQLMockWithConfig(t *testing.T) {
dbConfig := DBConfig{Dialect: "dialect", HostName: "hostname", User: "user", Password: "password", Port: "port", Database: "database"}
db, mock, mockMetric := NewSQLMocksWithConfig(t, &dbConfig)
assert.NotNil(t, db)
assert.Equal(t, db.config, &dbConfig)
assert.NotNil(t, mock)
assert.NotNil(t, mockMetric)
}
var errSqliteConnection = errors.New("connection failed")
func Test_sqliteSuccessfulConnLogs(t *testing.T) {
tests := []struct {
desc string
status string
expectedLog string
}{
{"sqlite connection in process", "connecting", `connecting to 'test' database`},
{"sqlite connected successfully", "connected", `connected to 'test' database`},
}
for _, test := range tests {
logs := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockConfig := &DBConfig{
Dialect: sqlite,
Database: "test",
}
printConnectionSuccessLog(test.status, mockConfig, mockLogger)
})
assert.Contains(t, logs, test.expectedLog)
}
}
func Test_sqliteErrConnLogs(t *testing.T) {
test := []struct {
desc string
action string
err error
expectedLog string
}{
{"sqlite connection failure", "connect", errSqliteConnection,
`could not connect database 'test', error: connection failed`},
{"sqlite open connection failure", "open connection with", errSqliteConnection,
`could not open connection with database 'test', error: connection failed`},
}
for _, tt := range test {
logs := testutil.StderrOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockConfig := &DBConfig{
Dialect: sqlite,
Database: "test",
}
printConnectionFailureLog(tt.action, mockConfig, mockLogger, tt.err)
})
assert.Contains(t, logs, tt.expectedLog)
}
}
func Test_SQLRetryConnectionInfoLog(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
ctrl := gomock.NewController(t)
mockMetrics := NewMockMetrics(ctrl)
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "postgres",
"DB_HOST": "host",
"DB_USER": "user",
"DB_PASSWORD": "password",
"DB_PORT": "3201",
"DB_NAME": "test",
})
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockMetrics.EXPECT().SetGauge("app_sql_open_connections", float64(0))
mockMetrics.EXPECT().SetGauge("app_sql_inUse_connections", float64(0))
_ = NewSQL(mockConfig, mockLogger, mockMetrics)
time.Sleep(100 * time.Millisecond)
})
assert.Contains(t, logs, "retrying SQL database connection")
}
func TestNewSQL_CockroachDB(t *testing.T) {
ctrl := gomock.NewController(t)
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "cockroachdb",
"DB_HOST": "localhost",
"DB_USER": "testuser",
"DB_PASSWORD": "testpassword",
"DB_PORT": "26257",
"DB_NAME": "testdb",
"DB_SSL_MODE": "disable",
})
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockMetrics := NewMockMetrics(ctrl)
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
testLogs := testutil.StderrOutputForFunc(func() {
db := NewSQL(mockConfig, mockLogger, mockMetrics)
assert.NotNil(t, db, "Expected a non-nil DB object for cockroachdb")
if db != nil {
assert.Equal(t, "cockroachdb", db.Dialect(), "Expected dialect to be cockroachdb")
}
})
fmt.Println("Test Logs for CockroachDB:", testLogs)
}
func TestGetServerName(t *testing.T) {
tests := []struct {
hostname string
expected string
}{
{"127.0.0.1", "localhost"},
{"::1", "localhost"},
{"db.example.com", "db.example.com"},
{"192.168.1.100", "192.168.1.100"},
}
for _, tt := range tests {
result := getServerName(tt.hostname)
assert.Equal(t, tt.expected, result)
}
}
func TestGetMySQLTLSParam(t *testing.T) {
tests := []struct {
sslMode string
expected string
}{
{"disable", ""},
{"require", "tls=skip-verify"},
{"verify-ca", "tls=custom"},
{"verify-full", "tls=custom"},
{"preferred", "tls=preferred"},
}
for _, tt := range tests {
result := getMySQLTLSParam(tt.sslMode)
assert.Equal(t, tt.expected, result)
}
}
func TestRegisterMySQLTLSConfig_WithValidCA(t *testing.T) {
tests := []struct {
name string
setupCerts func(t *testing.T) map[string]string
sslMode string
wantErr bool
}{
{
name: "MySQL verify-ca with valid CA cert",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
return map[string]string{"ca": caCertPath}
},
sslMode: "verify-ca",
wantErr: false,
},
{
name: "MySQL verify-full with valid CA cert",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
return map[string]string{"ca": caCertPath}
},
sslMode: "verify-full",
wantErr: false,
},
{
name: "MySQL with 127.0.0.1 hostname - uses localhost as ServerName",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
return map[string]string{"ca": caCertPath}
},
sslMode: "verify-ca",
wantErr: false,
},
{
name: "MySQL with ::1 hostname - uses localhost as ServerName",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
return map[string]string{"ca": caCertPath}
},
sslMode: "verify-ca",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
certPaths := tt.setupCerts(t)
defer cleanupCerts(certPaths)
mockLogger := logging.NewMockLogger(logging.DEBUG)
dbConfig := &DBConfig{
Dialect: "mysql",
HostName: "localhost",
SSLMode: tt.sslMode,
}
err := registerMySQLTLSConfig(dbConfig, mockLogger)
assert.NoError(t, err)
})
}
}
func TestRegisterMySQLTLSConfig_WithMutualTLS(t *testing.T) {
tests := []struct {
name string
setupCerts func(t *testing.T) map[string]string
sslMode string
wantErr bool
}{
{
name: "MySQL with valid CA and client certificates",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
clientCertPath, clientKeyPath := createValidClientCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_CERT", clientCertPath)
t.Setenv("DB_TLS_CLIENT_KEY", clientKeyPath)
return map[string]string{"ca": caCertPath, "cert": clientCertPath, "key": clientKeyPath}
},
sslMode: "verify-ca",
wantErr: false,
},
{
name: "MySQL verify-full with mutual TLS",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
clientCertPath, clientKeyPath := createValidClientCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_CERT", clientCertPath)
t.Setenv("DB_TLS_CLIENT_KEY", clientKeyPath)
return map[string]string{"ca": caCertPath, "cert": clientCertPath, "key": clientKeyPath}
},
sslMode: "verify-full",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
certPaths := tt.setupCerts(t)
defer cleanupCerts(certPaths)
mockLogger := logging.NewMockLogger(logging.DEBUG)
dbConfig := &DBConfig{
Dialect: "mysql",
HostName: "localhost",
SSLMode: tt.sslMode,
}
err := registerMySQLTLSConfig(dbConfig, mockLogger)
assert.NoError(t, err)
})
}
}
func TestRegisterMySQLTLSConfig_PartialClientCert(t *testing.T) {
tests := []struct {
name string
setupCerts func(t *testing.T) map[string]string
sslMode string
wantErr bool
}{
{
name: "MySQL with only client cert - no key provided",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
clientCertPath, _ := createValidClientCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_CERT", clientCertPath)
return map[string]string{"ca": caCertPath, "cert": clientCertPath}
},
sslMode: "verify-ca",
wantErr: false,
},
{
name: "MySQL with only client key - no cert provided",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
_, clientKeyPath := createValidClientCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_KEY", clientKeyPath)
return map[string]string{"ca": caCertPath, "key": clientKeyPath}
},
sslMode: "verify-ca",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
certPaths := tt.setupCerts(t)
defer cleanupCerts(certPaths)
mockLogger := logging.NewMockLogger(logging.DEBUG)
dbConfig := &DBConfig{
Dialect: "mysql",
HostName: "localhost",
SSLMode: tt.sslMode,
}
err := registerMySQLTLSConfig(dbConfig, mockLogger)
assert.NoError(t, err)
})
}
}
func TestRegisterMySQLTLSConfig_InvalidClientCert(t *testing.T) {
tests := []struct {
name string
setupCerts func(t *testing.T) map[string]string
sslMode string
wantErr bool
}{
{
name: "MySQL with invalid client certificate file",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
invalidCertPath := createInvalidCert(t)
_, clientKeyPath := createValidClientCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_CERT", invalidCertPath)
t.Setenv("DB_TLS_CLIENT_KEY", clientKeyPath)
return map[string]string{"ca": caCertPath, "cert": invalidCertPath, "key": clientKeyPath}
},
sslMode: "verify-ca",
wantErr: true,
},
{
name: "MySQL with invalid client key file",
setupCerts: func(t *testing.T) map[string]string {
t.Helper()
caCertPath := createValidCACert(t)
clientCertPath, _ := createValidClientCert(t)
invalidKeyPath := createInvalidCert(t)
t.Setenv("DB_TLS_CA_CERT", caCertPath)
t.Setenv("DB_TLS_CLIENT_CERT", clientCertPath)
t.Setenv("DB_TLS_CLIENT_KEY", invalidKeyPath)
return map[string]string{"ca": caCertPath, "cert": clientCertPath, "key": invalidKeyPath}
},
sslMode: "verify-ca",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
certPaths := tt.setupCerts(t)
defer cleanupCerts(certPaths)
mockLogger := logging.NewMockLogger(logging.DEBUG)
dbConfig := &DBConfig{
Dialect: "mysql",
HostName: "localhost",
SSLMode: tt.sslMode,
}
err := registerMySQLTLSConfig(dbConfig, mockLogger)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to load client certificate")
})
}
}
func createValidCACert(t *testing.T) string {
t.Helper()
tmpFile, err := os.CreateTemp(t.TempDir(), "test-ca-*.pem")
require.NoError(t, err)
caCert := `-----BEGIN CERTIFICATE-----
MIIDvTCCAqWgAwIBAgIUJz8pRPZUcAMOWjDIFs+5pA88kdswDQYJKoZIhvcNAQEL
BQAwZjELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5
MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDTALBgNVBAsMBFVuaXQxEjAQBgNVBAMM
CWxvY2FsaG9zdDAeFw0yNTEyMDgxMDE1MTZaFw0zNTEyMDYxMDE1MTZaMGYxCzAJ
BgNVBAYTAlVTMQ4wDAYDVQQIDAVTdGF0ZTENMAsGA1UEBwwEQ2l0eTEVMBMGA1UE
CgwMT3JnYW5pemF0aW9uMQ0wCwYDVQQLDARVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCgoYPEAOt59viipZCS
2ziFAcsCWKO2A7kLnTXkrwJYu0qkDf/liT1Eh+XYRDeURJd/9kjZoLpKKsY9ychI
Ap7beZK2YP5ZjqpP4xg4n66hdXlaUW4zW0PnjrlrG41yKx+t4U/pvw/C8it/x1eV
SLkPqb6cKB7Gibuu8CaqGTB6Dcn4OTM36jMqLvwniNoowU9TpUriONnnwKSevA/y
Q2PVcfF7dsgVxN7FkpGvB5YDhA3ZILIudBEDQsHzPeMWW/OzCCD50OEzvVTNbcxK
Jm8LpD43fWhnpZ/rd5t10/d2AESZTFP4IKMIxIY6ZZNtg7khlBEPClQtOfPnr3IN
w731AgMBAAGjYzBhMB0GA1UdDgQWBBQIV03Kq2NkXPSiizzpatoaYeLtPjAfBgNV
HSMEGDAWgBQIV03Kq2NkXPSiizzpatoaYeLtPjAPBgNVHRMBAf8EBTADAQH/MA4G
A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAQEAAaf4ZKoeFnxtUBAD1WI+
bHYezP8kQ0qSpXd5685SQ4EfG7zrEjzXMM19JCemss3euiJ2AgoqCRRPAtPTc2IR
Y99NvmoNnIlISaG2pmI5M0I9YKNdD8D8y/Dm6DQoBJ7gSlAIzKlWTT+wmeJmGFBW
+N95qB2BqoOlXF707ngnEA26o0Phdwvl+H006CebAA1vx7ZTln5CCjEd6VWZ/8Jg
Q+JQBufVKWbvnEcERZXHPV8+hut4qLhJmKHW76/2da7wefsU2B2CuKVdfKHo9SSF
A3PeXPVPJSAoBmD0o7nmviZWP+TaIxBojSnDeE7eNSWNF2Ug/PJo/LHvyeaGuq+5
uw==
-----END CERTIFICATE-----`
_, err = tmpFile.WriteString(caCert)
require.NoError(t, err)
tmpFile.Close()
return tmpFile.Name()
}
func createValidClientCert(t *testing.T) (certName, keyName string) {
t.Helper()
certFile, err := os.CreateTemp(t.TempDir(), "test-client-cert-*.pem")
require.NoError(t, err)
clientCert := `-----BEGIN CERTIFICATE-----
MIIDhjCCAm6gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJVUzEO
MAwGA1UECAwFU3RhdGUxDTALBgNVBAcMBENpdHkxFTATBgNVBAoMDE9yZ2FuaXph
dGlvbjENMAsGA1UECwwEVW5pdDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI1MTIw
ODEwMTUxNloXDTM1MTIwNjEwMTUxNlowYzELMAkGA1UEBhMCVVMxDjAMBgNVBAgM
BVN0YXRlMQ0wCwYDVQQHDARDaXR5MRUwEwYDVQQKDAxPcmdhbml6YXRpb24xDTAL
BgNVBAsMBFVuaXQxDzANBgNVBAMMBmNsaWVudDCCASIwDQYJKoZIhvcNAQEBBQAD
ggEPADCCAQoCggEBAPQxEO4bOB3U5qjGcl7D+qciCCOYvHnP5bTWjkI/0G+3LDa1
oJkhQsKn3jW7VlhUP9pnMiazS+Be2X4+NwcXX40jcBwPJTAFHdNnqtXwqaVeTzJv
nSRgBZvDNUEFjx14rHHqMBWqyaYPeLt2rd53dCxExHFtUJLyMiGKuv57YiNu00h7
umGTaOrZx2KjvssiDVo3MlckhKvr+H4MR6ashsP5Nx8bls3916iHX10APblpw6oZ
ZgrPY8Hw5ucL/dSyfAhlweUKwD/MT7P4OtXLtp6DX7UAdjC/YhXog4gIWurvCYAo
P7/2cl2+86JdjYXas55SfBoY9N9Y+rQQQO0I7SsCAwEAAaNCMEAwHQYDVR0OBBYE
FJWdyIL+p4qd4u7kGc1PsAa/tSZ5MB8GA1UdIwQYMBaAFAhXTcqrY2Rc9KKLPOlq
2hph4u0+MA0GCSqGSIb3DQEBCwUAA4IBAQAB70Pjep8SyWKJ2uqzHcMSO9VuNs37
BFRJ1F1zkmBmg5WmgJ61Cwf4PZofF5MRSQui0Bzkhi8A8pF558Sf8fZHkxQ0DHmH
jVNOp06K8BEfmpXVMR6AGwRx6WLjyoQ0g+z7xcIRhS3DDPz4R3WiTbOf4eZ0j+uq
GAYsLM9Ql4D6jdLPfn8A3mqy0xief9bj5dkLCkoEb4csPlmutrbSSTxEi+byEnQM
ASDTTY2dJpEJLXboZYwXY25R+69eVl6CjWnReGYrAoYueIS/1fJ0ik3wGvWZNtGz
TA8/qi+HLJqT79lYy5YVA0d4PuOJVyuAla4Mv64uyOv3g8HYvIUaWdMJ
-----END CERTIFICATE-----`
_, err = certFile.WriteString(clientCert)
require.NoError(t, err)
certFile.Close()
keyFile, err := os.CreateTemp(t.TempDir(), "test-client-key-*.pem")
require.NoError(t, err)
clientKey := `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD0MRDuGzgd1Oao
xnJew/qnIggjmLx5z+W01o5CP9Bvtyw2taCZIULCp941u1ZYVD/aZzIms0vgXtl+
PjcHF1+NI3AcDyUwBR3TZ6rV8KmlXk8yb50kYAWbwzVBBY8deKxx6jAVqsmmD3i7
dq3ed3QsRMRxbVCS8jIhirr+e2IjbtNIe7phk2jq2cdio77LIg1aNzJXJISr6/h+
DEemrIbD+TcfG5bN/deoh19dAD25acOqGWYKz2PB8ObnC/3UsnwIZcHlCsA/zE+z
+DrVy7aeg1+1AHYwv2IV6IOICFrq7wmAKD+/9nJdvvOiXY2F2rOeUnwaGPTfWPq0
EEDtCO0rAgMBAAECggEAANICC2K7sLH3PRLpmHLnw9ROmwas1MCY4Molu92TWYS6
g8vevb+fBfYNaOMiZPU81QLVaCGLEYu6sadg2ke+/O46YVsVq2XLq1r6TRyxXTUG
EWvO5yvhaPFiG5VQB+/QrdNKamWNUYmqGgB0kL5C/Xu/qIfkUOdlDrgfbQfEv8y3
qph9IWUX35nUKDF5MzrT7nlafpHw64fXsxDrlwGZUJZr1tQdayMc0GJs6cnFalQH
VhZ1CfEebiWJ4JcYcrlS3MLP9jqiJsLdE6V9FNVHOF8JGyU2QHjlwRs8g/iAsNp9
BI3lCoNJrDNE4bvgSW8BxJMRaTjNjdjcBGtDpTFTwQKBgQD7LzkHs/jbgzkTETy/
z8V2PiAHSYSBtfkKOzeno8Uru4NdTZL0ruSdfNH9zvtfvEhUmqbGFUYIeLZLDetA
E15N1KzX7QMb3/X0D2L58QuE0TXzlnKDrYzfn5GF4b7Rl2zYxtooRTc1N5yokaiI
cbis2bj4zLod1FPq4enb0OEMQQKBgQD434YRk/TvbpAgPCCUWns3OA2D5iqVsdDx
d8pd0dk5GKiEEMNz5kf874xWpdW7kmp/AoKP/eFLFhqa7FhQqohnd1i6P223S3jA
NaheK3RcEZuMFBuJEaevOU5Se9NUM1MN/EPnVSgPCkurYHGOT6xaleSHCOgcokdN
gsFasf1OawKBgQCVj2KXsZNlsNaVAdh4JVBfvVH4xM9/JEjqzKOwz5ShG392WLA9
vL0nAKFQTKPkNwmiRosyuov+k1GHkvwWJPIryYw47UjCmjGqZlb6l4nSRXeoWFZL
DVUp+ar+WpHx3gXTdWOEQuJCb6B5xnDg/UWGtgSrL8tJ45kr6+QBHHhDgQKBgE86
2fO+pruS909L1RNlutRZg/P50pTVhy9Yc5RqujzzHLLuo0rChSiBGqx7HxAYDM9i
fS5aJN9CqjWoCHWl1Mcbt6OTjdpMrKSEcJWKQAEPmfV+cUWx2TBvjf+0bBLiRA6v
wO5krdwb6vskOQKVWsl77sUOkNaM0yZZ+jRldb8BAoGAMpVJkshl4tPEOSF4Df/V
m63wZdsQP/X6tqJj+spzcrE2+vr+dyZoBy/XsWsATTVctcXyFEmjvcDCQ1LO84Ax
WxwqJrZDjE25KEkBYof96+VCOeOijx/UO8IjYDSlW74MFFqHkpg/pbVLbHLcGkne
RNJCjPCWqmCTE2F26ABGVXM=
-----END PRIVATE KEY-----`
_, err = keyFile.WriteString(clientKey)
require.NoError(t, err)
keyFile.Close()
return certFile.Name(), keyFile.Name()
}
func createInvalidCert(t *testing.T) string {
t.Helper()
tmpFile, err := os.CreateTemp(t.TempDir(), "test-invalid-*.pem")
require.NoError(t, err)
_, err = tmpFile.WriteString("INVALID CERTIFICATE CONTENT")
require.NoError(t, err)
tmpFile.Close()
return tmpFile.Name()
}
func cleanupCerts(certPaths map[string]string) {
for _, path := range certPaths {
os.Remove(path)
}
}
================================================
FILE: pkg/gofr/datasource/sql/supabase.go
================================================
package sql
import (
"fmt"
"strings"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/datasource"
)
const (
supabaseDialect = "supabase"
supabaseDirectHost = "db.%s.supabase.co"
supabasePoolerHost = "aws-0-%s.pooler.supabase.co"
directPort = "5432"
sessionPoolerPort = "5432"
transactionPoolerPort = "6543"
minConnectionStringParts = 2
)
// SupabaseConfig extends DBConfig to include Supabase-specific configuration.
type SupabaseConfig struct {
*DBConfig
ConnectionType string // direct, session, transaction
ProjectRef string // Supabase project reference
Region string // Supabase region
}
// GetSupabaseConfig builds a Supabase configuration from general config.
// It extracts Supabase-specific settings from the provided configs object.
// If the database dialect is not supabase, it returns nil.
func GetSupabaseConfig(configs config.Config) *SupabaseConfig {
dbConfig := getDBConfig(configs)
if dbConfig.Dialect != supabaseDialect {
return nil
}
dbConfig.SSLMode = requireSSLMode // Enforce SSL mode for Supabase
connectionType := strings.ToLower(configs.GetOrDefault("SUPABASE_CONNECTION_TYPE", "direct"))
projectRef := configs.Get("SUPABASE_PROJECT_REF")
region := configs.GetOrDefault("SUPABASE_REGION", "")
// If a direct connection string is provided, we'll use that instead
connStr := configs.Get("DB_URL")
if connStr != "" {
projectRef = extractProjectRefFromConnStr(connStr)
}
return &SupabaseConfig{
DBConfig: dbConfig,
ConnectionType: connectionType,
ProjectRef: projectRef,
Region: region,
}
}
// NewSupabaseSQL creates a new DB instance configured for Supabase connectivity.
// It initializes a DB with Supabase-specific settings based on the provided configs.
// If Supabase dialect is not specified, it returns nil.
func NewSupabaseSQL(configs config.Config, logger datasource.Logger, metrics Metrics) *DB {
supaConfig := GetSupabaseConfig(configs)
if supaConfig == nil {
return nil
}
configureSupabaseConnection(supaConfig, logger)
return NewSQL(configs, logger, metrics)
}
// configureSupabaseConnection sets up connection parameters based on the Supabase connection type.
// It configures the host, port, and user fields of the SupabaseConfig according to the
// connection type (direct, session, or transaction) and logs debug information.
func configureSupabaseConnection(supaConfig *SupabaseConfig, logger datasource.Logger) {
connStr := supaConfig.User
if strings.HasPrefix(connStr, "postgresql://") || strings.HasPrefix(connStr, "postgres://") {
// User field might contain the full connection string
// In this case, we'll keep it as is and return (NewSQL will handle it)
return
}
if supaConfig.SSLMode != requireSSLMode {
logger.Warnf("Supabase connections require SSL. Setting DB_SSL_MODE to 'require'")
supaConfig.SSLMode = requireSSLMode
}
switch supaConfig.ConnectionType {
case "direct":
// Format: db.[PROJECT_REF].supabase.co
supaConfig.HostName = fmt.Sprintf(supabaseDirectHost, supaConfig.ProjectRef)
supaConfig.Port = directPort
logger.Debugf("Configured direct connection to Supabase at %s:%s", supaConfig.HostName, supaConfig.Port)
case "session":
// Format: postgres.[PROJECT_REF]@aws-0-[REGION].pooler.supabase.co
supaConfig.HostName = fmt.Sprintf(supabasePoolerHost, supaConfig.Region)
supaConfig.User = fmt.Sprintf("postgres.%s", supaConfig.ProjectRef)
supaConfig.Port = sessionPoolerPort
logger.Debugf("Configured session pooler connection to Supabase at %s:%s", supaConfig.HostName, supaConfig.Port)
case "transaction":
// Format: postgres.[PROJECT_REF]@aws-0-[REGION].pooler.supabase.co
supaConfig.HostName = fmt.Sprintf(supabasePoolerHost, supaConfig.Region)
supaConfig.User = fmt.Sprintf("postgres.%s", supaConfig.ProjectRef)
supaConfig.Port = transactionPoolerPort
logger.Debugf("Configured transaction pooler connection to Supabase at %s:%s", supaConfig.HostName, supaConfig.Port)
default:
logger.Warnf("Unknown Supabase connection type '%s', defaulting to direct connection", supaConfig.ConnectionType)
supaConfig.HostName = fmt.Sprintf(supabaseDirectHost, supaConfig.ProjectRef)
supaConfig.Port = directPort
}
if supaConfig.Database == "" {
supaConfig.Database = "postgres"
}
}
// extractProjectRefFromConnStr extracts the Supabase project reference from a connection string.
// Connection string format is expected to be:
// postgresql://postgres:[PASSWORD]@db.[PROJECT_REF].supabase.co:5432/postgres
// It returns the project reference, or an empty string if it cannot be extracted.
func extractProjectRefFromConnStr(connStr string) string {
// Expecting format like: postgresql://postgres:[PASSWORD]@db.[PROJECT_REF].supabase.co:5432/postgres
parts := strings.Split(connStr, "@")
if len(parts) < minConnectionStringParts {
return ""
}
hostPart := strings.Split(parts[1], ":")[0]
hostSegments := strings.Split(hostPart, ".")
// Looking for the segment between "db." and ".supabase.co"
if len(hostSegments) >= 3 && hostSegments[0] == "db" && strings.Contains(hostPart, "supabase.co") {
return hostSegments[1]
}
return ""
}
// IsSupabaseDialect checks if the provided dialect is the Supabase dialect.
// Returns true if the dialect is "supabase", false otherwise.
func IsSupabaseDialect(dialect string) bool {
return dialect == supabaseDialect
}
================================================
FILE: pkg/gofr/datasource/sql/supabase_test.go
================================================
package sql
import (
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestGetSupabaseConfig(t *testing.T) {
testCases := []struct {
name string
configs map[string]string
expected *SupabaseConfig
}{
{
name: "Complete Supabase Config",
configs: map[string]string{
"DB_DIALECT": "supabase",
"DB_HOST": "db.xyz.supabase.co",
"DB_USER": "postgres",
"DB_PASSWORD": "password",
"DB_PORT": "5432",
"DB_NAME": "postgres",
"DB_SSL_MODE": "require",
"SUPABASE_PROJECT_REF": "xyz",
"SUPABASE_CONNECTION_TYPE": "direct",
"SUPABASE_REGION": "us-east-1",
},
expected: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
HostName: "db.xyz.supabase.co",
User: "postgres",
Password: "password",
Port: "5432",
Database: "postgres",
SSLMode: "require",
MaxIdleConn: 2, // default value
MaxOpenConn: 0, // default value
},
ConnectionType: "direct",
ProjectRef: "xyz",
Region: "us-east-1",
},
},
{
name: "Non-Supabase Dialect",
configs: map[string]string{
"DB_DIALECT": "postgres",
"DB_HOST": "localhost",
},
expected: nil,
},
{
name: "With DB_URL",
configs: map[string]string{
"DB_DIALECT": "supabase",
"DB_PASSWORD": "password",
"DB_SSL_MODE": "disable", // should be overridden to require
"DB_URL": "postgresql://postgres:password@db.xyz123.supabase.co:5432/postgres",
},
expected: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
Password: "password",
SSLMode: "require", // should be overridden
MaxIdleConn: 2, // default value
MaxOpenConn: 0, // default value
},
ConnectionType: "direct", // default
ProjectRef: "xyz123",
Region: "",
},
},
{
name: "With Default Values",
configs: map[string]string{
"DB_DIALECT": "supabase",
"SUPABASE_PROJECT_REF": "abc",
},
expected: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
SSLMode: "require",
MaxIdleConn: 2, // default value
MaxOpenConn: 0, // default value
},
ConnectionType: "direct", // default
ProjectRef: "abc",
Region: "",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockConfig := config.NewMockConfig(tc.configs)
result := GetSupabaseConfig(mockConfig)
if tc.expected == nil {
assert.Nil(t, result)
} else {
assert.NotNil(t, result)
assert.Equal(t, tc.expected.DBConfig.Dialect, result.DBConfig.Dialect)
assert.Equal(t, tc.expected.DBConfig.SSLMode, result.DBConfig.SSLMode)
assert.Equal(t, tc.expected.ConnectionType, result.ConnectionType)
assert.Equal(t, tc.expected.ProjectRef, result.ProjectRef)
assert.Equal(t, tc.expected.Region, result.Region)
}
})
}
}
func TestConfigureSupabaseConnection(t *testing.T) {
testCases := getSupabaseConnectionTestCases()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
configureSupabaseConnection(tc.config, mockLogger)
})
assertSupabaseConnectionConfig(t, &tc, logs)
})
}
}
func getSupabaseConnectionTestCases() []struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return []struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
getDirectConnectionCase(),
getSessionPoolerCase(),
getTransactionPoolerCase(),
getUnknownConnectionCase(),
getEmptyDBNameCase(),
getNonRequireSSLCase(),
}
}
func getDirectConnectionCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Direct Connection",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "require",
},
ConnectionType: "direct",
ProjectRef: "xyz",
},
expectedHost: "db.xyz.supabase.co",
expectedPort: "5432",
expectedUser: "postgres",
expectedSSLMode: "require",
logContains: "Configured direct connection to Supabase",
}
}
func getSessionPoolerCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Session Pooler Connection",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "require",
},
ConnectionType: "session",
ProjectRef: "xyz",
Region: "us-east-1",
},
expectedHost: "aws-0-us-east-1.pooler.supabase.co",
expectedPort: "5432",
expectedUser: "postgres.xyz",
expectedSSLMode: "require",
logContains: "Configured session pooler connection to Supabase",
}
}
func getTransactionPoolerCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Transaction Pooler Connection",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "require",
},
ConnectionType: "transaction",
ProjectRef: "xyz",
Region: "us-east-1",
},
expectedHost: "aws-0-us-east-1.pooler.supabase.co",
expectedPort: "6543",
expectedUser: "postgres.xyz",
expectedSSLMode: "require",
logContains: "Configured transaction pooler connection to Supabase",
}
}
func getUnknownConnectionCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Unknown Connection Type",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "require",
},
ConnectionType: "unknown",
ProjectRef: "xyz",
},
expectedHost: "db.xyz.supabase.co",
expectedPort: "5432",
expectedUser: "postgres",
expectedSSLMode: "require",
logContains: "Unknown Supabase connection type 'unknown', defaulting to direct connection",
}
}
func getEmptyDBNameCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Default Database For Empty Database Name",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "require",
Database: "",
},
ConnectionType: "direct",
ProjectRef: "xyz",
},
expectedHost: "db.xyz.supabase.co",
expectedPort: "5432",
expectedUser: "postgres",
expectedSSLMode: "require",
logContains: "Configured direct connection to Supabase",
}
}
func getNonRequireSSLCase() struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
} {
return struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}{
name: "Direct Connection With Non-Require SSL Mode",
config: &SupabaseConfig{
DBConfig: &DBConfig{
Dialect: "supabase",
User: "postgres",
Password: "password",
SSLMode: "disable",
},
ConnectionType: "direct",
ProjectRef: "xyz",
},
expectedHost: "db.xyz.supabase.co",
expectedPort: "5432",
expectedUser: "postgres",
expectedSSLMode: "require", // Should be forced to require
logContains: "Supabase connections require SSL. Setting DB_SSL_MODE to 'require'",
}
}
func assertSupabaseConnectionConfig(t *testing.T, tc *struct {
name string
config *SupabaseConfig
expectedHost string
expectedPort string
expectedUser string
expectedSSLMode string
logContains string
}, logs string) {
t.Helper()
assert.Equal(t, tc.expectedHost, tc.config.DBConfig.HostName)
assert.Equal(t, tc.expectedPort, tc.config.DBConfig.Port)
assert.Equal(t, tc.expectedSSLMode, tc.config.DBConfig.SSLMode)
if tc.expectedUser != tc.config.DBConfig.User {
assert.Equal(t, tc.expectedUser, tc.config.DBConfig.User)
}
assert.Contains(t, logs, tc.logContains)
}
func TestExtractProjectRefFromConnStr(t *testing.T) {
testCases := []struct {
name string
connStr string
expectedRef string
}{
{
name: "Valid Supabase Connection String",
connStr: "postgresql://postgres:password@db.abc123.supabase.co:5432/postgres",
expectedRef: "abc123",
},
{
name: "Valid Connection String With Extra Parts",
connStr: "postgresql://postgres:password@db.xyz789.supabase.co:5432/postgres?sslmode=require",
expectedRef: "xyz789",
},
{
name: "Invalid Format - No @ Symbol",
connStr: "postgresql://postgres:password-db.abc123.supabase.co:5432/postgres",
expectedRef: "",
},
{
name: "Invalid Format - No db. Prefix",
connStr: "postgresql://postgres:password@wrongprefix.abc123.supabase.co:5432/postgres",
expectedRef: "",
},
{
name: "Invalid Format - Not Supabase Domain",
connStr: "postgresql://postgres:password@db.abc123.postgresql.com:5432/postgres",
expectedRef: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := extractProjectRefFromConnStr(tc.connStr)
assert.Equal(t, tc.expectedRef, result)
})
}
}
func TestNewSupabaseSQL(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
testCases := []struct {
name string
configs map[string]string
expectNil bool
logContains string
}{
{
name: "Valid Supabase Config",
configs: map[string]string{
"DB_DIALECT": "supabase",
"DB_USER": "postgres",
"DB_PASSWORD": "password",
"DB_SSL_MODE": "require",
"SUPABASE_PROJECT_REF": "abc123",
"SUPABASE_CONNECTION_TYPE": "direct",
},
expectNil: false,
logContains: "Configured direct connection to Supabase",
},
{
name: "Non-Supabase Dialect",
configs: map[string]string{
"DB_DIALECT": "postgres",
"DB_HOST": "localhost",
},
expectNil: true,
logContains: "",
},
{
name: "Empty DB_HOST with automatic configuration",
configs: map[string]string{
"DB_DIALECT": "supabase",
"DB_USER": "postgres",
"DB_PASSWORD": "password",
"SUPABASE_PROJECT_REF": "abc123",
"SUPABASE_CONNECTION_TYPE": "direct",
},
expectNil: false,
logContains: "connecting to 'postgres' user to 'postgres' database at 'db.abc123.supabase.co:5432'",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mockConfig := config.NewMockConfig(tc.configs)
mockMetrics := NewMockMetrics(ctrl)
// We expect metrics to be set regardless of the result
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
logs := testutil.StdoutOutputForFunc(func() {
mockLogger := logging.NewMockLogger(logging.DEBUG)
result := NewSupabaseSQL(mockConfig, mockLogger, mockMetrics)
if tc.expectNil {
assert.Nil(t, result)
} else {
assert.NotNil(t, result)
}
})
if tc.logContains != "" {
assert.Contains(t, logs, tc.logContains)
}
})
}
}
func TestIsSupabaseDialect(t *testing.T) {
assert.True(t, IsSupabaseDialect("supabase"))
assert.False(t, IsSupabaseDialect("postgres"))
assert.False(t, IsSupabaseDialect("mysql"))
assert.False(t, IsSupabaseDialect(""))
}
func TestSupabaseWithConnectionString(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Test that full connection strings are preserved
mockConfig := config.NewMockConfig(map[string]string{
"DB_DIALECT": "supabase",
"DB_USER": "postgresql://postgres:password@db.abc123.supabase.co:5432/postgres",
"DB_URL": "postgresql://postgres:password@db.xyz789.supabase.co:5432/postgres",
"SUPABASE_PROJECT_REF": "should-be-ignored", // Should extract from connection string instead
})
mockLogger := logging.NewMockLogger(logging.DEBUG)
mockMetrics := NewMockMetrics(ctrl)
// We expect metrics to be set
mockMetrics.EXPECT().SetGauge(gomock.Any(), gomock.Any()).AnyTimes()
supaConfig := GetSupabaseConfig(mockConfig)
assert.NotNil(t, supaConfig)
// When DB_USER contains a full connection string, it should be preserved
assert.Equal(t, "postgresql://postgres:password@db.abc123.supabase.co:5432/postgres", supaConfig.DBConfig.User)
// Project ref should be extracted from the connection string
assert.Equal(t, "xyz789", supaConfig.ProjectRef)
logs := testutil.StdoutOutputForFunc(func() {
configureSupabaseConnection(supaConfig, mockLogger)
})
// Should not modify the connection string
assert.Equal(t, "postgresql://postgres:password@db.abc123.supabase.co:5432/postgres", supaConfig.DBConfig.User)
// No host/port configuration should happen
assert.NotContains(t, logs, "Configured direct connection to Supabase")
}
================================================
FILE: pkg/gofr/datasource/surrealdb/go.mod
================================================
module gofr.dev/pkg/gofr/datasource/surrealdb
go 1.25.0
require (
github.com/stretchr/testify v1.11.1
github.com/surrealdb/surrealdb.go v1.3.0
go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.uber.org/mock v0.6.0
)
require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: pkg/gofr/datasource/surrealdb/go.sum
================================================
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lxzan/gws v1.8.9 h1:VU3SGUeWlQrEwfUSfokcZep8mdg/BrUF+y73YYshdBM=
github.com/lxzan/gws v1.8.9/go.mod h1:d9yHaR1eDTBHagQC6KY7ycUOaz5KWeqQtP3xu7aMK8Y=
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/surrealdb/surrealdb.go v1.3.0 h1:/ccM9zQnx+SXYjQh1eFxcc0UagDd3VDHNUzmbyU/QEc=
github.com/surrealdb/surrealdb.go v1.3.0/go.mod h1:ju3vn9OHXde9Ulvc7/fP9I8ylkiapOdBSdrEs2PmTtA=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: pkg/gofr/datasource/surrealdb/interface.go
================================================
package surrealdb
import (
"context"
"github.com/surrealdb/surrealdb.go"
)
// DB defines the interface for SurrealDB database operations.
// It wraps the underlying surrealdb.DB to enable testing and mocking.
type DB interface {
// Use sets the namespace and database to use.
Use(ctx context.Context, namespace, database string) error
// SignIn authenticates a user.
SignIn(ctx context.Context, auth *surrealdb.Auth) (string, error)
// Info retrieves information about the current session.
Info(ctx context.Context) (any, error)
}
// Logger defines methods for logging debug, log, and error messages.
type Logger interface {
// Debugf logs a formatted debug message.
Debugf(pattern string, args ...any)
// Debug logs a debug message
Debug(args ...any)
// Logf logs a formatted log message.
Logf(pattern string, args ...any)
// Errorf logs a formatted error message
Errorf(pattern string, args ...any)
}
// Metrics provides methods to record and manage application metrics.
type Metrics interface {
// NewCounter creates a new counter metric.
NewCounter(name, desc string)
// NewUpDownCounter creates a new up-down counter metric.
NewUpDownCounter(name, desc string)
// NewHistogram creates a new histogram metric with specified buckets.
NewHistogram(name, desc string, buckets ...float64)
// NewGauge creates a new gauge metric.
NewGauge(name, desc string)
// IncrementCounter increments a counter by 1.
IncrementCounter(ctx context.Context, name string, labels ...string)
// DeltaUpDownCounter updates a delta for an up-down counter.
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
// RecordHistogram records a value in a histogram.
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
// SetGauge sets the value of a gauge.
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/datasource/surrealdb/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=surrealdb
//
// Package surrealdb is a generated GoMock package.
package surrealdb
import (
context "context"
reflect "reflect"
surrealdb "github.com/surrealdb/surrealdb.go"
gomock "go.uber.org/mock/gomock"
)
// MockDB is a mock of DB interface.
type MockDB struct {
ctrl *gomock.Controller
recorder *MockDBMockRecorder
isgomock struct{}
}
// MockDBMockRecorder is the mock recorder for MockDB.
type MockDBMockRecorder struct {
mock *MockDB
}
// NewMockDB creates a new mock instance.
func NewMockDB(ctrl *gomock.Controller) *MockDB {
mock := &MockDB{ctrl: ctrl}
mock.recorder = &MockDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDB) EXPECT() *MockDBMockRecorder {
return m.recorder
}
// Info mocks base method.
func (m *MockDB) Info(ctx context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Info", ctx)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Info indicates an expected call of Info.
func (mr *MockDBMockRecorder) Info(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Info", reflect.TypeOf((*MockDB)(nil).Info), ctx)
}
// SignIn mocks base method.
func (m *MockDB) SignIn(ctx context.Context, auth *surrealdb.Auth) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SignIn", ctx, auth)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// SignIn indicates an expected call of SignIn.
func (mr *MockDBMockRecorder) SignIn(ctx, auth any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignIn", reflect.TypeOf((*MockDB)(nil).SignIn), ctx, auth)
}
// Use mocks base method.
func (m *MockDB) Use(ctx context.Context, namespace, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Use", ctx, namespace, database)
ret0, _ := ret[0].(error)
return ret0
}
// Use indicates an expected call of Use.
func (mr *MockDBMockRecorder) Use(ctx, namespace, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Use", reflect.TypeOf((*MockDB)(nil).Use), ctx, namespace, database)
}
// MockLogger is a mock of Logger interface.
type MockLogger struct {
ctrl *gomock.Controller
recorder *MockLoggerMockRecorder
isgomock struct{}
}
// MockLoggerMockRecorder is the mock recorder for MockLogger.
type MockLoggerMockRecorder struct {
mock *MockLogger
}
// NewMockLogger creates a new mock instance.
func NewMockLogger(ctrl *gomock.Controller) *MockLogger {
mock := &MockLogger{ctrl: ctrl}
mock.recorder = &MockLoggerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockLogger) EXPECT() *MockLoggerMockRecorder {
return m.recorder
}
// Debug mocks base method.
func (m *MockLogger) Debug(args ...any) {
m.ctrl.T.Helper()
varargs := []any{}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debug", varargs...)
}
// Debug indicates an expected call of Debug.
func (mr *MockLoggerMockRecorder) Debug(args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debug", reflect.TypeOf((*MockLogger)(nil).Debug), args...)
}
// Debugf mocks base method.
func (m *MockLogger) Debugf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Debugf", varargs...)
}
// Debugf indicates an expected call of Debugf.
func (mr *MockLoggerMockRecorder) Debugf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Debugf", reflect.TypeOf((*MockLogger)(nil).Debugf), varargs...)
}
// Errorf mocks base method.
func (m *MockLogger) Errorf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Errorf", varargs...)
}
// Errorf indicates an expected call of Errorf.
func (mr *MockLoggerMockRecorder) Errorf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Errorf", reflect.TypeOf((*MockLogger)(nil).Errorf), varargs...)
}
// Logf mocks base method.
func (m *MockLogger) Logf(pattern string, args ...any) {
m.ctrl.T.Helper()
varargs := []any{pattern}
for _, a := range args {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "Logf", varargs...)
}
// Logf indicates an expected call of Logf.
func (mr *MockLoggerMockRecorder) Logf(pattern any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{pattern}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Logf", reflect.TypeOf((*MockLogger)(nil).Logf), varargs...)
}
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// DeltaUpDownCounter mocks base method.
func (m *MockMetrics) DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "DeltaUpDownCounter", varargs...)
}
// DeltaUpDownCounter indicates an expected call of DeltaUpDownCounter.
func (mr *MockMetricsMockRecorder) DeltaUpDownCounter(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeltaUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).DeltaUpDownCounter), varargs...)
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
// NewCounter mocks base method.
func (m *MockMetrics) NewCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewCounter", name, desc)
}
// NewCounter indicates an expected call of NewCounter.
func (mr *MockMetricsMockRecorder) NewCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), name, desc)
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, desc)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, desc)
}
// NewHistogram mocks base method.
func (m *MockMetrics) NewHistogram(name, desc string, buckets ...float64) {
m.ctrl.T.Helper()
varargs := []any{name, desc}
for _, a := range buckets {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "NewHistogram", varargs...)
}
// NewHistogram indicates an expected call of NewHistogram.
func (mr *MockMetricsMockRecorder) NewHistogram(name, desc any, buckets ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, desc}, buckets...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewHistogram", reflect.TypeOf((*MockMetrics)(nil).NewHistogram), varargs...)
}
// NewUpDownCounter mocks base method.
func (m *MockMetrics) NewUpDownCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewUpDownCounter", name, desc)
}
// NewUpDownCounter indicates an expected call of NewUpDownCounter.
func (mr *MockMetricsMockRecorder) NewUpDownCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewUpDownCounter", reflect.TypeOf((*MockMetrics)(nil).NewUpDownCounter), name, desc)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/datasource/surrealdb/surrealdb.go
================================================
package surrealdb
import (
"context"
"errors"
"fmt"
"math"
"strings"
"time"
"github.com/surrealdb/surrealdb.go"
"github.com/surrealdb/surrealdb.go/pkg/models"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
var (
errNotConnected = errors.New("not connected to database")
errNoDatabaseInstance = errors.New("failed to connect to SurrealDB: no valid database instance")
errInvalidCredentialsConfig = errors.New("both username and password must be provided")
errNoRecord = errors.New("no record found")
errNoResult = errors.New("no result found in query response")
errUnexpectedResult = errors.New("unexpected result type: expected []any")
errQueryError = errors.New("query error")
)
const (
schemeHTTP = "http"
schemeHTTPS = "https"
schemeWS = "ws"
schemeWSS = "wss"
schemeMemory = "memory"
schemeMem = "mem"
schemeSurrealkv = "surrealkv"
statusOK = "OK"
defaultTimeout = 30 * time.Second
)
// Config represents the configuration required to connect to SurrealDB.
type Config struct {
Host string
Port int
Username string
Password string
Namespace string
Database string
TLSEnabled bool
}
// Client represents a client to interact with SurrealDB.
type Client struct {
config *Config
db DB
logger Logger
metrics Metrics
tracer trace.Tracer
}
// New creates a new Client with the provided configuration.
func New(config *Config) *Client {
return &Client{
config: config,
}
}
// UseLogger sets a custom logger for the Client.
func (c *Client) UseLogger(customlogger any) {
if l, ok := customlogger.(Logger); ok {
c.logger = l
}
}
// UseMetrics sets a custom metrics recorder for the Client.
func (c *Client) UseMetrics(metrics any) {
if m, ok := metrics.(Metrics); ok {
c.metrics = m
}
}
// UseTracer sets a custom tracer for the Client.
func (c *Client) UseTracer(tracer any) {
if t, ok := tracer.(trace.Tracer); ok {
c.tracer = t
}
}
// newDB creates a new SurrealDB client using v1.0.0 API.
func newDB(ctx context.Context, connectionURL string) (DB, error) {
// Use the new FromEndpointURLString which handles both HTTP and WebSocket connections
db, err := surrealdb.FromEndpointURLString(ctx, connectionURL)
if err != nil {
return nil, fmt.Errorf("failed to create SurrealDB client: %w", err)
}
return NewDBWrapper(db), nil
}
// Connect establishes a connection to the SurrealDB database.
func (c *Client) Connect() {
c.logger.Debugf("connecting to SurrealDB at %v:%v to database %v", c.config.Host, c.config.Port, c.config.Database)
surrealDBBuckets := []float64{.05, .075, .1, .125, .15, .2, .3, .5, .75, 1, 2, 3, 4, 5, 7.5, 10}
c.metrics.NewHistogram("app_surrealdb_stats", "Response time of SurrealDB operations in microseconds.", surrealDBBuckets...)
c.metrics.NewGauge("app_surrealdb_open_connections", "Number of open SurrealDB connections.")
endpoint := c.buildEndpoint()
// Create context with timeout for connection
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()
err := c.connectToDatabase(ctx, endpoint)
if err != nil {
return
}
err = c.setupNamespaceAndDatabase(ctx)
if err != nil {
return
}
err = c.authenticateCredentials(ctx)
if err != nil {
return
}
c.logger.Logf("Successfully connected to SurrealDB at %v:%v to database %v", c.config.Host, c.config.Port, c.config.Database)
}
// buildEndpoint constructs the SurrealDB endpoint based on the configuration.
func (c *Client) buildEndpoint() string {
scheme := schemeWS
if c.config.TLSEnabled {
scheme = schemeHTTPS
}
return fmt.Sprintf("%s://%s:%d", scheme, c.config.Host, c.config.Port)
}
// connectToDatabase handles the connection to SurrealDB and returns an error if failed.
func (c *Client) connectToDatabase(ctx context.Context, endpoint string) error {
c.logger.Debugf("connecting to SurrealDB at %s", endpoint)
var err error
c.db, err = newDB(ctx, endpoint)
if err != nil {
c.logError("failed to connect to SurrealDB", err)
return err
}
if c.db == nil {
c.logError("failed to connect to SurrealDB: no valid database instance", nil)
return errNoDatabaseInstance
}
return nil
}
// setupNamespaceAndDatabase sets the namespace and database for SurrealDB.
func (c *Client) setupNamespaceAndDatabase(ctx context.Context) error {
err := c.db.Use(ctx, c.config.Namespace, c.config.Database)
if err != nil {
c.logError("unable to set the namespace and database for SurrealDB", err)
return err
}
return nil
}
// signIn is a helper method for signing in a user using v1.0.0 API.
func (c *Client) signIn(ctx context.Context, authData *surrealdb.Auth) (string, error) {
token, err := c.db.SignIn(ctx, authData)
if err != nil {
return "", err
}
return token, nil
}
// authenticateCredentials handles the authentication process if credentials are provided.
func (c *Client) authenticateCredentials(ctx context.Context) error {
if c.config.Username == "" && c.config.Password == "" {
return nil
}
if c.config.Username == "" || c.config.Password == "" {
return errInvalidCredentialsConfig
}
_, err := c.signIn(ctx, &surrealdb.Auth{
Username: c.config.Username,
Password: c.config.Password,
})
if err != nil {
c.logError("failed to sign in to SurrealDB", err)
return err
}
return nil
}
// logError is a helper function to log errors.
func (c *Client) logError(message string, err error) {
if c.logger == nil {
return
}
if err != nil {
c.logger.Errorf("%s: %v", message, err)
return
}
c.logger.Errorf("%s", message)
}
// Query executes a query on the SurrealDB instance.
func (c *Client) Query(ctx context.Context, query string, vars map[string]any) ([]any, error) {
const unknown = "unknown"
table := unknown
id := unknown
if vars != nil {
if idVal, ok := vars["id"]; ok {
id = fmt.Sprintf("%v", idVal)
}
if strings.Contains(query, "type::thing") {
parts := strings.Split(query, "'")
if len(parts) > 1 {
table = parts[1]
}
}
}
logMessage := fmt.Sprintf("Fetching record with ID %q from table %q", id, table)
span := c.addTrace(ctx, "Query", query)
if c.db == nil {
return nil, errNotConnected
}
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "query",
Namespace: c.config.Namespace,
Database: c.config.Database,
Data: vars,
Span: span,
}, startTime)
// Use the new v1.0.0 Query function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
results, err := surrealdb.Query[any](ctx, dbWrapper.GetDB(), query, vars)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
if results == nil || len(*results) == 0 {
return nil, errNoResult
}
return c.processQueryResults(query, *results)
}
// processQueryResults processes query results from v1.0.0 API format.
func (c *Client) processQueryResults(query string, results []surrealdb.QueryResult[any]) ([]any, error) {
var resp []any
for _, result := range results {
if result.Error != nil {
c.logger.Errorf("query error: %v", result.Error.Message)
if !isAdministrativeOperation(query) {
return nil, fmt.Errorf("%w: %s", errQueryError, result.Error.Message)
}
continue
}
if result.Result == nil {
continue
}
if isCustomNil(result.Result) {
resp = append(resp, true)
continue
}
c.handleResultRecord(result.Result, &resp)
}
return resp, nil
}
// handleResultRecord processes a single result record and appends to response.
func (c *Client) handleResultRecord(result any, resp *[]any) {
switch res := result.(type) {
case []any:
for _, record := range res {
extracted, err := c.extractRecord(record)
if err != nil {
c.logger.Errorf("failed to extract record: %v", err)
continue
}
*resp = append(*resp, extracted)
}
case map[string]any:
// Handle single record returned as map directly (e.g., from type::thing() queries)
extracted, err := c.extractRecord(res)
if err != nil {
c.logger.Errorf("failed to extract record: %v", err)
} else {
*resp = append(*resp, extracted)
}
case map[any]any:
// Handle single record as map[any]any for compatibility
extracted, err := c.extractRecord(res)
if err != nil {
c.logger.Errorf("failed to extract record: %v", err)
} else {
*resp = append(*resp, extracted)
}
default:
*resp = append(*resp, result)
}
}
// extractRecord extracts and processes a single record into a map[string]any}.
func (c *Client) extractRecord(record any) (map[string]any, error) {
// Handle map[string]any first (SurrealDB v1.0.0 format)
if recordMap, ok := record.(map[string]any); ok {
extracted := make(map[string]any, len(recordMap))
for k, v := range recordMap {
val := c.convertValue(v)
extracted[k] = val
}
return extracted, nil
}
// Fall back to map[any]any for compatibility
if recordMap, ok := record.(map[any]any); ok {
extracted := make(map[string]any, len(recordMap))
for k, v := range recordMap {
keyStr, ok := k.(string)
if !ok {
c.logger.Errorf("non-string key encountered: %v", k)
continue
}
val := c.convertValue(v)
extracted[keyStr] = val
}
return extracted, nil
}
return nil, errUnexpectedResult
}
// convertValue handles the conversion of different numeric types and strings to appropriate Go types.
func (*Client) convertValue(v any) any {
switch val := v.(type) {
case float64:
if val > math.MaxInt || val < math.MinInt {
return nil
}
return int(val)
case uint64:
if val > math.MaxInt {
return nil
}
return int(val)
case int64:
if val > math.MaxInt || val < math.MinInt {
return nil
}
return int(val)
case string:
return val
default:
return val
}
}
// executeQuery is a helper function that encapsulates common query execution logic.
func (c *Client) executeQuery(ctx context.Context, operation, entity, query string) error {
span := c.addTrace(ctx, operation, query)
if c.db == nil {
return errNotConnected
}
logMessage := fmt.Sprintf("%s %q", operation, entity)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: strings.ToLower(operation),
Namespace: c.config.Namespace,
Database: c.config.Database,
Span: span,
}, startTime)
_, err := c.Query(ctx, query, nil)
return err
}
// CreateNamespace creates a new namespace in the SurrealDB instance.
func (c *Client) CreateNamespace(ctx context.Context, namespace string) error {
query := fmt.Sprintf("DEFINE NAMESPACE %s;", namespace)
return c.executeQuery(ctx, "Creating", namespace, query)
}
// CreateDatabase creates a new database in the SurrealDB instance.
func (c *Client) CreateDatabase(ctx context.Context, database string) error {
query := fmt.Sprintf("DEFINE DATABASE %s;", database)
return c.executeQuery(ctx, "Creating", database, query)
}
// DropNamespace deletes a namespace from the SurrealDB instance.
func (c *Client) DropNamespace(ctx context.Context, namespace string) error {
query := fmt.Sprintf("REMOVE NAMESPACE %s;", namespace)
return c.executeQuery(ctx, "Dropping", namespace, query)
}
// DropDatabase deletes a database from the SurrealDB instance.
func (c *Client) DropDatabase(ctx context.Context, database string) error {
query := fmt.Sprintf("REMOVE DATABASE %s;", database)
return c.executeQuery(ctx, "Dropping", database, query)
}
// Select retrieves all records from the specified table in the SurrealDB database.
func (c *Client) Select(ctx context.Context, table string) ([]map[string]any, error) {
query := fmt.Sprintf("SELECT * FROM %s", table)
span := c.addTrace(ctx, "Select", query)
if c.db == nil {
return nil, errNotConnected
}
logMessage := fmt.Sprintf("Fetching all records from table %q", table)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "select",
Namespace: c.config.Namespace,
Database: c.config.Database,
Collection: table,
Span: span,
}, startTime)
// Use the new v1.0.0 Select function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
results, err := surrealdb.Select[[]map[string]any](ctx, dbWrapper.GetDB(), models.Table(table))
if err != nil {
return nil, fmt.Errorf("select operation failed: %w", err)
}
if results == nil {
return []map[string]any{}, nil
}
return *results, nil
}
// Create creates a new record into the specified table in the database.
func (c *Client) Create(ctx context.Context, table string, data any) (map[string]any, error) {
query := fmt.Sprintf("CREATE INTO %s SET", table)
span := c.addTrace(ctx, "Create", query)
if c.db == nil {
return nil, errNotConnected
}
logMessage := fmt.Sprintf("Creating new record in table %q", table)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "create",
Namespace: c.config.Namespace,
Database: c.config.Database,
Collection: table,
Data: data,
Span: span,
}, startTime)
// Use the new v1.0.0 Create function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
result, err := surrealdb.Create[map[string]any](ctx, dbWrapper.GetDB(), models.Table(table), data)
if err != nil {
return nil, fmt.Errorf("create operation failed: %w", err)
}
if result == nil {
return nil, errNoRecord
}
return *result, nil
}
// Update modifies an existing record in the specified table using a generic MERGE update.
func (c *Client) Update(ctx context.Context, table, id string, data any) (any, error) {
if c.db == nil {
return nil, errNotConnected
}
recordID := models.RecordID{
Table: table,
ID: id,
}
span := c.addTrace(ctx, "Update", fmt.Sprintf("%s:%s", table, id))
logMessage := fmt.Sprintf("Updating record with ID %q in table %q", id, table)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "update",
Namespace: c.config.Namespace,
Database: c.config.Database,
Collection: table,
Data: data,
Span: span,
}, startTime)
// Use the new v1.0.0 Update function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
result, err := surrealdb.Update[map[string]any](ctx, dbWrapper.GetDB(), recordID, data)
if err != nil {
return nil, fmt.Errorf("update operation failed: %w", err)
}
if result == nil {
return nil, errNoRecord
}
return *result, nil
}
// Insert inserts a new record into the specified table in SurrealDB.
func (c *Client) Insert(ctx context.Context, table string, data any) ([]map[string]any, error) {
query := fmt.Sprintf("INSERT INTO %s", table)
span := c.addTrace(ctx, "Insert", query)
if c.db == nil {
return nil, errNotConnected
}
logMessage := fmt.Sprintf("Inserting record to table %q", table)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "insert",
Namespace: c.config.Namespace,
Database: c.config.Database,
Collection: table,
Data: data,
Span: span,
}, startTime)
// Use the new v1.0.0 Insert function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
results, err := surrealdb.Insert[map[string]any](ctx, dbWrapper.GetDB(), models.Table(table), data)
if err != nil {
return nil, fmt.Errorf("insert operation failed: %w", err)
}
if results == nil {
return []map[string]any{}, nil
}
return *results, nil
}
// Delete removes a record from the specified table in SurrealDB.
func (c *Client) Delete(ctx context.Context, table, id string) (any, error) {
query := fmt.Sprintf("DELETE FROM %s:%s RETURN BEFORE;", table, id)
span := c.addTrace(ctx, "Delete", query)
if c.db == nil {
return nil, errNotConnected
}
logMessage := fmt.Sprintf("Deleting record with ID %q in table %q", id, table)
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "delete",
Namespace: c.config.Namespace,
Database: c.config.Database,
Collection: table,
ID: id,
Span: span,
}, startTime)
// Use the new v1.0.0 Delete function
recordID := models.RecordID{
Table: table,
ID: id,
}
// Use the new v1.0.0 Delete function with the wrapped DB
dbWrapper := c.db.(*DBWrapper)
result, err := surrealdb.Delete[map[string]any](ctx, dbWrapper.GetDB(), recordID)
if err != nil {
return nil, fmt.Errorf("delete operation failed: %w", err)
}
if result == nil {
return nil, nil
}
return *result, nil
}
// addTrace starts a new trace span for the specified method and query.
func (c *Client) addTrace(ctx context.Context, method, query string) trace.Span {
if c.tracer == nil {
return nil
}
_, span := c.tracer.Start(ctx, fmt.Sprintf("SurrealDB.%v", method))
span.SetAttributes(
attribute.String("surrealdb.query", query),
attribute.String("surrealdb.namespace", c.config.Namespace),
attribute.String("surrealdb.database", c.config.Database),
)
return span
}
func (c *Client) sendOperationStats(ql *QueryLog, startTime time.Time) {
duration := time.Since(startTime).Microseconds()
ql.Duration = duration
c.logger.Debug(ql)
ql.Namespace = c.config.Namespace
ql.Database = c.config.Database
c.metrics.RecordHistogram(context.Background(), "app_surrealdb_stats", float64(duration),
"namespace", ql.Namespace,
"database", ql.Database,
"operation", ql.OperationName)
var nbConnection float64
if c.db != nil {
nbConnection = 1
}
c.metrics.SetGauge("app_surrealdb_open_connections", nbConnection)
if ql.Span == nil {
return
}
defer ql.Span.End()
ql.Span.SetAttributes(
attribute.Int64("surrealdb.duration", duration),
attribute.String("surrealdb.query", ql.Query),
attribute.String("surrealdb.operation", ql.OperationName),
attribute.String("surrealdb.namespace", ql.Namespace),
attribute.String("surrealdb.database", ql.Database),
attribute.String("surrealdb.collection", ql.Collection),
)
}
type Health struct {
Status string `json:"status,omitempty"`
Details map[string]any `json:"details,omitempty"`
}
func (c *Client) HealthCheck(ctx context.Context) (any, error) {
const (
statusDown = "DOWN"
statusUP = "UP"
)
logMessage := fmt.Sprintf("Database health at \"%s:%d\"", c.config.Host, c.config.Port)
span := c.addTrace(ctx, "HealthCheck", "info")
startTime := time.Now()
defer c.sendOperationStats(&QueryLog{
Query: logMessage,
OperationName: "health_check",
Namespace: c.config.Namespace,
Database: c.config.Database,
Span: span,
}, startTime)
h := Health{
Details: make(map[string]any),
}
h.Details["host"] = fmt.Sprintf("%s:%d", c.config.Host, c.config.Port)
h.Details["namespace"] = c.config.Namespace
h.Details["database"] = c.config.Database
if c.db == nil {
h.Status = statusDown
h.Details["error"] = "Database client is not connected"
return &h, errNotConnected
}
// Use the new v1.0.0 Info method
if _, err := c.db.Info(ctx); err != nil {
h.Status = statusDown
h.Details["error"] = fmt.Sprintf("Connection test failed: %v", err)
h.Details["connection_state"] = "failed"
return &h, err
}
if err := c.db.Use(ctx, c.config.Namespace, c.config.Database); err != nil {
h.Status = statusDown
h.Details["error"] = fmt.Sprintf("Database access verification failed: %v", err)
h.Details["connection_state"] = "connected_but_access_failed"
return &h, err
}
h.Status = statusUP
h.Details["message"] = "Database is healthy"
h.Details["connection_state"] = "fully_connected"
return &h, nil
}
================================================
FILE: pkg/gofr/datasource/surrealdb/surrealdb_test.go
================================================
package surrealdb
import (
"context"
"math"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
)
// Test_NewClient verifies that a new client is created with the provided configuration.
func Test_NewClient(t *testing.T) {
config := &Config{
Host: "localhost",
Port: 8000,
Username: "root",
Password: "root",
Namespace: "test_namespace",
Database: "test_database",
TLSEnabled: false,
}
client := New(config)
assert.NotNil(t, client)
assert.Equal(t, config, client.config)
assert.Nil(t, client.db)
}
// Test_UseLogger verifies that a custom logger can be set.
func Test_UseLogger(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := New(&Config{})
mockLogger := NewMockLogger(ctrl)
client.UseLogger(mockLogger)
assert.NotNil(t, client.logger)
}
// Test_UseMetrics verifies that custom metrics can be set.
func Test_UseMetrics(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := New(&Config{})
mockMetrics := NewMockMetrics(ctrl)
client.UseMetrics(mockMetrics)
assert.NotNil(t, client.metrics)
}
// Test_UseTracer verifies that a custom tracer can be set.
func Test_UseTracer(t *testing.T) {
client := New(&Config{})
t.Run("valid tracer", func(t *testing.T) {
tracer := otel.GetTracerProvider().Tracer("test-tracer")
client.UseTracer(tracer)
assert.NotNil(t, client.tracer)
})
t.Run("invalid tracer type", func(t *testing.T) {
originalTracer := client.tracer
client.UseTracer("invalid")
assert.Equal(t, originalTracer, client.tracer)
})
}
// Test_extractRecord verifies the extraction and conversion of record data.
func Test_extractRecord(t *testing.T) {
client := &Client{
logger: NewMockLogger(gomock.NewController(t)),
}
t.Run("extract map[string]any record", func(t *testing.T) {
record := map[string]any{
"id": "user:1",
"name": "John",
"age": float64(30),
"email": "john@example.com",
}
result, err := client.extractRecord(record)
require.NoError(t, err)
assert.Equal(t, "user:1", result["id"])
assert.Equal(t, "John", result["name"])
assert.Equal(t, 30, result["age"])
assert.Equal(t, "john@example.com", result["email"])
})
t.Run("extract map[any]any record", func(t *testing.T) {
record := map[any]any{
"id": "user:2",
"name": "Jane",
"age": float64(28),
"email": "jane@example.com",
}
result, err := client.extractRecord(record)
require.NoError(t, err)
assert.Equal(t, "user:2", result["id"])
assert.Equal(t, "Jane", result["name"])
assert.Equal(t, 28, result["age"])
assert.Equal(t, "jane@example.com", result["email"])
})
t.Run("extract with numeric conversions", func(t *testing.T) {
record := map[string]any{
"float64_val": float64(42),
"uint64_val": uint64(99),
"int64_val": int64(77),
"string_val": "test",
}
result, err := client.extractRecord(record)
require.NoError(t, err)
assert.Equal(t, 42, result["float64_val"])
assert.Equal(t, 99, result["uint64_val"])
assert.Equal(t, 77, result["int64_val"])
assert.Equal(t, "test", result["string_val"])
})
t.Run("extract invalid record type", func(t *testing.T) {
record := "invalid record"
_, err := client.extractRecord(record)
require.ErrorIs(t, err, errUnexpectedResult)
})
t.Run("extract nil record", func(t *testing.T) {
_, err := client.extractRecord(nil)
require.ErrorIs(t, err, errUnexpectedResult)
})
}
// Test_handleResultRecord verifies processing of different result types.
func Test_handleResultRecord(t *testing.T) {
client := &Client{
logger: NewMockLogger(gomock.NewController(t)),
}
t.Run("handle array of records", func(t *testing.T) {
result := []any{
map[string]any{"id": "1", "name": "Alice"},
map[string]any{"id": "2", "name": "Bob"},
}
var resp []any
client.handleResultRecord(result, &resp)
require.Len(t, resp, 2)
assert.Equal(t, "Alice", resp[0].(map[string]any)["name"])
assert.Equal(t, "Bob", resp[1].(map[string]any)["name"])
})
t.Run("handle single record as map[string]any", func(t *testing.T) {
result := map[string]any{"id": "user:1", "name": "Charlie"}
var resp []any
client.handleResultRecord(result, &resp)
require.Len(t, resp, 1)
extracted := resp[0].(map[string]any)
assert.Equal(t, "Charlie", extracted["name"])
})
t.Run("handle single record as map[any]any", func(t *testing.T) {
result := map[any]any{"id": "user:2", "name": "Diana"}
var resp []any
client.handleResultRecord(result, &resp)
require.Len(t, resp, 1)
extracted := resp[0].(map[string]any)
assert.Equal(t, "Diana", extracted["name"])
})
t.Run("handle scalar value", func(t *testing.T) {
result := "some scalar"
var resp []any
client.handleResultRecord(result, &resp)
require.Len(t, resp, 1)
assert.Equal(t, "some scalar", resp[0])
})
t.Run("handle boolean value", func(t *testing.T) {
result := true
var resp []any
client.handleResultRecord(result, &resp)
require.Len(t, resp, 1)
assert.Equal(t, true, resp[0])
})
}
// Test_convertValue verifies numeric type conversions.
func Test_convertValue(t *testing.T) {
client := &Client{}
t.Run("float64 conversion", func(t *testing.T) {
tests := []struct {
name string
input float64
expected any
}{
{"valid float64", 42.0, 42},
{"too large float64", math.MaxFloat64, nil},
{"too small float64", -math.MaxFloat64, nil},
{"zero", 0.0, 0},
{"negative", -5.0, -5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := client.convertValue(tt.input)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("uint64 conversion", func(t *testing.T) {
tests := []struct {
name string
input uint64
expected any
}{
{"valid uint64", uint64(42), 42},
{"too large uint64", uint64(math.MaxInt + 1), nil},
{"zero", uint64(0), 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := client.convertValue(tt.input)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("int64 conversion", func(t *testing.T) {
tests := []struct {
name string
input int64
expected any
}{
{"valid int64", int64(42), 42},
{"max boundary", int64(math.MaxInt), int(math.MaxInt)},
{"min boundary", int64(math.MinInt), int(math.MinInt)},
{"zero", int64(0), 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := client.convertValue(tt.input)
assert.Equal(t, tt.expected, result)
})
}
})
t.Run("string conversion", func(t *testing.T) {
input := "test string"
result := client.convertValue(input)
assert.Equal(t, input, result)
})
t.Run("default case", func(t *testing.T) {
input := []int{1, 2, 3}
result := client.convertValue(input)
assert.Equal(t, input, result)
})
t.Run("boolean value", func(t *testing.T) {
input := true
result := client.convertValue(input)
assert.Equal(t, input, result)
})
}
// Test_NotConnectedError verifies behavior when database is not connected.
func Test_NotConnectedError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := New(&Config{})
client.UseLogger(NewMockLogger(ctrl))
client.UseMetrics(NewMockMetrics(ctrl))
t.Run("query without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Query(ctx, "SELECT * FROM users", nil)
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
t.Run("select without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Select(ctx, "users")
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
t.Run("create without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Create(ctx, "users", map[string]any{"name": "test"})
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
t.Run("update without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Update(ctx, "users", "1", map[string]any{"name": "updated"})
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
t.Run("insert without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Insert(ctx, "users", []map[string]any{{"name": "test"}})
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
t.Run("delete without connection", func(t *testing.T) {
ctx := context.Background()
result, err := client.Delete(ctx, "users", "1")
require.ErrorIs(t, err, errNotConnected)
assert.Nil(t, result)
})
}
// Test_UseDBInterface verifies that the client can use the DB interface.
func Test_UseDBInterface(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
client := New(&Config{
Namespace: "test_ns",
Database: "test_db",
})
mockDB := NewMockDB(ctrl)
client.db = mockDB
t.Run("db interface is used correctly", func(t *testing.T) {
assert.NotNil(t, client.db)
assert.Equal(t, mockDB, client.db)
})
}
// Test_ExtractRecordWithNonStringKey verifies handling of non-string keys in map[any]any.
func Test_ExtractRecordWithNonStringKey(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockLogger := NewMockLogger(ctrl)
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)
client := &Client{logger: mockLogger}
record := map[any]any{
"id": "user:1",
123: "numeric key",
"name": "John",
}
result, err := client.extractRecord(record)
require.NoError(t, err)
// Should successfully extract string keys and skip non-string keys
assert.Equal(t, "user:1", result["id"])
assert.Equal(t, "John", result["name"])
assert.NotContains(t, result, 123)
}
================================================
FILE: pkg/gofr/datasource/surrealdb/utils.go
================================================
package surrealdb
import (
"fmt"
"io"
"regexp"
"strings"
"github.com/surrealdb/surrealdb.go/pkg/models"
"go.opentelemetry.io/otel/trace"
)
var rgx = regexp.MustCompile(`\s+`)
// clean takes a string query as input and performs two operations to clean it up:
// 1. It replaces multiple consecutive whitespace characters with a single space.
// 2. It trims leading and trailing whitespace from the string.
// The cleaned-up query string is then returned.
func clean(query string) string {
// Replace multiple consecutive whitespace characters with a single space
query = rgx.ReplaceAllString(query, " ")
// Trim leading and trailing whitespace from the string
query = strings.TrimSpace(query)
return query
}
type QueryLog struct {
Query string `json:"query"` // The query executed.
OperationName string `json:"operationName"` // The operation name
Duration int64 `json:"duration"` // Execution time in microseconds.
Namespace string `json:"namespace"` // The namespace of the query.
Database string `json:"database"` // The database the query was executed on.
ID any `json:"id"` // The ID of the affected items.
Data any `json:"data"` // The data affected or retrieved.
Filter any `json:"filter,omitempty"` // Optional filter applied to the query.
Update any `json:"update,omitempty"` // Optional update data for the query.
Collection string `json:"collection,omitempty"` // Optional collection affected.\
Span trace.Span `json:"span,omitempty"` // Optional tracing span associated with the query.
}
const defaultValue = "default"
// PrettyPrint outputs a formatted string representation of the QueryLog.
func (ql *QueryLog) PrettyPrint(writer io.Writer) {
// Set default values for nil fields
if ql.Filter == nil {
ql.Filter = ""
}
if ql.ID == nil {
ql.ID = ""
}
if ql.Update == nil {
ql.Update = ""
}
if ql.Database == "" {
ql.Database = defaultValue
}
if ql.Namespace == "" {
ql.Namespace = defaultValue
}
// Format string with proper color codes and positioning
fmt.Fprintf(writer, "\u001B[38;5;8m%-32s \u001B[38;5;206m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s:%s \u001B[38;5;8m%-32s\u001B[0m\n",
clean(ql.OperationName),
"SRLDB",
ql.Duration,
ql.Database,
ql.Namespace,
clean(ql.Query),
)
}
func isAdministrativeOperation(query string) bool {
return strings.HasPrefix(query, "DEFINE") ||
strings.HasPrefix(query, "REMOVE") ||
strings.Contains(query, "NAMESPACE") ||
strings.Contains(query, "DATABASE")
}
// isCustomNil checks for CustomNil type.
func isCustomNil(result any) bool {
_, ok := result.(models.CustomNil)
return ok
}
================================================
FILE: pkg/gofr/datasource/surrealdb/wrapper.go
================================================
package surrealdb
import (
"context"
"github.com/surrealdb/surrealdb.go"
)
// DBWrapper wraps a *surrealdb.DB and implements the DB interface.
// This allows package-level generic functions to be used through the interface.
type DBWrapper struct {
db *surrealdb.DB
}
// NewDBWrapper creates a new wrapper around a surrealdb.DB instance.
func NewDBWrapper(db *surrealdb.DB) *DBWrapper {
return &DBWrapper{db: db}
}
// Use sets the namespace and database to use.
func (w *DBWrapper) Use(ctx context.Context, namespace, database string) error {
return w.db.Use(ctx, namespace, database)
}
// SignIn authenticates a user.
func (w *DBWrapper) SignIn(ctx context.Context, auth *surrealdb.Auth) (string, error) {
return w.db.SignIn(ctx, auth)
}
// Info retrieves information about the current session.
func (w *DBWrapper) Info(ctx context.Context) (any, error) {
return w.db.Info(ctx)
}
// GetDB returns the underlying *surrealdb.DB for package-level operations.
// This is used internally for Query, Select, Create, Update, Insert, Delete operations.
func (w *DBWrapper) GetDB() *surrealdb.DB {
return w.db
}
================================================
FILE: pkg/gofr/default.go
================================================
package gofr
const (
defaultHTTPPort = 8000
defaultGRPCPort = 9000
defaultMetricPort = 2121
)
================================================
FILE: pkg/gofr/exporter.go
================================================
package gofr
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"gofr.dev/pkg/gofr/logging"
)
// errUnexpectedStatusCode is returned when an unexpected status code is received from the remote endpoint.
var errUnexpectedStatusCode = errors.New("unexpected response status code")
// Exporter is responsible for exporting spans to a remote endpoint.
type Exporter struct {
endpoint string // The endpoint to which spans will be exported.
logger logging.Logger // Logger for logging errors and other messages.
}
// NewExporter creates a new Exporter instance with a custom endpoint and logger.
func NewExporter(endpoint string, logger logging.Logger) *Exporter {
return &Exporter{
endpoint: endpoint,
logger: logger,
}
}
// Span represents a span that will be exported.
type Span struct {
TraceID string `json:"traceId"` // Trace ID of the span.
ID string `json:"id"` // ID of the span.
ParentID string `json:"parentId,omitempty"` // Parent ID of the span.
Name string `json:"name"` // Name of the span.
Timestamp int64 `json:"timestamp"` // Timestamp of the span.
Duration int64 `json:"duration"` // Duration of the span.
Tags map[string]string `json:"tags,omitempty"` // Tags associated with the span.
LocalEndpoint map[string]string `json:"localEndpoint"` // Local endpoint of the span.
}
// ExportSpans exports spans to the configured remote endpoint.
func (e *Exporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error {
return e.processSpans(ctx, e.logger, spans)
}
// Shutdown shuts down the exporter.
func (*Exporter) Shutdown(context.Context) error {
return nil
}
// processSpans processes spans and exports them to the configured endpoint.
func (e *Exporter) processSpans(ctx context.Context, logger logging.Logger, spans []sdktrace.ReadOnlySpan) error {
if len(spans) == 0 {
return nil
}
convertedSpans := convertSpans(spans)
payload, err := json.Marshal(convertedSpans)
if err != nil {
return fmt.Errorf("failed to marshal spans, error: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, e.endpoint, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("failed to create HTTP request, error: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Errorf("failed to create spans, error: %v", err)
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("failed to post spans on '%v', %w: '%d'", e.endpoint, errUnexpectedStatusCode, resp.StatusCode)
}
return nil
}
// convertSpans converts OpenTelemetry spans to the format expected by the exporter.
func convertSpans(spans []sdktrace.ReadOnlySpan) []Span {
convertedSpans := make([]Span, 0, len(spans))
for i, s := range spans {
convertedSpan := Span{
TraceID: s.SpanContext().TraceID().String(),
ID: s.SpanContext().SpanID().String(),
ParentID: s.Parent().SpanID().String(),
Name: s.Name(),
Timestamp: s.StartTime().UnixNano() / int64(time.Microsecond),
Duration: s.EndTime().Sub(s.StartTime()).Nanoseconds() / int64(time.Microsecond),
Tags: make(map[string]string, len(s.Attributes())+len(s.Resource().Attributes())),
}
for _, kv := range s.Attributes() {
k, v := attributeToStringPair(kv)
convertedSpan.Tags[k] = v
}
for _, kv := range s.Resource().Attributes() {
k, v := attributeToStringPair(kv)
convertedSpan.Tags[k] = v
}
convertedSpans = append(convertedSpans, convertedSpan)
convertedSpans[i].LocalEndpoint = map[string]string{"serviceName": convertedSpans[0].Tags["service.name"]}
}
return convertedSpans
}
func attributeToStringPair(kv attribute.KeyValue) (key, value string) {
switch kv.Value.Type() {
// For slice attributes, serialize as JSON list string.
case attribute.BOOLSLICE:
data, _ := json.Marshal(kv.Value.AsBoolSlice())
return string(kv.Key), string(data)
case attribute.INT64SLICE:
data, _ := json.Marshal(kv.Value.AsInt64Slice())
return string(kv.Key), string(data)
case attribute.FLOAT64SLICE:
data, _ := json.Marshal(kv.Value.AsFloat64Slice())
return string(kv.Key), string(data)
case attribute.STRINGSLICE:
data, _ := json.Marshal(kv.Value.AsStringSlice())
return string(kv.Key), string(data)
case attribute.BOOL:
return string(kv.Key), strconv.FormatBool(kv.Value.AsBool())
case attribute.INT64:
return string(kv.Key), strconv.FormatInt(kv.Value.AsInt64(), 10)
case attribute.FLOAT64:
return string(kv.Key), strconv.FormatFloat(kv.Value.AsFloat64(), 'f', -1, 64)
case attribute.STRING:
return string(kv.Key), kv.Value.AsString()
case attribute.INVALID:
return string(kv.Key), "invalid"
default:
return string(kv.Key), kv.Value.Emit()
}
}
================================================
FILE: pkg/gofr/exporter_test.go
================================================
package gofr
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"gofr.dev/pkg/gofr/logging"
)
func Test_ExportSpans(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
logger := logging.NewLogger(logging.INFO)
exporter := NewExporter(server.URL, logger)
tests := []struct {
desc string
spans []sdktrace.ReadOnlySpan
}{
{"Empty Spans Slice", []sdktrace.ReadOnlySpan{}},
{"Success case", provideSampleSpan(t)},
}
for i, tc := range tests {
err := exporter.ExportSpans(t.Context(), tc.spans)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_ExportSpansError(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
server.Close()
exporter := NewExporter(server.URL, logging.NewLogger(logging.INFO))
err := exporter.ExportSpans(t.Context(), provideSampleSpan(t))
require.Error(t, err, "Expected error for failed request")
}
func provideSampleSpan(t *testing.T) []sdktrace.ReadOnlySpan {
t.Helper()
tp := sdktrace.NewTracerProvider()
defer func(tp *sdktrace.TracerProvider, ctx context.Context) {
err := tp.Shutdown(ctx)
if err != nil {
t.Error(err)
}
}(tp, t.Context())
otel.SetTracerProvider(tp)
tracer := otel.Tracer("test-tracer")
_, span := tracer.Start(t.Context(), "test-span")
span.End()
ro := span.(sdktrace.ReadOnlySpan)
return []sdktrace.ReadOnlySpan{ro}
}
func Test_attributeToStringPair(t *testing.T) {
tests := []struct {
name string
keyValue attribute.KeyValue
expectedKey string
expectedValue string
expectedErrMsg string
}{
{
name: "BoolSlice",
keyValue: attribute.BoolSlice("boolKey", []bool{true, false}),
expectedKey: "boolKey",
expectedValue: `[true,false]`,
expectedErrMsg: "",
},
{
name: "Int64Slice",
keyValue: attribute.Int64Slice("int64Key", []int64{1, 2, 3}),
expectedKey: "int64Key",
expectedValue: `[1,2,3]`,
expectedErrMsg: "",
},
{
name: "Float64Slice",
keyValue: attribute.Float64Slice("float64Key", []float64{1.1, 2.2, 3.3}),
expectedKey: "float64Key",
expectedValue: `[1.1,2.2,3.3]`,
expectedErrMsg: "",
},
{
name: "StringSlice",
keyValue: attribute.StringSlice("stringKey", []string{"a", "b", "c"}),
expectedKey: "stringKey",
expectedValue: `["a","b","c"]`,
expectedErrMsg: "",
},
{
name: "Bool",
keyValue: attribute.Bool("boolKey", true),
expectedKey: "boolKey",
expectedValue: "true",
expectedErrMsg: "",
},
{
name: "Int64",
keyValue: attribute.Int64("int64Key", 123),
expectedKey: "int64Key",
expectedValue: "123",
expectedErrMsg: "",
},
{
name: "Float64",
keyValue: attribute.Float64("float64Key", 1.23),
expectedKey: "float64Key",
expectedValue: "1.23",
expectedErrMsg: "",
},
{
name: "String",
keyValue: attribute.String("stringKey", "stringValue"),
expectedKey: "stringKey",
expectedValue: "stringValue",
expectedErrMsg: "",
},
}
for _, tt := range tests {
key, value := attributeToStringPair(tt.keyValue)
assert.Equal(t, tt.expectedKey, key, "Key mismatch")
assert.Equal(t, tt.expectedValue, value, "Value mismatch")
}
}
================================================
FILE: pkg/gofr/external_db.go
================================================
package gofr
import (
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource/file"
)
// AddMongo sets the Mongo datasource in the app's container.
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
}
// AddFTP sets the FTP datasource in the app's container.
// Deprecated: Use the AddFile method instead.
func (a *App) AddFTP(fs file.FileSystemProvider) {
fs.UseLogger(a.Logger())
fs.UseMetrics(a.Metrics())
fs.Connect()
a.container.File = fs
}
// AddPubSub sets the PubSub client in the app's container.
func (a *App) AddPubSub(pubsub container.PubSubProvider) {
pubsub.UseLogger(a.Logger())
pubsub.UseMetrics(a.Metrics())
pubsub.Connect()
a.container.PubSub = pubsub
}
// AddFileStore sets the FTP, SFTP, S3, GCS, or Azure File Storage datasource in the app's container.
func (a *App) AddFileStore(fs file.FileSystemProvider) {
fs.UseLogger(a.Logger())
fs.UseMetrics(a.Metrics())
fs.Connect()
a.container.File = fs
}
// AddClickhouse initializes the clickhouse client.
// Official implementation is available in the package : gofr.dev/pkg/gofr/datasource/clickhouse .
func (a *App) AddClickhouse(db container.ClickhouseProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-clickhouse")
db.UseTracer(tracer)
db.Connect()
a.container.Clickhouse = db
}
// AddOracle initializes the OracleDB client.
// Official implementation is available in the package: gofr.dev/pkg/gofr/datasource/oracle.
func (a *App) AddOracle(db container.OracleProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-oracle")
db.UseTracer(tracer)
db.Connect()
a.container.Oracle = db
}
// UseMongo sets the Mongo datasource in the app's container.
// Deprecated: Use the AddMongo method instead.
func (a *App) UseMongo(db container.Mongo) {
a.container.Mongo = db
}
// AddCassandra sets the Cassandra datasource in the app's container.
func (a *App) AddCassandra(db container.CassandraProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-cassandra")
db.UseTracer(tracer)
db.Connect()
a.container.Cassandra = db
}
// AddKVStore sets the KV-Store datasource in the app's container.
func (a *App) AddKVStore(db container.KVStoreProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-kvstore")
db.UseTracer(tracer)
db.Connect()
a.container.KVStore = db
}
// AddSolr sets the Solr datasource in the app's container.
func (a *App) AddSolr(db container.SolrProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-solr")
db.UseTracer(tracer)
db.Connect()
a.container.Solr = db
}
// AddDgraph sets the Dgraph datasource in the app's container.
func (a *App) AddDgraph(db container.DgraphProvider) {
// Create the Dgraph client with the provided configuration
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-dgraph")
db.UseTracer(tracer)
db.Connect()
a.container.DGraph = db
}
// AddOpenTSDB sets the OpenTSDB datasource in the app's container.
func (a *App) AddOpenTSDB(db container.OpenTSDBProvider) {
// Create the Opentsdb client with the provided configuration
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-opentsdb")
db.UseTracer(tracer)
db.Connect()
a.container.OpenTSDB = db
}
// AddScyllaDB sets the ScyllaDB datasource in the app's container.
func (a *App) AddScyllaDB(db container.ScyllaDBProvider) {
// Create the ScyllaDB client with the provided configuration
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-scylladb")
db.UseTracer(tracer)
db.Connect()
a.container.ScyllaDB = db
}
// AddArangoDB sets the ArangoDB datasource in the app's container.
func (a *App) AddArangoDB(db container.ArangoDBProvider) {
// Set up logger, metrics, and tracer
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
// Get tracer from OpenTelemetry
tracer := otel.GetTracerProvider().Tracer("gofr-arangodb")
db.UseTracer(tracer)
// Connect to ArangoDB
db.Connect()
// Add the ArangoDB provider to the container
a.container.ArangoDB = db
}
func (a *App) AddSurrealDB(db container.SurrealBDProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-surrealdb")
db.UseTracer(tracer)
db.Connect()
a.container.SurrealDB = db
}
func (a *App) AddElasticsearch(db container.ElasticsearchProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-elasticsearch")
db.UseTracer(tracer)
db.Connect()
a.container.Elasticsearch = db
}
func (a *App) AddCouchbase(db container.CouchbaseProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-couchbase")
db.UseTracer(tracer)
db.Connect()
a.container.Couchbase = db
}
// AddDBResolver sets up database resolver with read/write splitting.
func (a *App) AddDBResolver(resolver container.DBResolverProvider) {
// Validate primary SQL exists
if a.container.SQL == nil {
a.Logger().Fatal("Primary SQL connection must be configured before adding DBResolver")
return
}
resolver.UseLogger(a.Logger())
resolver.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-dbresolver")
resolver.UseTracer(tracer)
resolver.Connect()
// Replace the SQL connection with the resolver
a.container.SQL = resolver.GetResolver()
a.Logger().Logf("DB Resolver initialized successfully")
}
func (a *App) AddInfluxDB(db container.InfluxDBProvider) {
db.UseLogger(a.Logger())
db.UseMetrics(a.Metrics())
tracer := otel.GetTracerProvider().Tracer("gofr-influxdb")
db.UseTracer(tracer)
db.Connect()
a.container.InfluxDB = db
}
func (a *App) GetSQL() container.DB {
return a.container.SQL
}
================================================
FILE: pkg/gofr/external_db_test.go
================================================
package gofr
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource/file"
"gofr.dev/pkg/gofr/testutil"
)
func TestApp_AddKVStore(t *testing.T) {
t.Run("Adding KV-Store", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockKVStoreProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-kvstore"))
mock.EXPECT().Connect()
app.AddKVStore(mock)
assert.Equal(t, mock, app.container.KVStore)
})
}
func TestApp_AddMongo(t *testing.T) {
t.Run("Adding MongoDB", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockMongoProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(gomock.Any())
mock.EXPECT().Connect()
app.AddMongo(mock)
assert.Equal(t, mock, app.container.Mongo)
})
}
func TestApp_AddCassandra(t *testing.T) {
t.Run("Adding Cassandra", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockCassandraProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-cassandra"))
mock.EXPECT().Connect()
app.AddCassandra(mock)
assert.Equal(t, mock, app.container.Cassandra)
})
}
func TestApp_AddClickhouse(t *testing.T) {
t.Run("Adding Clickhouse", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockClickhouseProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-clickhouse"))
mock.EXPECT().Connect()
app.AddClickhouse(mock)
assert.Equal(t, mock, app.container.Clickhouse)
})
}
func TestApp_AddOracle(t *testing.T) {
t.Run("Adding OracleDB", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockOracleProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-oracle"))
mock.EXPECT().Connect()
app.AddOracle(mock)
assert.Equal(t, mock, app.container.Oracle)
})
}
func TestApp_AddFTP(t *testing.T) {
t.Run("Adding FTP", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().Connect()
app.AddFTP(mock)
assert.Equal(t, mock, app.container.File)
})
t.Run("Adding FTP", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().Connect()
app.AddFileStore(mock)
assert.Equal(t, mock, app.container.File)
})
}
func TestApp_AddS3(t *testing.T) {
t.Run("Adding S3", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := file.NewMockFileSystemProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().Connect()
app.AddFileStore(mock)
assert.Equal(t, mock, app.container.File)
})
}
func TestApp_AddOpenTSDB(t *testing.T) {
t.Run("Adding OpenTSDB", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockOpenTSDBProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(gomock.Any())
mock.EXPECT().Connect()
app.AddOpenTSDB(mock)
assert.Equal(t, mock, app.container.OpenTSDB)
})
}
func TestApp_AddScyllaDB(t *testing.T) {
t.Run("Adding ScyllaDB", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockScyllaDBProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(gomock.Any())
mock.EXPECT().Connect()
app.AddScyllaDB(mock)
assert.Equal(t, mock, app.container.ScyllaDB)
})
}
func TestApp_AddArangoDB(t *testing.T) {
t.Run("Adding ArangoDB", func(t *testing.T) {
port := testutil.GetFreePort(t)
t.Setenv("METRICS_PORT", strconv.Itoa(port))
app := New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mock := container.NewMockArangoDBProvider(ctrl)
mock.EXPECT().UseLogger(app.Logger())
mock.EXPECT().UseMetrics(app.Metrics())
mock.EXPECT().UseTracer(otel.GetTracerProvider().Tracer("gofr-arangodb"))
mock.EXPECT().Connect()
app.AddArangoDB(mock)
assert.Equal(t, mock, app.container.ArangoDB)
})
}
================================================
FILE: pkg/gofr/factory.go
================================================
package gofr
import (
"os"
"path/filepath"
"strconv"
"gofr.dev/pkg/gofr/cmd/terminal"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/http/middleware"
"gofr.dev/pkg/gofr/logging"
)
// New creates an HTTP Server Application and returns that App.
func New() *App {
app := &App{}
app.readConfig(false)
app.container = container.NewContainer(app.Config)
app.initTracer()
app.initMetricsServer()
// HTTP Server
port, err := strconv.Atoi(app.Config.Get("HTTP_PORT"))
if err != nil || port <= 0 {
port = defaultHTTPPort
}
app.httpServer = newHTTPServer(app.container, port, middleware.GetConfigs(app.Config))
app.httpServer.certFile = app.Config.GetOrDefault("CERT_FILE", "")
app.httpServer.keyFile = app.Config.GetOrDefault("KEY_FILE", "")
app.httpServer.staticFiles = make(map[string]string)
// Note: Default routes (health, alive, favicon, swagger) are registered in httpServerSetup()
// only when HTTP server actually starts. This prevents gRPC-only apps from starting HTTP server.
// gRPC Server
port, err = strconv.Atoi(app.Config.Get("GRPC_PORT"))
if err != nil || port <= 0 {
port = defaultGRPCPort
}
app.grpcServer, err = newGRPCServer(app.container, port, app.Config)
// Continue without gRPC server rather than failing the entire app
if err != nil {
app.container.Logger.Errorf("failed to create gRPC server: %v", err)
}
app.subscriptionManager = newSubscriptionManager(app.container)
// static file server
currentWd, _ := os.Getwd()
checkDirectory := filepath.Join(currentWd, defaultPublicStaticDir)
if _, err = os.Stat(checkDirectory); err == nil {
app.httpServer.staticFiles[checkDirectory] = "/static"
app.httpRegistered = true
}
return app
}
// NewCMD creates a command-line application.
func NewCMD() *App {
app := &App{}
app.readConfig(true)
app.container = container.NewContainer(nil)
app.container.Logger = logging.NewFileLogger(app.Config.Get("CMD_LOGS_FILE"))
app.cmd = &cmd{
out: terminal.New(),
}
app.container.Create(app.Config)
app.initTracer()
return app
}
// initMetricsServer initializes the metrics server based on configuration.
// If METRICS_PORT is explicitly set to 0, the metrics server is disabled.
func (a *App) initMetricsServer() {
metricsPortStr := a.Config.Get("METRICS_PORT")
if metricsPortStr == "0" {
a.container.Logger.Logf("Metrics server is disabled (METRICS_PORT=0)")
return
}
port, err := strconv.Atoi(metricsPortStr)
if err != nil || port <= 0 {
port = defaultMetricPort
}
if !isPortAvailable(port) {
a.container.Logger.Fatalf("metrics port %d is blocked or unreachable", port)
}
a.metricServer = newMetricServer(port)
}
================================================
FILE: pkg/gofr/file/file.go
================================================
/*
Package file provides unified access to various file operations, such as creating, reading, writing files across :
- S3
- FTP
- SFTP
- Local FileSystem
*/
package file
type file struct {
name string
content []byte
size int64
isDir bool
}
func (f file) GetName() string {
return f.name
}
func (f file) GetSize() int64 {
return f.size
}
func (f file) Bytes() []byte {
return f.content
}
func (f file) IsDir() bool {
return f.isDir
}
================================================
FILE: pkg/gofr/file/file_test.go
================================================
package file
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestFile(t *testing.T) {
// Create a sample file
content := []byte("This is a test file.")
f := file{
name: "test.txt",
content: content,
size: int64(len(content)),
isDir: false,
}
// Test GetName method
assert.Equal(t, "test.txt", f.GetName(), "File name should be 'test.txt'")
// Test GetSize method
assert.Equal(t, int64(20), f.GetSize(), "File size should be 20 bytes")
// Test Bytes method
assert.Equal(t, content, f.Bytes(), "File content should match")
// Test IsDir method
assert.False(t, f.IsDir(), "File should not be a directory")
}
================================================
FILE: pkg/gofr/file/zip.go
================================================
package file
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
const (
maxFileSize = 100 * 1024 * 1024 // 100MB
)
var (
errMaxFileSize = errors.New("uncompressed file is greater than file size limit of 100MBs")
errPathTraversal = errors.New("path traversal attempt detected")
)
type Zip struct {
Files map[string]file
}
func NewZip(content []byte) (*Zip, error) {
reader := bytes.NewReader(content)
zipReader, err := zip.NewReader(reader, int64(len(content)))
if err != nil {
return nil, err
}
// Create a map to store file contents
files := make(map[string]file)
for _, zrf := range zipReader.File {
// Validate file name to prevent path traversal attacks at ZIP parse time. Reject entries with absolute paths or path traversal sequences
cleanName := filepath.Clean(zrf.Name)
if isUnsafePath(cleanName, zrf.Name) {
return nil, fmt.Errorf("invalid file path %q: %w", zrf.Name, errPathTraversal)
}
f, err := zrf.Open()
if err != nil {
return nil, err
}
buf, err := copyToBuffer(f, zrf.UncompressedSize64)
if err != nil {
return nil, err
}
files[cleanName] = file{
name: cleanName,
content: buf.Bytes(),
isDir: zrf.FileInfo().IsDir(),
size: zrf.FileInfo().Size(),
}
f.Close()
}
return &Zip{Files: files}, nil
}
func (z *Zip) CreateLocalCopies(dest string) error {
dest = filepath.Clean(dest)
destPrefix := dest + string(os.PathSeparator)
for _, zf := range z.Files {
destPath := filepath.Clean(filepath.Join(dest, zf.name))
// Prevent Zip Slip / path traversal attack by ensuring the destination path is within the intended extraction directory
if !strings.HasPrefix(destPath, destPrefix) && destPath != dest {
return fmt.Errorf("invalid file path %q: %w", zf.name, errPathTraversal)
}
if zf.isDir {
err := os.MkdirAll(destPath, os.ModePerm)
if err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(destPath), os.ModePerm); err != nil {
return err
}
destFile, err := os.Create(destPath)
if err != nil {
return err
}
if _, err := io.Copy(destFile, bytes.NewReader(zf.content)); err != nil {
return err
}
destFile.Close()
}
return nil
}
func copyToBuffer(f io.ReadCloser, size uint64) (*bytes.Buffer, error) {
// check that max file size of unzipped file is less than 100MB
if size > maxFileSize {
return nil, errMaxFileSize
}
buf := new(bytes.Buffer)
if n, err := io.CopyN(buf, f, maxFileSize); err != nil && !errors.Is(err, io.EOF) && n < int64(size) {
f.Close()
return nil, err
}
return buf, nil
}
// isUnsafePath checks if a path is unsafe (absolute, traversal, or Windows-specific patterns).
func isUnsafePath(cleanName, originalName string) bool {
// Absolute path (current OS) or relative to current/parent directory
if filepath.IsAbs(cleanName) || cleanName == "." || cleanName == ".." ||
strings.HasPrefix(cleanName, ".."+string(os.PathSeparator)) {
return true
}
// Windows paths (checked on all platforms): drive letter (C:\) or UNC (\\server)
return (len(originalName) >= 2 && originalName[1] == ':') || strings.HasPrefix(originalName, "\\\\")
}
================================================
FILE: pkg/gofr/file/zip_test.go
================================================
package file
import (
"archive/zip"
"bytes"
"errors"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
errTest = errors.New("mock read error")
)
func TestNewZip(t *testing.T) {
// Create some mock content for the ZIP file
content := []byte("file1.txt content")
zipContent := bytes.NewBuffer(nil)
// Create a new ZIP file using the mock content
zipWriter := zip.NewWriter(zipContent)
defer zipWriter.Close()
// Add a file to the ZIP archive
fileWriter, err := zipWriter.Create("file1.txt")
if err != nil {
t.Fatalf("Error creating file in ZIP: %v", err)
}
_, err = fileWriter.Write(content)
if err != nil {
t.Fatalf("Error writing to file in ZIP: %v", err)
}
// Close the ZIP writer
err = zipWriter.Close()
if err != nil {
t.Fatalf("Error closing ZIP writer: %v", err)
}
// Create a new Zip instance from the ZIP content
z, err := NewZip(zipContent.Bytes())
require.NoError(t, err, "Error creating Zip instance")
// Check if the Zip struct contains the expected files
expectedFiles := map[string]file{
"file1.txt": {name: "file1.txt", content: content, isDir: false, size: int64(len(content))},
}
assert.Equal(t, expectedFiles, z.Files, "Unexpected files in Zip struct")
}
func TestNewZipError(t *testing.T) {
input := []byte(``)
z, err := NewZip(input)
assert.Nil(t, z)
require.Error(t, err)
assert.Equal(t, zip.ErrFormat, err)
}
func TestCreateLocalCopies_Success(t *testing.T) {
mockZip := &Zip{
// Create a Zip instance with some mock data
Files: map[string]file{
"file1.txt": {name: "file1.txt", content: []byte("File 1 content"), isDir: false, size: 13},
"dir1/file2.txt": {name: "dir1/file2.txt", content: []byte("File 2 content"), isDir: false, size: 13},
},
}
destDir := "test"
defer os.RemoveAll(destDir)
if err := mockZip.CreateLocalCopies(destDir); err != nil {
t.Fatalf("Error creating local copies: %v", err)
}
// Verify that the files were created
expectedFiles := []string{"file1.txt", "dir1/file2.txt"}
for _, filename := range expectedFiles {
destPath := filepath.Join(destDir, filename)
_, err := os.Stat(destPath)
if os.IsNotExist(err) {
t.Errorf("Expected file %s does not exist", destPath)
} else if err != nil {
t.Errorf("Error checking file %s: %v", destPath, err)
}
}
}
func TestCopyToBuffer(t *testing.T) {
// Test when size is within limits
t.Run("WithinSizeLimit", func(t *testing.T) {
testData := "This is a test data"
buffer := bytes.NewBufferString(testData)
mock := &mockReadCloser{Buffer: buffer}
buf, err := copyToBuffer(mock, uint64(len(testData)))
require.NoError(t, err)
assert.Equal(t, testData, buf.String())
})
// Test when size exceeds the maximum allowed size
t.Run("ExceedsMaxSize", func(t *testing.T) {
testData := "This is a test data"
buffer := bytes.NewBufferString(testData)
mock := &mockReadCloser{Buffer: buffer}
_, err := copyToBuffer(mock, maxFileSize+1)
require.Error(t, err)
assert.Equal(t, errMaxFileSize, err)
})
// Test when an error occurs during copying
t.Run("CopyError", func(t *testing.T) {
// Create a mock reader that always returns an error
mock := &mockReadCloser{err: errTest}
_, err := copyToBuffer(mock, 10)
require.Error(t, err)
assert.Equal(t, errTest, err)
})
}
// mockReadCloser is a mock implementation of io.Reader for testing error conditions.
type mockReadCloser struct {
*bytes.Buffer
err error
}
func (m *mockReadCloser) Read(p []byte) (int, error) {
if m.err != nil {
return 0, m.err
}
return m.Buffer.Read(p)
}
func (*mockReadCloser) Close() error {
return nil
}
func TestCreateLocalCopies_WithDirectory(t *testing.T) {
mockZip := &Zip{
Files: map[string]file{
"dir1/": {name: "dir1/", isDir: true},
},
}
destDir := "test-dir"
defer os.RemoveAll(destDir)
err := mockZip.CreateLocalCopies(destDir)
require.NoError(t, err)
// Check if the directory exists
expectedDir := filepath.Join(destDir, "dir1")
info, err := os.Stat(expectedDir)
require.NoError(t, err)
assert.True(t, info.IsDir(), "Expected dir1 to be a directory")
}
func TestCreateLocalCopies_Failure(t *testing.T) {
mockZip := &Zip{
Files: map[string]file{
string([]byte{0x00}): {name: string([]byte{0x00}), content: []byte("invalid"), isDir: false, size: 7}, // Invalid path
},
}
err := mockZip.CreateLocalCopies("test-bad")
require.Error(t, err, "Expected error when creating file with invalid name")
}
func TestNewZip_PathTraversal_Success(t *testing.T) {
tests := []struct {
name string
filename string
}{
{
name: "valid filename",
filename: "file.txt",
},
{
name: "valid nested path",
filename: "dir/subdir/file.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
zipContent := bytes.NewBuffer(nil)
zipWriter := zip.NewWriter(zipContent)
fileWriter, err := zipWriter.Create(tt.filename)
require.NoError(t, err)
_, err = fileWriter.Write([]byte("test content"))
require.NoError(t, err)
err = zipWriter.Close()
require.NoError(t, err)
z, err := NewZip(zipContent.Bytes())
require.NoError(t, err)
assert.NotNil(t, z)
})
}
}
func TestNewZip_PathTraversal_Error(t *testing.T) {
tests := []struct {
name string
filename string
}{
{
name: "absolute path unix",
filename: "/etc/passwd",
},
{
name: "absolute path windows",
filename: "C:\\Windows\\System32\\config\\sam",
},
{
name: "unc path windows",
filename: "\\\\server\\share\\file.txt",
},
{
name: "path traversal with parent directory",
filename: "../etc/passwd",
},
{
name: "path traversal with multiple parent dirs",
filename: "../../../../../../etc/passwd",
},
{
name: "path traversal hidden in path",
filename: "foo/../../../etc/passwd",
},
{
name: "double dot only",
filename: "..",
},
{
name: "single dot only",
filename: ".",
},
{
name: "empty filename",
filename: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
zipContent := bytes.NewBuffer(nil)
zipWriter := zip.NewWriter(zipContent)
fileWriter, err := zipWriter.Create(tt.filename)
require.NoError(t, err)
_, err = fileWriter.Write([]byte("test content"))
require.NoError(t, err)
err = zipWriter.Close()
require.NoError(t, err)
z, err := NewZip(zipContent.Bytes())
require.Error(t, err)
require.ErrorIs(t, err, errPathTraversal)
assert.Nil(t, z)
})
}
}
func TestCreateLocalCopies_PathTraversal_Success(t *testing.T) {
tests := []struct {
name string
filename string
}{
{
name: "valid filename",
filename: "file.txt",
},
{
name: "valid nested path",
filename: "dir/subdir/file.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
destDir := t.TempDir()
mockZip := &Zip{
Files: map[string]file{
tt.filename: {name: tt.filename, content: []byte("test content"), isDir: false, size: 12},
},
}
err := mockZip.CreateLocalCopies(destDir)
require.NoError(t, err)
expectedPath := filepath.Join(destDir, tt.filename)
_, statErr := os.Stat(expectedPath)
assert.NoError(t, statErr, "Expected file to exist at %s", expectedPath)
})
}
}
func TestCreateLocalCopies_PathTraversal_Error(t *testing.T) {
tests := []struct {
name string
filename string
}{
{
name: "path traversal with parent directory",
filename: "../etc/passwd",
},
{
name: "path traversal with multiple parent dirs",
filename: "../../../../../../etc/passwd",
},
{
name: "path traversal hidden in path",
filename: "foo/../../../etc/passwd",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
destDir := t.TempDir()
mockZip := &Zip{
Files: map[string]file{
tt.filename: {name: tt.filename, content: []byte("test content"), isDir: false, size: 12},
},
}
err := mockZip.CreateLocalCopies(destDir)
require.Error(t, err)
require.ErrorIs(t, err, errPathTraversal)
assert.Contains(t, err.Error(), tt.filename)
})
}
}
================================================
FILE: pkg/gofr/gofr.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/gorilla/mux"
"golang.org/x/sync/errgroup"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/http/response"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/metrics"
"gofr.dev/pkg/gofr/migration"
"gofr.dev/pkg/gofr/service"
)
const (
configLocation = "./configs"
)
var errStartupHookPanic = errors.New("startup hook panicked")
// App is the main application in the GoFr framework.
type App struct {
// Config can be used by applications to fetch custom configurations from environment or file.
Config config.Config // If we directly embed, unnecessary confusion between app.Get and app.GET will happen.
grpcServer *grpcServer
httpServer *httpServer
metricServer *metricServer
cmd *cmd
cron *Crontab
// container is unexported because this is an internal implementation and applications are provided access to it via Context
container *container.Container
grpcRegistered bool
httpRegistered bool
subscriptionManager SubscriptionManager
graphqlManager *graphQLManager
onStartHooks []func(ctx *Context) error
mu sync.Mutex
}
func (a *App) runOnStartHooks(ctx context.Context) error {
// Use the existing newContext function with noopRequest
gofrCtx := newContext(nil, noopRequest{}, a.container)
// Set the context for cancellation support
gofrCtx.Context = ctx
for i, hook := range a.onStartHooks {
// Add panic recovery to prevent entire application crash
var hookErr error
func() {
defer func() {
if r := recover(); r != nil {
a.Logger().Errorf("OnStart hook %d panicked: %v", i, r)
hookErr = fmt.Errorf("hook %d: %w: %v", i, errStartupHookPanic, r)
}
}()
hookErr = hook(gofrCtx)
}()
if hookErr != nil {
a.Logger().Errorf("OnStart hook failed: %v", hookErr)
return hookErr
}
// Check if context was canceled
if ctx.Err() != nil {
return ctx.Err()
}
}
return nil
}
// Shutdown stops the service(s) and close the application.
// It shuts down the HTTP, gRPC, Metrics servers and closes the container's active connections to datasources.
func (a *App) Shutdown(ctx context.Context) error {
var err error
if a.httpServer != nil {
err = errors.Join(err, a.httpServer.Shutdown(ctx))
}
if a.grpcServer != nil {
err = errors.Join(err, a.grpcServer.Shutdown(ctx))
}
if a.container != nil {
err = errors.Join(err, a.container.Close())
}
if a.metricServer != nil {
err = errors.Join(err, a.metricServer.Shutdown(ctx))
}
if err != nil {
return err
}
a.container.Logger.Info("Application shutdown complete")
return err
}
func isPortAvailable(port int) bool {
dialer := net.Dialer{Timeout: checkPortTimeout}
conn, err := dialer.DialContext(context.Background(), "tcp", fmt.Sprintf(":%d", port))
if err != nil {
return true
}
conn.Close()
return false
}
func (a *App) httpServerSetup() {
// TODO: find a way to read REQUEST_TIMEOUT config only once and log it there. currently doing it twice one for populating
// the value and other for logging
requestTimeout := a.Config.Get("REQUEST_TIMEOUT")
if requestTimeout != "" {
timeoutVal, err := strconv.Atoi(requestTimeout)
if err != nil || timeoutVal < 0 {
a.container.Error("invalid value of config REQUEST_TIMEOUT.")
}
}
// Register default routes - these are only added when HTTP server is actually starting
a.add(http.MethodGet, service.HealthPath, healthHandler)
a.add(http.MethodGet, service.AlivePath, liveHandler)
a.add(http.MethodGet, "/favicon.ico", faviconHandler)
// Add OpenAPI/Swagger routes if openapi.json exists
a.checkAndAddOpenAPIDocumentation()
// Register GraphQL Playground UI under /.well-known/ if GraphQL is enabled
if a.graphqlManager != nil {
a.add(http.MethodGet, "/.well-known/graphql/ui", playgroundHandler)
}
for dirName, endpoint := range a.httpServer.staticFiles {
a.httpServer.router.AddStaticFiles(a.Logger(), endpoint, dirName)
}
a.setupGraphQL()
if a.container.Logger != nil {
a.container.Logger.Infof("Registered HTTP server on port: %d", a.httpServer.port)
}
a.httpServer.router.PathPrefix("/").Handler(handler{
function: catchAllHandler,
container: a.container,
})
var registeredMethods []string
_ = a.httpServer.router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
met, _ := route.GetMethods()
for _, method := range met {
if !contains(registeredMethods, method) { // Check for uniqueness before adding
registeredMethods = append(registeredMethods, method)
}
}
return nil
})
*a.httpServer.router.RegisteredRoutes = registeredMethods
}
func (a *App) startSubscriptions(ctx context.Context) error {
if len(a.subscriptionManager.subscriptions) == 0 {
return nil
}
group := errgroup.Group{}
// Start subscribers concurrently using go-routines
for topic, handler := range a.subscriptionManager.subscriptions {
subscriberTopic, subscriberHandler := topic, handler
group.Go(func() error {
return a.subscriptionManager.startSubscriber(ctx, subscriberTopic, subscriberHandler)
})
}
return group.Wait()
}
// readConfig reads the configuration from the default location.
func (a *App) readConfig(isAppCMD bool) {
var location string
if _, err := os.Stat(configLocation); err == nil {
location = configLocation
}
if isAppCMD {
a.Config = config.NewEnvFile(location, logging.NewFileLogger(""))
return
}
a.Config = config.NewEnvFile(location, logging.NewLogger(logging.INFO))
}
// AddHTTPService registers HTTP service in container.
func (a *App) AddHTTPService(serviceName, serviceAddress string, options ...service.Options) {
if a.container.Services == nil {
a.container.Services = make(map[string]service.HTTP)
}
if _, ok := a.container.Services[serviceName]; ok {
a.container.Debugf("Service already registered Name: %v", serviceName)
}
options = append([]service.Options{service.WithAttributes(map[string]string{"name": serviceName})}, options...)
a.container.Services[serviceName] = service.NewHTTPService(serviceAddress, a.container.Logger, a.container.Metrics(), options...)
}
// GraphQLQuery registers a named query resolver for the GraphQL schema.
//
// Developer Notes:
// - GraphQL uses dedicated methods (GraphQLQuery, GraphQLMutation) instead of standard HTTP verb
// methods (GET, POST) because GraphQL operations are distinguished by type in the request body,
// not by HTTP method. All operations are served through a single POST /graphql endpoint.
// - POST-only is intentional: it is the required method per the GraphQL-over-HTTP spec. GET is
// optional and only useful for CDN caching, which adds complexity without clear benefit here.
// - Only query and mutation are supported for now. Subscriptions require a persistent connection
// (WebSocket) and are out of scope for this initial implementation.
// - The resolver name must match a field in the Query type defined in ./configs/schema.graphqls.
// Multiple resolvers can be registered, but all resolve fields within that single schema file.
func (a *App) GraphQLQuery(name string, handler Handler) {
if !a.httpRegistered && !isPortAvailable(a.httpServer.port) {
a.container.Logger.Fatalf("http port %d is blocked or unreachable", a.httpServer.port)
}
a.mu.Lock()
if a.graphqlManager == nil {
a.graphqlManager = newGraphQLManager(a.container)
}
a.mu.Unlock()
a.httpRegistered = true
a.graphqlManager.RegisterQuery(name, handler)
}
// GraphQLMutation registers a named mutation resolver for the GraphQL schema.
// See GraphQLQuery for design rationale. Mutations follow the same pattern but are intended
// for operations with side effects (create, update, delete).
func (a *App) GraphQLMutation(name string, handler Handler) {
if !a.httpRegistered && !isPortAvailable(a.httpServer.port) {
a.container.Logger.Fatalf("http port %d is blocked or unreachable", a.httpServer.port)
}
a.mu.Lock()
if a.graphqlManager == nil {
a.graphqlManager = newGraphQLManager(a.container)
}
a.mu.Unlock()
a.httpRegistered = true
a.graphqlManager.RegisterMutation(name, handler)
}
// Metrics returns the metrics manager associated with the App.
func (a *App) Metrics() metrics.Manager {
return a.container.Metrics()
}
// Logger returns the logger instance associated with the App.
func (a *App) Logger() logging.Logger {
return a.container.Logger
}
// SubCommand adds a sub-command to the CLI application.
// Can be used to create commands like "kubectl get" or "kubectl get ingress".
func (a *App) SubCommand(pattern string, handler Handler, options ...Options) {
a.cmd.addRoute(pattern, handler, options...)
}
// Migrate applies a set of migrations to the application's database.
//
// The migrationsMap argument is a map where the key is the version number of the migration
// and the value is a migration.Migrate instance that implements the migration logic.
func (a *App) Migrate(migrationsMap map[int64]migration.Migrate) {
// TODO : Move panic recovery at central location which will manage for all the different cases.
defer func() {
panicRecovery(recover(), a.container.Logger)
}()
migration.Run(migrationsMap, a.container)
}
// Subscribe registers a handler for the given topic.
//
// If the subscriber is not initialized in the container, an error is logged and
// the subscription is not registered.
func (a *App) Subscribe(topic string, handler SubscribeFunc) {
if topic == "" || handler == nil {
a.container.Logger.Errorf("invalid subscription: topic and handler must not be empty or nil")
return
}
if a.container.GetSubscriber() == nil {
a.container.Logger.Errorf("subscriber not initialized in the container")
return
}
a.subscriptionManager.subscriptions[topic] = handler
}
// UseMiddleware is a setter method for adding user defined custom middleware to GoFr's router.
func (a *App) UseMiddleware(middlewares ...gofrHTTP.Middleware) {
a.httpServer.router.UseMiddleware(middlewares...)
}
// UseMiddlewareWithContainer adds a middleware that has access to the container
// and wraps the provided handler with the middleware logic.
//
// The `middleware` function receives the container and the handler, allowing
// the middleware to modify the request processing flow.
// Deprecated: UseMiddlewareWithContainer will be removed in a future release.
// Please use the [*App.UseMiddleware] method that does not depend on the container.
func (a *App) UseMiddlewareWithContainer(middlewareHandler func(c *container.Container, handler http.Handler) http.Handler) {
a.httpServer.router.Use(func(h http.Handler) http.Handler {
// Wrap the provided handler `h` with the middleware function `middlewareHandler`
return middlewareHandler(a.container, h)
})
}
// AddCronJob registers a cron job to the cron table.
// The cron expression can be either a 5-part or 6-part format. The 6-part format includes an
// optional second field (in beginning) and others being minute, hour, day, month and day of week respectively.
func (a *App) AddCronJob(schedule, jobName string, job CronFunc) {
if a.cron == nil {
a.cron = NewCron(a.container)
}
if err := a.cron.AddJob(schedule, jobName, job); err != nil {
a.Logger().Errorf("error adding cron job, err: %v", err)
}
}
// contains is a helper function checking for duplicate entry in a slice.
func contains(elems []string, v string) bool {
for _, s := range elems {
if v == s {
return true
}
}
return false
}
// AddStaticFiles registers a static file endpoint for the application.
//
// The provided `endpoint` will be used as the prefix for the static file
// server. The `filePath` specifies the directory containing the static files.
// If `filePath` starts with "./", it will be interpreted as a relative path
// to the current working directory.
func (a *App) AddStaticFiles(endpoint, filePath string) {
if !a.httpRegistered && !isPortAvailable(a.httpServer.port) {
a.container.Logger.Fatalf("http port %d is blocked or unreachable", a.httpServer.port)
}
a.httpRegistered = true
if !strings.HasPrefix(filePath, "./") && !filepath.IsAbs(filePath) {
filePath = "./" + filePath
}
// update file path based on current directory if it starts with ./
if strings.HasPrefix(filePath, "./") {
currentWorkingDir, _ := os.Getwd()
filePath = filepath.Join(currentWorkingDir, filePath)
}
endpoint = "/" + strings.TrimPrefix(endpoint, "/")
if _, err := os.Stat(filePath); err != nil {
a.container.Logger.Errorf("error in registering '%s' static endpoint, error: %v", endpoint, err)
return
}
a.httpServer.staticFiles[filePath] = endpoint
}
// OnStart registers a startup hook that will be executed when the application starts.
// The hook function receives a Context that provides access to the application's
// container, logger, and configuration. This is useful for performing initialization
// tasks such as database connections, service registrations, or other setup operations
// that need to be completed before the application begins serving requests.
//
// Example usage:
//
// app := gofr.New()
// app.OnStart(func(ctx *gofr.Context) error {
// // Initialize database connection
// db, err := database.Connect(ctx.Config.Get("DB_URL"))
// if err != nil {
// return err
// }
// ctx.Container.SQL = db
// return nil
// })
func (a *App) OnStart(hook func(ctx *Context) error) {
a.onStartHooks = append(a.onStartHooks, hook)
}
func (a *App) setupGraphQL() {
if a.graphqlManager != nil {
err := a.graphqlManager.buildSchema()
if err != nil {
a.container.Logger.Fatalf("GraphQL build error: %v", err)
}
// Functional endpoint: served via POST per spec to ensure data safety and consistency.
a.httpServer.router.NewRoute().Methods(http.MethodPost).Path("/graphql").Handler(a.graphqlManager.GetHandler())
}
}
// playgroundHandler serves the GraphQL interactive playground UI.
func playgroundHandler(_ *Context) (any, error) {
return response.File{Content: []byte(graphiqlHTML), ContentType: "text/html"}, nil
}
================================================
FILE: pkg/gofr/gofr_test.go
================================================
package gofr
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/migration"
"gofr.dev/pkg/gofr/testutil"
)
const helloWorld = "Hello World!"
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestNewCMD(t *testing.T) {
a := NewCMD()
// Without args we should get error on stderr.
outputWithoutArgs := testutil.StderrOutputForFunc(a.Run)
assert.Contains(t, outputWithoutArgs, "is not a valid command", "TEST Failed.\n%s", "Stderr output mismatch")
}
func TestGofr_readConfig(t *testing.T) {
app := App{}
app.readConfig(false)
if app.Config == nil {
t.Errorf("config was not read")
}
}
func TestGoFr_isPortAvailable(t *testing.T) {
configs := testutil.NewServerConfigs(t)
tests := []struct {
name string
isAvailable bool
}{
{"Port is available", true},
{"Port is not available", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.isAvailable {
g := New()
// Register a route to ensure HTTP server starts
// (HTTP server only starts when user routes are registered)
g.GET("/test", func(*Context) (any, error) {
return "test", nil
})
go g.Run()
time.Sleep(100 * time.Millisecond)
}
isAvailable := isPortAvailable(configs.HTTPPort)
require.Equal(t, tt.isAvailable, isAvailable)
})
}
}
// mockRoundTripper is a mock implementation of http.RoundTripper.
type mockRoundTripper struct {
lastRequest *http.Request // Store the last request for assertions
mockResponse *http.Response
mockError error
}
// RoundTrip mocks the HTTP request and stores the request for verification.
func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
m.lastRequest = req // Store the request for assertions
return m.mockResponse, m.mockError
}
func TestPingGoFr(t *testing.T) {
tests := []struct {
name string
input bool
expectedURL string
}{
{"Ping Start Server", true, gofrHost + startServerPing},
{"Ping Shut Server", false, gofrHost + shutServerPing},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockTransport := &mockRoundTripper{
mockResponse: &http.Response{
StatusCode: http.StatusOK,
Body: http.NoBody,
},
mockError: nil,
}
mockClient := &http.Client{Transport: mockTransport}
_ = testutil.NewServerConfigs(t)
a := New()
a.sendTelemetry(mockClient, tt.input)
assert.NotNil(t, mockTransport.lastRequest, "Request should not be nil")
assert.Equal(t, tt.expectedURL, mockTransport.lastRequest.URL.String(), "Unexpected request URL")
assert.Equal(t, http.MethodPost, mockTransport.lastRequest.Method, "Unexpected HTTP method")
})
}
}
func TestGofr_ServerRoutes(t *testing.T) {
_ = testutil.NewServerConfigs(t)
type response struct {
Data any `json:"data"`
}
testCases := []struct {
// Given
method string
target string
// Expectations
response string
headerKey string
headerVal string
}{
{http.MethodGet, "/hello", "Hello World!", "content-type", "application/json"},
{http.MethodGet, "/hello2", "Hello World!", "content-type", "application/json"},
{http.MethodPut, "/hello", "Hello World!", "content-type", "application/json"},
{http.MethodPost, "/hello", "Hello World!", "content-type", "application/json"},
{http.MethodGet, "/params?name=Vikash", "Hello Vikash!", "content-type", "application/json"},
{http.MethodDelete, "/delete", "Success", "content-type", "application/json"},
{http.MethodPatch, "/patch", "Success", "content-type", "application/json"},
}
g := New()
g.GET("/hello", func(*Context) (any, error) {
return helloWorld, nil
})
// using add() func
g.add(http.MethodGet, "/hello2", func(*Context) (any, error) {
return helloWorld, nil
})
g.PUT("/hello", func(*Context) (any, error) {
return helloWorld, nil
})
g.POST("/hello", func(*Context) (any, error) {
return helloWorld, nil
})
g.GET("/params", func(c *Context) (any, error) {
return fmt.Sprintf("Hello %s!", c.Param("name")), nil
})
g.DELETE("/delete", func(*Context) (any, error) {
return "Success", nil
})
g.PATCH("/patch", func(*Context) (any, error) {
return "Success", nil
})
for i, tc := range testCases {
w := httptest.NewRecorder()
r := httptest.NewRequest(tc.method, tc.target, http.NoBody)
r.Header.Set("Content-Type", "application/json")
g.httpServer.router.ServeHTTP(w, r)
var res response
respBytes, _ := io.ReadAll(w.Body)
_ = json.Unmarshal(respBytes, &res)
assert.Equal(t, res.Data, tc.response, "TEST[%d], Failed.\nUnexpected response for %s %s.", i, tc.method, tc.target)
assert.Equal(t, w.Header().Get(tc.headerKey), tc.headerVal,
"TEST[%d], Failed.\nHeader mismatch for %s %s", i, tc.method, tc.target)
}
}
func TestGofr_ServerRun(t *testing.T) {
configs := testutil.NewServerConfigs(t)
g := New()
g.GET("/hello", func(*Context) (any, error) {
return helloWorld, nil
})
go g.Run()
time.Sleep(100 * time.Millisecond)
var netClient = &http.Client{
Timeout: 200 * time.Millisecond,
}
re, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
"http://localhost:"+fmt.Sprint(configs.HTTPPort)+"/hello", http.NoBody)
resp, err := netClient.Do(re)
require.NoError(t, err, "TEST Failed.\n")
assert.Equal(t, http.StatusOK, resp.StatusCode, "TEST Failed.\n")
resp.Body.Close()
}
func Test_AddHTTPService(t *testing.T) {
_ = testutil.NewServerConfigs(t)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/test", r.URL.Path)
w.WriteHeader(http.StatusOK)
}))
g := New()
g.AddHTTPService("test-service", server.URL)
resp, _ := g.container.GetHTTPService("test-service").
Get(t.Context(), "test", nil)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func Test_AddDuplicateHTTPService(t *testing.T) {
configs := testutil.NewServerConfigs(t)
t.Setenv("LOG_LEVEL", "DEBUG")
t.Setenv("METRICS_PORT", strconv.Itoa(configs.MetricsPort))
t.Setenv("HTTP_PORT", strconv.Itoa(configs.HTTPPort))
logs := testutil.StdoutOutputForFunc(func() {
a := New()
a.AddHTTPService("test-service", "http://localhost")
a.AddHTTPService("test-service", "http://google")
})
assert.Contains(t, logs, "Service already registered Name: test-service")
}
func TestApp_Metrics(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
assert.NotNil(t, app.Metrics())
}
func TestApp_MetricsServerDisabled(t *testing.T) {
// Set METRICS_PORT=0 to disable the metrics server
t.Setenv("METRICS_PORT", "0")
logs := testutil.StdoutOutputForFunc(func() {
app := New()
// Verify that metricServer is nil when METRICS_PORT=0
assert.Nil(t, app.metricServer, "metrics server should be nil when METRICS_PORT=0")
})
// Verify log message is printed
assert.Contains(t, logs, "Metrics server is disabled (METRICS_PORT=0)")
}
func TestApp_AddAndGetHTTPService(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
app.AddHTTPService("test-service", "http://test")
svc := app.container.GetHTTPService("test-service")
assert.NotNil(t, svc)
}
func TestApp_MigrateInvalidKeys(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
testutil.NewServerConfigs(t)
app := New()
app.Migrate(map[int64]migration.Migrate{1: {}})
})
assert.Contains(t, logs, "migration run failed! UP not defined for the following keys: [1]")
}
func TestApp_MigratePanicRecovery(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
testutil.NewServerConfigs(t)
app := New()
app.container.PubSub = &container.MockPubSub{}
app.Migrate(map[int64]migration.Migrate{1: {UP: func(_ migration.Datasource) error {
panic("test panic")
}}})
})
assert.Contains(t, logs, "test panic")
}
func Test_otelErrorHandler(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
h := otelErrorHandler{
logger: logging.NewLogger(logging.DEBUG),
}
h.Handle(testutil.CustomError{ErrorMessage: "OTEL Error override"})
})
assert.Contains(t, logs, `"message":"OTEL Error override"`)
assert.Contains(t, logs, `"level":"ERROR"`)
}
func Test_addRoute(t *testing.T) {
originalArgs := os.Args // Save the original os.Args
// Modify os.Args for the duration of this test
os.Args = []string{"", "log"}
t.Cleanup(func() { os.Args = originalArgs }) // Restore os.Args after the test
// Capture the standard output to verify the logs.
logs := testutil.StdoutOutputForFunc(func() {
a := NewCMD()
// Add the "log" sub-command with its handler and description.
a.SubCommand("log", func(c *Context) (any, error) {
c.Logger.Info("logging in handler")
return "handler called", nil
}, AddDescription("Logs a message"))
// Run the command-line application.
a.Run()
})
// Verify that the handler was called and the expected log message was output.
assert.Contains(t, logs, "handler called")
}
func TestEnableBasicAuthWithFunc(t *testing.T) {
port := testutil.GetFreePort(t)
jwksServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
c := container.NewContainer(config.NewMockConfig(nil))
// Initialize a new App instance
a := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: c,
}
a.httpServer.router.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Println(w, "Hello, world!")
}))
a.EnableOAuth(jwksServer.URL, 600)
server := httptest.NewServer(a.httpServer.router)
defer server.Close()
client := server.Client()
// Create a mock HTTP request
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, http.NoBody)
if err != nil {
t.Fatal(err)
}
// Add a basic authorization header
req.Header.Add("Authorization", "dXNlcjpwYXNzd29yZA==")
// Send the HTTP request
resp, err := client.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "TestEnableBasicAuthWithFunc Failed!")
}
func encodeBasicAuthorization(t *testing.T, arg string) string {
t.Helper()
data := []byte(arg)
dst := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
base64.StdEncoding.Encode(dst, data)
s := "Basic " + string(dst)
return s
}
func Test_EnableBasicAuth(t *testing.T) {
port := testutil.GetFreePort(t)
mockContainer, _ := container.NewMockContainer(t)
tests := []struct {
name string
args []string
passedCredentials string
expectedStatusCode int
}{
{
"No Authorization header passed",
[]string{"user1", "password1", "user2", "password2"},
"",
http.StatusUnauthorized,
},
{
"Even number of arguments",
[]string{"user1", "password1", "user2", "password2"},
"user1:password1",
http.StatusOK,
},
{
"Odd number of arguments with no authorization header passed",
[]string{"user1", "password1", "user2"},
"",
http.StatusOK,
},
{
"Odd number of arguments with wrong authorization header passed",
[]string{"user1", "password1", "user2"},
"user1:password2",
http.StatusOK,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize a new App instance
a := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: mockContainer,
}
a.httpServer.router.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}))
a.EnableBasicAuth(tt.args...)
server := httptest.NewServer(a.httpServer.router)
defer server.Close()
client := server.Client()
// Create a mock HTTP request
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, http.NoBody)
require.NoError(t, err)
// Add a basic authorization header
req.Header.Add("Authorization", encodeBasicAuthorization(t, tt.passedCredentials))
// Send the HTTP request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tt.expectedStatusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tt.name)
})
}
}
func Test_EnableBasicAuthWithValidator(t *testing.T) {
port := testutil.GetFreePort(t)
mockContainer, _ := container.NewMockContainer(t)
tests := []struct {
name string
passedCredentials string
expectedStatusCode int
}{
{
"No Authorization header passed",
"",
http.StatusUnauthorized,
},
{
"Correct Authorization",
"user:password",
http.StatusOK,
},
{
"Wrong Authorization header passed",
"user2:password2",
http.StatusUnauthorized,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Initialize a new App instance
a := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: mockContainer,
}
validateFunc := func(_ *container.Container, username string, password string) bool {
return username == "user" && password == "password"
}
a.EnableBasicAuthWithValidator(validateFunc)
a.httpServer.router.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "Hello, world!")
}))
server := httptest.NewServer(a.httpServer.router)
defer server.Close()
client := server.Client()
// Create a mock HTTP request
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL, http.NoBody)
require.NoError(t, err)
// Add a basic authorization header
req.Header.Add("Authorization", encodeBasicAuthorization(t, tt.passedCredentials))
// Send the HTTP request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, tt.expectedStatusCode, resp.StatusCode, "TEST[%d], Failed.\n%s", i, tt.name)
})
}
}
func Test_AddRESTHandlers(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
type user struct {
ID int
Name string
}
var invalidObject int
tests := []struct {
desc string
input any
err error
}{
{"success case", &user{}, nil},
{"invalid object", &invalidObject, errInvalidObject},
{"invalid object", user{}, fmt.Errorf("failed to register routes for 'user' struct, %w", errNonPointerObject)},
{"invalid object", nil, errObjectIsNil},
}
for i, tc := range tests {
err := app.AddRESTHandlers(tc.input)
assert.Equal(t, tc.err, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_initTracer(t *testing.T) {
createMockConfig := func(traceExporter, url, authKey string) config.Config {
return config.NewMockConfig(map[string]string{
"TRACE_EXPORTER": traceExporter,
"TRACER_URL": url,
"TRACER_AUTH_KEY": authKey,
})
}
mockConfig1 := createMockConfig("zipkin", "http://localhost:2005/api/v2/spans", "")
mockConfig2 := createMockConfig("zipkin", "http://localhost:2005/api/v2/spans", "valid-token")
mockConfig3 := createMockConfig("jaeger", "localhost:4317", "")
mockConfig4 := createMockConfig("jaeger", "localhost:4317", "valid-token")
mockConfig5 := createMockConfig("otlp", "localhost:4317", "")
mockConfig6 := createMockConfig("otlp", "localhost:4317", "valid-token")
mockConfig7 := createMockConfig("gofr", "", "")
tests := []struct {
desc string
config config.Config
expectedLogMessage string
}{
{"tracing disabled", config.NewMockConfig(nil), "tracing is disabled"},
{"zipkin exporter", mockConfig1, "Exporting traces to zipkin at http://localhost:2005/api/v2/spans"},
{"zipkin exporter with authkey", mockConfig2, "Exporting traces to zipkin at http://localhost:2005/api/v2/spans"},
{"jaeger exporter", mockConfig3, "Exporting traces to jaeger at localhost:4317"},
{"jaeger exporter with auth", mockConfig4, "Exporting traces to jaeger at localhost:4317"},
{"otlp exporter", mockConfig5, "Exporting traces to otlp at localhost:4317"},
{"otlp exporter with authKey", mockConfig6, "Exporting traces to otlp at localhost:4317"},
{"gofr exporter with default url", mockConfig7, "Exporting traces to GoFr at https://tracer-api.gofr.dev/api/spans"},
}
for i, tc := range tests {
logMessage := testutil.StdoutOutputForFunc(func() {
mockContainer, _ := container.NewMockContainer(t)
a := App{
Config: tc.config,
container: mockContainer,
}
a.initTracer()
})
assert.Contains(t, logMessage, tc.expectedLogMessage, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_initTracer_invalidConfig(t *testing.T) {
createMockConfig := func(traceExporter, url, authKey string) config.Config {
return config.NewMockConfig(map[string]string{
"TRACE_EXPORTER": traceExporter,
"TRACER_URL": url,
"TRACER_AUTH_KEY": authKey,
})
}
mockConfig1 := createMockConfig("abc", "https://tracer-service.dev", "")
mockConfig2 := createMockConfig("", "https://tracer-service.dev", "")
mockConfig3 := createMockConfig("otlp", "", "")
testErr := []struct {
desc string
config config.Config
expectedLogMessage string
}{
{"unsupported trace_exporter", mockConfig1, "unsupported TRACE_EXPORTER: abc"},
{"missing trace_exporter", mockConfig2, "missing TRACE_EXPORTER config, should be provided with TRACER_URL to enable tracing"},
{"miss tracer_url ", mockConfig3,
"missing TRACER_URL config, should be provided with TRACE_EXPORTER to enable tracing"},
}
for i, tc := range testErr {
logMessage := testutil.StderrOutputForFunc(func() {
mockContainer, _ := container.NewMockContainer(t)
a := App{
Config: tc.config,
container: mockContainer,
}
a.initTracer()
})
assert.Contains(t, logMessage, tc.expectedLogMessage, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_UseMiddleware(t *testing.T) {
port := testutil.GetFreePort(t)
testMiddleware := func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test-Middleware", "applied")
inner.ServeHTTP(w, r)
})
}
c := container.NewContainer(config.NewMockConfig(nil))
app := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: c,
Config: config.NewMockConfig(map[string]string{
"REQUEST_TIMEOUT": "5",
"SHUTDOWN_GRACE_PERIOD": "1s",
}),
}
app.UseMiddleware(testMiddleware)
app.GET("/test", func(*Context) (any, error) {
return "success", nil
})
go app.Run()
time.Sleep(100 * time.Millisecond)
var netClient = &http.Client{
Timeout: 200 * time.Millisecond,
}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
fmt.Sprintf("http://localhost:%d", port)+"/test", http.NoBody)
resp, err := netClient.Do(req)
if err != nil {
t.Errorf("error while making HTTP request in Test_UseMiddleware. err : %v", err)
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Test_UseMiddleware Failed! Expected Status 200 Got : %v", resp.StatusCode)
// checking if the testMiddleware has added the required header in the response properly.
testHeaderValue := resp.Header.Get("X-Test-Middleware")
assert.Equal(t, "applied", testHeaderValue, "Test_UseMiddleware Failed! header value mismatch.")
}
// Test the UseMiddlewareWithContainer function.
func TestUseMiddlewareWithContainer(t *testing.T) {
port := testutil.GetFreePort(t)
// Initialize the mock container
mockContainer := container.NewContainer(config.NewMockConfig(nil))
// Create a simple handler to test middleware functionality
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Hello, world!"))
})
// Middleware to modify response and test container access
middleware := func(c *container.Container, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Ensure the container is passed correctly (for this test, we are just logging)
assert.NotNil(t, c, "Container should not be nil in the middleware")
// Continue with the handler execution
handler.ServeHTTP(w, r)
})
}
// Create a new App with a mock server
app := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: mockContainer,
Config: config.NewMockConfig(map[string]string{"REQUEST_TIMEOUT": "5"}),
}
// Use the middleware with the container
app.UseMiddlewareWithContainer(middleware)
// Register the handler to a route for testing
app.httpServer.router.Handle("/test", handler)
// Create a test request
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
// Create a test response recorder
rr := httptest.NewRecorder()
// Call the handler with the request and recorder
app.httpServer.router.ServeHTTP(rr, req)
// Assert the status code and response body
assert.Equal(t, http.StatusOK, rr.Code)
assert.Equal(t, "Hello, world!", rr.Body.String())
}
func Test_APIKeyAuthMiddleware(t *testing.T) {
port := testutil.GetFreePort(t)
c, _ := container.NewMockContainer(t)
app := &App{
httpServer: &httpServer{
router: gofrHTTP.NewRouter(),
port: port,
},
container: c,
Config: config.NewMockConfig(map[string]string{"REQUEST_TIMEOUT": "5"}),
}
apiKeys := []string{"test-key"}
validateFunc := func(_ *container.Container, apiKey string) bool {
return apiKey == "test-key"
}
// Registering APIKey middleware with and without custom validator
app.EnableAPIKeyAuth(apiKeys...)
app.EnableAPIKeyAuthWithValidator(validateFunc)
app.GET("/test", func(_ *Context) (any, error) {
return "success", nil
})
go app.Run()
time.Sleep(100 * time.Millisecond)
var netClient = &http.Client{
Timeout: 200 * time.Millisecond,
}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
fmt.Sprintf("http://localhost:%d", port)+"/test", http.NoBody)
req.Header.Set("X-Api-Key", "test-key")
// Send the request and check for successful response
resp, err := netClient.Do(req)
if err != nil {
t.Errorf("error while making HTTP request in Test_APIKeyAuthMiddleware. err: %v", err)
return
}
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "Test_APIKeyAuthMiddleware Failed!")
}
func Test_SwaggerEndpoints(t *testing.T) {
configs := testutil.NewServerConfigs(t)
// Create the openapi.json file within the static directory
openAPIFilePath := filepath.Join("static", OpenAPIJSON)
openAPIContent := []byte(`{"swagger": "2.0", "info": {"version": "1.0.0", "title": "Sample API"}}`)
if err := os.WriteFile(openAPIFilePath, openAPIContent, 0600); err != nil {
t.Errorf("Failed to create openapi.json file: %v", err)
return
}
// Defer removal of swagger file from the static directory
defer func() {
if err := os.RemoveAll("static/openapi.json"); err != nil {
t.Errorf("Failed to remove swagger file from static directory: %v", err)
}
}()
app := New()
app.httpRegistered = true
app.httpServer.port = configs.HTTPPort
go app.Run()
time.Sleep(100 * time.Millisecond)
var netClient = &http.Client{
Timeout: 200 * time.Millisecond,
}
re, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
configs.HTTPHost+"/.well-known/swagger", http.NoBody)
resp, err := netClient.Do(re)
defer func() {
err = resp.Body.Close()
if err != nil {
t.Errorf("error closing response body: %v", err)
}
}()
require.NoError(t, err, "Expected error to be nil, got : %v", err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
}
func Test_AddCronJob_Fail(t *testing.T) {
a := App{container: &container.Container{}}
stderr := testutil.StderrOutputForFunc(func() {
a.container.Logger = logging.NewLogger(logging.ERROR)
a.AddCronJob("* * * *", "test-job", func(ctx *Context) {
ctx.Logger.Info("test-job-fail")
})
})
assert.Contains(t, stderr, "error adding cron job")
assert.NotContains(t, stderr, "test-job-fail")
}
func Test_AddCronJob_Success(t *testing.T) {
pass := false
a := App{
container: &container.Container{},
}
a.AddCronJob("* * * * *", "test-job", func(ctx *Context) {
ctx.Logger.Info("test-job-success")
})
assert.Len(t, a.cron.jobs, 1)
for _, j := range a.cron.jobs {
if j.name == "test-job" {
pass = true
break
}
}
assert.Truef(t, pass, "unable to add cron job to cron table")
}
func setupTestEnvironment(t *testing.T) (host string, htmlContent []byte) {
t.Helper()
configs := testutil.NewServerConfigs(t)
// Generating some files for testing
htmlContent = []byte("Test Static File Testing Static File
")
createPublicDirectory(t, defaultPublicStaticDir, htmlContent)
createPublicDirectory(t, "testdir", htmlContent)
app := New()
app.AddStaticFiles("gofrTest", "testdir")
app.httpServer.port = configs.HTTPPort
go app.Run()
time.Sleep(100 * time.Millisecond)
host = configs.HTTPHost
return host, htmlContent
}
func TestStaticHandler(t *testing.T) {
const indexHTML = "indexTest.html"
host, htmlContent := setupTestEnvironment(t)
defer os.Remove("static/indexTest.html")
defer os.RemoveAll("testdir")
tests := []struct {
desc string
method string
path string
statusCode int
expectedBody string
expectedBodyLength int
expectedResponseHeaderType string
}{
{
desc: "check file content index.html", method: http.MethodGet, path: "/" + defaultPublicStaticDir + "/" + indexHTML,
statusCode: http.StatusOK, expectedBodyLength: len(htmlContent),
expectedResponseHeaderType: "text/html; charset=utf-8", expectedBody: string(htmlContent),
},
{
desc: "check public endpoint", method: http.MethodGet,
path: "/" + defaultPublicStaticDir, statusCode: http.StatusNotFound,
},
{
desc: "check file content index.html in custom dir", method: http.MethodGet, path: "/" + "gofrTest" + "/" + indexHTML,
statusCode: http.StatusOK, expectedBodyLength: len(htmlContent),
expectedResponseHeaderType: "text/html; charset=utf-8", expectedBody: string(htmlContent),
},
{
desc: "check public endpoint in custom dir", method: http.MethodGet, path: "/" + "gofrTest",
statusCode: http.StatusNotFound,
},
}
for i, tc := range tests {
request, err := http.NewRequestWithContext(t.Context(), tc.method, host+tc.path, http.NoBody)
if err != nil {
t.Fatalf("TEST[%d], Failed to create request, error: %s", i, err)
}
request.Header.Set("Content-Type", "application/json")
client := http.Client{}
resp, err := client.Do(request)
if err != nil {
t.Fatalf("TEST[%d], Request failed, error: %s", i, err)
}
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("TEST[%d], Failed to read response body, error: %s", i, err)
}
body := string(bodyBytes)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, resp.StatusCode, "TEST[%d], Failed with Status Body.\n%s", i, tc.desc)
if tc.expectedBody != "" {
assert.Contains(t, body, tc.expectedBody, "TEST[%d], Failed with Expected Body.\n%s", i, tc.desc)
}
if tc.expectedBodyLength != 0 {
contentLength := resp.Header.Get("Content-Length")
assert.Equal(t, strconv.Itoa(tc.expectedBodyLength), contentLength, "TEST[%d], Failed at Content-Length.\n%s", i, tc.desc)
}
if tc.expectedResponseHeaderType != "" {
assert.Equal(t,
tc.expectedResponseHeaderType,
resp.Header.Get("Content-Type"),
"TEST[%d], Failed at Expected Content-Type.\n%s", i, tc.desc)
}
resp.Body.Close()
}
}
func TestStaticHandlerInvalidFilePath(t *testing.T) {
// Generating some files for testing
logs := testutil.StderrOutputForFunc(func() {
testutil.NewServerConfigs(t)
app := New()
app.AddStaticFiles("gofrTest", ".//,.!@#$%^&")
})
assert.Contains(t, logs, "no such file or directory")
assert.Contains(t, logs, "error in registering '/gofrTest' static endpoint")
}
func TestNewSetsHTTPRegisteredWhenStaticDirExists(t *testing.T) {
testutil.NewServerConfigs(t)
createPublicDirectory(t, defaultPublicStaticDir, []byte(""))
defer os.Remove("static/indexTest.html")
app := New()
assert.True(t, app.httpRegistered,
"httpRegistered should be true when ./static directory exists")
assert.NotEmpty(t, app.httpServer.staticFiles,
"staticFiles map should contain the auto-detected directory")
}
func createPublicDirectory(t *testing.T, defaultPublicStaticDir string, htmlContent []byte) {
t.Helper()
const indexHTML = "indexTest.html"
directory := "./" + defaultPublicStaticDir
if _, err := os.Stat(directory); err != nil {
if err = os.Mkdir("./"+defaultPublicStaticDir, os.ModePerm); err != nil {
t.Fatalf("Couldn't create a "+defaultPublicStaticDir+" directory, error: %s", err)
}
}
file, err := os.Create(filepath.Join(directory, indexHTML))
if err != nil {
t.Fatalf("Couldn't create %s file", indexHTML)
}
_, err = file.Write(htmlContent)
if err != nil {
t.Fatalf("Couldn't write to %s file", indexHTML)
}
file.Close()
}
func Test_Shutdown(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
testutil.NewServerConfigs(t)
g := New()
g.GET("/hello", func(*Context) (any, error) {
return helloWorld, nil
})
go g.Run()
time.Sleep(10 * time.Millisecond)
err := g.Shutdown(t.Context())
require.NoError(t, err, "Test_Shutdown Failed!")
})
assert.Contains(t, logs, "Application shutdown complete", "Test_Shutdown Failed!")
}
func TestApp_SubscriberInitialize(t *testing.T) {
t.Run("subscriber is initialized", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
mockContainer := container.Container{
Logger: logging.NewLogger(logging.ERROR),
PubSub: mockSubscriber{},
}
app.container = &mockContainer
app.Subscribe("Hello", func(*Context) error {
// this is a test subscriber
return nil
})
_, ok := app.subscriptionManager.subscriptions["Hello"]
assert.True(t, ok)
})
t.Run("subscriber is not initialized", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
app.Subscribe("Hello", func(*Context) error {
// this is a test subscriber
return nil
})
_, ok := app.subscriptionManager.subscriptions["Hello"]
assert.False(t, ok)
})
}
func TestApp_Subscribe(t *testing.T) {
t.Run("topic is empty", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
mockContainer := container.Container{
Logger: logging.NewLogger(logging.ERROR),
PubSub: mockSubscriber{},
}
app.container = &mockContainer
app.Subscribe("", func(*Context) error { return nil })
_, ok := app.subscriptionManager.subscriptions[""]
assert.False(t, ok)
})
t.Run("handler is nil", func(t *testing.T) {
testutil.NewServerConfigs(t)
app := New()
mockContainer := container.Container{
Logger: logging.NewLogger(logging.ERROR),
PubSub: mockSubscriber{},
}
app.container = &mockContainer
app.Subscribe("Hello", nil)
_, ok := app.subscriptionManager.subscriptions["Hello"]
assert.False(t, ok)
})
}
// Define static error for testing.
var errHookFailed = errors.New("hook failed")
func TestApp_OnStart(t *testing.T) {
// Test case 1: Hook executes successfully
t.Run("success", func(t *testing.T) {
var hookCalled bool
app := New()
app.OnStart(func(_ *Context) error {
hookCalled = true
return nil
})
err := app.runOnStartHooks(t.Context())
require.NoError(t, err, "Expected no error from runOnStartHooks")
assert.True(t, hookCalled, "Expected the OnStart hook to be called")
})
// Test case 2: Hook returns an error
t.Run("error", func(t *testing.T) {
app := New()
app.OnStart(func(_ *Context) error {
return errHookFailed
})
err := app.runOnStartHooks(t.Context())
require.ErrorIs(t, err, errHookFailed, "Expected an error from runOnStartHooks")
})
// Test case 4: Verify panic recovery
t.Run("panic recovery", func(t *testing.T) {
app := New()
app.OnStart(func(_ *Context) error {
panic("test panic")
})
err := app.runOnStartHooks(t.Context())
require.Error(t, err, "Expected error from panicked hook")
assert.Contains(t, err.Error(), "panicked", "Expected error message to mention panic")
})
}
func TestUnifiedAuthenticationRegistration(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
t.Setenv("HTTP_PORT", strconv.Itoa(testutil.GetFreePort(t)))
app := New()
// Enable various auth methods
app.EnableBasicAuth("user", "pass")
app.EnableAPIKeyAuth("key1")
app.EnableOAuth("http://jwks", 3600)
// Verify HTTP middleware count (approximate check)
// We can't easily inspect the router's middleware slice directly without reflection or exposing it,
// but we can check if the grpcServer has interceptors added.
assert.GreaterOrEqual(t, len(app.grpcServer.interceptors), 2, "gRPC unary interceptors should be registered")
assert.GreaterOrEqual(t, len(app.grpcServer.streamInterceptors), 2, "gRPC stream interceptors should be registered")
}
================================================
FILE: pkg/gofr/graphql.go
================================================
package gofr
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime/debug"
"strings"
"sync"
"time"
"github.com/graphql-go/graphql"
"github.com/vektah/gqlparser/v2"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/parser"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/container"
)
var (
errSchemaMissing = errors.New("GraphQL schema file missing: ./configs/schema.graphqls")
errResolverMissing = errors.New("resolver missing for field")
)
const (
graphqlString = "String"
graphqlID = "ID"
graphqlInt = "Int"
graphqlFloat = "Float"
graphqlBoolean = "Boolean"
graphqlQuery = "query"
graphqlMutation = "mutation"
graphqlSuccess = "success"
graphqlError = "error"
graphqlUnknown = "unknown"
)
// GraphQLLog represents a logged GraphQL resolver execution.
type GraphQLLog struct {
Resolver string `json:"resolver"`
Type string `json:"type"`
Duration int64 `json:"duration"`
Error string `json:"error,omitempty"`
}
func (l *GraphQLLog) PrettyPrint(writer io.Writer) {
opType := "GraphQL Query"
if l.Type == graphqlMutation {
opType = "GraphQL Mutation"
}
if l.Error != "" {
fmt.Fprintf(writer, "\u001B[38;5;202m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s: %s \n",
"FAIL", l.Duration, opType, l.Resolver, l.Error)
return
}
fmt.Fprintf(writer, "\u001B[38;5;34m%-6s\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s \n",
"OK", l.Duration, opType, l.Resolver)
}
type graphQLManager struct {
container *container.Container
queries map[string]Handler
mutations map[string]Handler
schema graphql.Schema
mu sync.RWMutex
tracer trace.Tracer
typeCache map[string]graphql.Output
enumCache map[string]*graphql.Enum
}
func newGraphQLManager(c *container.Container) *graphQLManager {
// GraphQL metrics are registered here (not in registerFrameworkMetrics) because
// GraphQL is an opt-in feature. Metrics are only created when resolvers are registered.
c.Metrics().NewCounter("app_graphql_operations_total", "Total number of GraphQL operations received.")
c.Metrics().NewCounter("app_graphql_error_total", "Total number of GraphQL operations that returned an error.")
c.Metrics().NewHistogram("app_graphql_request_duration", "Response time of GraphQL requests in seconds.",
.001, .003, .005, .01, .02, .03, .05, .1, .2, .3, .5, .75, 1, 2, 3, 5, 10, 30) //nolint:mnd // histogram buckets
return &graphQLManager{
container: c,
queries: make(map[string]Handler),
mutations: make(map[string]Handler),
tracer: otel.Tracer("gofr-graphql"),
typeCache: make(map[string]graphql.Output),
enumCache: make(map[string]*graphql.Enum),
}
}
func (m *graphQLManager) RegisterQuery(name string, handler Handler) {
m.mu.Lock()
defer m.mu.Unlock()
m.queries[name] = handler
}
func (m *graphQLManager) RegisterMutation(name string, handler Handler) {
m.mu.Lock()
defer m.mu.Unlock()
m.mutations[name] = handler
}
func (m *graphQLManager) buildSchema() error {
schemaContent, err := os.ReadFile("./configs/schema.graphqls")
if err != nil {
if os.IsNotExist(err) {
return errSchemaMissing
}
return err
}
// Parse SDL using gqlparser
src := &ast.Source{
Name: "schema.graphqls",
Input: string(schemaContent),
}
gqlSchema, gqlErr := gqlparser.LoadSchema(src)
if gqlErr != nil {
return fmt.Errorf("failed to parse schema: %w", gqlErr)
}
// Bridge gqlparser AST to graphql-go Schema
queryFields, err := m.buildFields(gqlSchema.Query, m.queries, gqlSchema)
if err != nil {
return err
}
mutationFields, err := m.buildFields(gqlSchema.Mutation, m.mutations, gqlSchema)
if err != nil {
return err
}
schemaConfig := graphql.SchemaConfig{
Query: graphql.NewObject(graphql.ObjectConfig{
Name: "Query",
Fields: queryFields,
}),
}
if len(mutationFields) > 0 {
schemaConfig.Mutation = graphql.NewObject(graphql.ObjectConfig{
Name: "Mutation",
Fields: mutationFields,
})
}
m.schema, err = graphql.NewSchema(schemaConfig)
if err != nil {
return fmt.Errorf("failed to build graphql-go schema: %w", err)
}
return nil
}
func (m *graphQLManager) buildFields(obj *ast.Definition, handlers map[string]Handler, schema *ast.Schema) (graphql.Fields, error) {
fields := graphql.Fields{}
if obj == nil {
return fields, nil
}
for _, field := range obj.Fields {
m.mu.RLock()
handler, ok := handlers[field.Name]
m.mu.RUnlock()
if !ok {
if strings.HasPrefix(field.Name, "__") {
// Skip internal GraphQL fields like __schema, __type, etc.
continue
}
return nil, fmt.Errorf("%w: %s", errResolverMissing, field.Name)
}
fields[field.Name] = &graphql.Field{
Type: m.mapType(field.Type, schema),
Args: m.mapArgs(field.Arguments, schema),
Resolve: m.getResolver(field.Name, handler),
}
}
return fields, nil
}
func (m *graphQLManager) mapArgs(args ast.ArgumentDefinitionList, schema *ast.Schema) graphql.FieldConfigArgument {
res := graphql.FieldConfigArgument{}
for _, arg := range args {
res[arg.Name] = &graphql.ArgumentConfig{
Type: m.mapInputType(arg.Type, schema),
}
}
return res
}
func (m *graphQLManager) mapInputType(t *ast.Type, schema *ast.Schema) graphql.Input {
var coreType graphql.Input
coreType = m.getCoreInputType(t.Name(), schema)
if t.Elem != nil {
coreType = graphql.NewList(m.mapInputType(t.Elem, schema))
}
if t.NonNull {
return graphql.NewNonNull(coreType)
}
return coreType
}
func (m *graphQLManager) getCoreInputType(name string, schema *ast.Schema) graphql.Input {
switch name {
case graphqlString, graphqlID:
return graphql.String
case graphqlInt:
return graphql.Int
case graphqlFloat:
return graphql.Float
case graphqlBoolean:
return graphql.Boolean
default:
return m.getCustomInputType(name, schema)
}
}
func (m *graphQLManager) getCustomInputType(name string, schema *ast.Schema) graphql.Input {
def, ok := schema.Types[name]
if ok && def.Kind == ast.InputObject {
fields := graphql.InputObjectConfigFieldMap{}
for _, f := range def.Fields {
fields[f.Name] = &graphql.InputObjectFieldConfig{
Type: m.mapInputType(f.Type, schema),
}
}
return graphql.NewInputObject(graphql.InputObjectConfig{
Name: name,
Fields: fields,
})
}
if def != nil && def.Kind == ast.Enum {
return m.getEnum(def)
}
m.container.Errorf("unsupported GraphQL input type: %s", name)
return graphql.String // Fallback
}
func (m *graphQLManager) getEnum(def *ast.Definition) *graphql.Enum {
if e, ok := m.enumCache[def.Name]; ok {
return e
}
config := graphql.EnumConfig{
Name: def.Name,
Values: graphql.EnumValueConfigMap{},
}
for _, val := range def.EnumValues {
config.Values[val.Name] = &graphql.EnumValueConfig{
Value: val.Name,
}
}
e := graphql.NewEnum(config)
m.enumCache[def.Name] = e
return e
}
func (m *graphQLManager) mapType(t *ast.Type, schema *ast.Schema) graphql.Output {
var coreType graphql.Output
if t.Elem != nil {
coreType = graphql.NewList(m.mapType(t.Elem, schema))
} else if gqlType, ok := m.typeCache[t.Name()]; ok {
coreType = gqlType
} else {
coreType = m.getCoreOutputType(t.Name(), schema)
}
if t.NonNull {
return graphql.NewNonNull(coreType)
}
return coreType
}
func (m *graphQLManager) getCoreOutputType(name string, schema *ast.Schema) graphql.Output {
switch name {
case graphqlString, graphqlID:
return graphql.String
case graphqlInt:
return graphql.Int
case graphqlFloat:
return graphql.Float
case graphqlBoolean:
return graphql.Boolean
default:
return m.getCustomOutputType(name, schema)
}
}
func (m *graphQLManager) getCustomOutputType(name string, schema *ast.Schema) graphql.Output {
def, ok := schema.Types[name]
if def != nil && def.Kind == ast.Enum {
return m.getEnum(def)
}
if !ok || def.Kind != ast.Object {
m.container.Errorf("unsupported GraphQL output type: %s, defaulting to String", name)
return graphql.String // Fallback
}
obj := graphql.NewObject(graphql.ObjectConfig{
Name: name,
Fields: graphql.Fields{},
})
// Cache it immediately to avoid infinite recursion for circular references
m.typeCache[name] = obj
for _, f := range def.Fields {
obj.AddFieldConfig(f.Name, &graphql.Field{
Type: m.mapType(f.Type, schema),
})
}
return obj
}
// getResolver binds a handler of type gofr.Handler to a GraphQL field.
// Arguments are accessed inside the handler via c.Bind().
func (m *graphQLManager) getResolver(name string, h Handler) graphql.FieldResolveFn {
return func(p graphql.ResolveParams) (any, error) {
ctx, span := m.tracer.Start(p.Context, "graphql-resolver-"+name)
defer span.End()
start := time.Now()
gReq := &graphQLRequest{ctx: ctx, params: p.Args}
c := newContext(noopResponder{}, gReq, m.container)
res, err := h(c)
duration := time.Since(start).Microseconds()
resolverType := m.getResolverType(name)
if err != nil {
c.Error(&GraphQLLog{Resolver: name, Type: resolverType, Duration: duration, Error: err.Error()})
return nil, err
}
c.Debug(&GraphQLLog{Resolver: name, Type: resolverType, Duration: duration})
return res, nil
}
}
func (m *graphQLManager) getResolverType(name string) string {
m.mu.RLock()
defer m.mu.RUnlock()
if _, ok := m.mutations[name]; ok {
return graphqlMutation
}
return graphqlQuery
}
const maxRequestBodySize = 32 << 20 // 32 MB
func (m *graphQLManager) Handle(w http.ResponseWriter, r *http.Request) {
// Standard request protection - addressing review point 6 (bypassing middleware benefits)
// Apply body size limit (using 32MB default as per GoFr's multipart decoder)
r.Body = http.MaxBytesReader(w, r.Body, maxRequestBodySize)
// Standard panic recovery for raw HTTP handler
defer func() {
if re := recover(); re != nil {
m.container.Errorf("GraphQL Panic: %v\n%s", re, string(debug.Stack()))
m.respondWithErrors(w, http.StatusInternalServerError, "Internal Server Error")
}
}()
m.handleGraphQLRequest(w, r)
}
func (m *graphQLManager) handleGraphQLRequest(w http.ResponseWriter, r *http.Request) {
ct := r.Header.Get("Content-Type")
if ct != "" && !strings.HasPrefix(ct, "application/json") {
m.respondWithErrors(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json")
return
}
start := time.Now()
req, err := m.parseGraphQLRequest(r)
if err != nil {
m.container.Metrics().IncrementCounter(r.Context(), "app_graphql_error_total", "operation_name", graphqlUnknown, "type", graphqlUnknown)
m.respondWithErrors(w, http.StatusBadRequest, "invalid JSON request body")
return
}
opName, opType := m.parseOperation(req.Query, req.OperationName)
ctx, span := m.tracer.Start(r.Context(), "graphql-request")
span.SetAttributes(attribute.String("graphql.operation_name", opName), attribute.String("graphql.operation_type", opType))
var result *graphql.Result
defer func() {
status := graphqlSuccess
if result != nil && len(result.Errors) > 0 {
status = graphqlError
}
m.container.Metrics().RecordHistogram(ctx, "app_graphql_request_duration",
time.Since(start).Seconds(), "operation_name", opName, "type", opType, "status", status)
span.End()
}()
m.container.Metrics().IncrementCounter(ctx, "app_graphql_operations_total", "operation_name", opName, "type", opType)
result = graphql.Do(graphql.Params{
Schema: m.schema,
RequestString: req.Query,
VariableValues: req.Variables,
OperationName: req.OperationName,
Context: ctx,
})
w.Header().Set("Content-Type", "application/json")
if len(result.Errors) > 0 {
m.container.Metrics().IncrementCounter(ctx, "app_graphql_error_total", "operation_name", opName, "type", opType)
}
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(result)
if err != nil {
m.container.Errorf("error encoding GraphQL response: %v", err)
}
}
type gqlRequest struct {
Query string `json:"query"`
OperationName string `json:"operationName"`
Variables map[string]any `json:"variables"`
}
func (*graphQLManager) parseGraphQLRequest(r *http.Request) (gqlRequest, error) {
var req gqlRequest
err := json.NewDecoder(r.Body).Decode(&req)
return req, err
}
func (*graphQLManager) parseOperation(query, operationName string) (opName, opType string) {
opName = operationName
opType = graphqlQuery
astDoc, err := parser.ParseQuery(&ast.Source{Input: query})
if err != nil || len(astDoc.Operations) == 0 {
if opName == "" {
opName = graphqlUnknown
}
return opName, opType
}
op := astDoc.Operations[0]
opType = string(op.Operation)
if opName == "" && len(op.SelectionSet) > 0 {
if field, ok := op.SelectionSet[0].(*ast.Field); ok {
opName = field.Name
}
}
if opName == "" {
opName = graphqlUnknown
}
return opName, opType
}
func (*graphQLManager) respondWithErrors(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"errors": []map[string]any{
{"message": message},
},
})
}
func (m *graphQLManager) GetHandler() http.Handler {
return http.HandlerFunc(m.Handle)
}
// graphQLRequest implements the gofr.Request interface for GraphQL.
type graphQLRequest struct {
ctx context.Context
params map[string]any
}
func (r *graphQLRequest) Param(name string) string {
if v, ok := r.params[name]; ok {
return fmt.Sprintf("%v", v)
}
return ""
}
func (*graphQLRequest) PathParam(string) string { return "" }
func (r *graphQLRequest) Bind(v any) error {
b, err := json.Marshal(r.params)
if err != nil {
return err
}
return json.Unmarshal(b, v)
}
func (r *graphQLRequest) Context() context.Context { return r.ctx }
func (*graphQLRequest) HostName() string { return "" }
func (*graphQLRequest) Params(string) []string { return nil }
const graphiqlHTML = `
GoFr GraphQL Playground
`
================================================
FILE: pkg/gofr/graphql_test.go
================================================
package gofr
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func setupSchema(t *testing.T, content string) string {
t.Helper()
tmpDir := t.TempDir()
configDir := filepath.Join(tmpDir, "configs")
err := os.MkdirAll(configDir, 0755)
require.NoError(t, err)
err = os.WriteFile(filepath.Join(configDir, "schema.graphqls"), []byte(content), 0600)
require.NoError(t, err)
return tmpDir
}
func TestGraphQL_Query(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
setupSchema(t, `type Query { hello: String }`)
app := New()
tmpDir := setupSchema(t, `type Query { hello: String }`)
t.Chdir(tmpDir)
app.GraphQLQuery("hello", func(_ *Context) (any, error) {
return "world", nil
})
reqBody := `{"query": "{ hello }"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
var result struct {
Data struct {
Hello string `json:"hello"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, "world", result.Data.Hello)
}
func TestGraphQL_Mutation(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
tmpDir := setupSchema(t, `type User { id: Int name: String } type Query { dummy: String }
type Mutation { createUser(name: String): User }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("dummy", func(_ *Context) (any, error) { return "ok", nil })
app.GraphQLMutation("createUser", func(c *Context) (any, error) {
var args struct {
Name string `json:"name"`
}
err := c.Bind(&args)
if err != nil {
return nil, err
}
return map[string]any{"id": 1, "name": args.Name}, nil
})
reqBody := `{"query": "mutation { createUser(name: \"test\") { id name } }"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var result struct {
Data struct {
CreateUser User `json:"createUser"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, 1, result.Data.CreateUser.ID)
assert.Equal(t, "test", result.Data.CreateUser.Name)
}
func TestGraphQL_Playground(t *testing.T) {
tests := []struct {
desc string
appEnv string
expectedCode int
}{
{"Development Environment", "development", http.StatusOK},
{"Production Environment", "production", http.StatusOK},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
t.Setenv("APP_ENV", tc.appEnv)
tmpDir := setupSchema(t, `type Query { dummy: String }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("dummy", func(_ *Context) (any, error) { return "ok", nil })
// Internal call to setup router as App.Run would do
app.httpServerSetup()
req := httptest.NewRequest(http.MethodGet, "/.well-known/graphql/ui", http.NoBody)
resp := httptest.NewRecorder()
app.httpServer.router.ServeHTTP(resp, req)
assert.Equal(t, tc.expectedCode, resp.Code)
if tc.expectedCode == http.StatusOK {
assert.Contains(t, resp.Body.String(), "GoFr GraphQL Playground")
}
})
}
}
func TestGraphQL_ArgumentTypes(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
tmpDir := setupSchema(t, `type Query {
user(id: Int, score: Float, isAdmin: Boolean, tags: [String]): DetailedUser
}
type DetailedUser { id: Int score: Float isAdmin: Boolean tags: [String] }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("user", func(c *Context) (any, error) {
var args struct {
ID int `json:"id"`
Score float64 `json:"score"`
IsAdmin bool `json:"isAdmin"`
Tags []string `json:"tags"`
}
err := c.Bind(&args)
if err != nil {
return nil, err
}
return args, nil
})
reqBody := `{"query": "{ user(id: 1, score: 9.5, isAdmin: true, tags: [\"a\", \"b\"]) { id score isAdmin tags } }"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
type DetailedUser struct {
ID int `json:"id"`
Score float64 `json:"score"`
IsAdmin bool `json:"isAdmin"`
Tags []string `json:"tags"`
}
var result struct {
Data struct {
User DetailedUser `json:"user"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, 1, result.Data.User.ID)
assert.InDelta(t, 9.5, result.Data.User.Score, 0.001)
assert.True(t, result.Data.User.IsAdmin)
assert.Equal(t, []string{"a", "b"}, result.Data.User.Tags)
}
func TestGraphQL_BuildFailure(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
// No schema file setup, should fail
app := New()
app.GraphQLQuery("hello", func(_ *Context) (any, error) { return "world", nil })
err := app.graphqlManager.buildSchema()
assert.Error(t, err)
}
func TestGraphQL_ResolverError(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
tmpDir := setupSchema(t, `type Query { fail: String }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("fail", func(_ *Context) (any, error) {
return nil, assert.AnError
})
reqBody := `{"query": "{ fail }"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
var result struct {
Data any `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.NotEmpty(t, result.Errors)
assert.Contains(t, result.Errors[0].Message, assert.AnError.Error())
}
func TestGraphQL_RequestMethods(t *testing.T) {
params := map[string]any{"id": 1, "name": "test"}
req := &graphQLRequest{ctx: context.Background(), params: params}
assert.Equal(t, "1", req.Param("id"))
assert.Equal(t, "test", req.Param("name"))
assert.Empty(t, req.Param("invalid"))
assert.Empty(t, req.PathParam("any"))
assert.Empty(t, req.HostName())
assert.Nil(t, req.Params("any"))
assert.NotNil(t, req.Context())
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
var u User
err := req.Bind(&u)
require.NoError(t, err)
assert.Equal(t, 1, u.ID)
assert.Equal(t, "test", u.Name)
}
func TestGraphQL_Enums(t *testing.T) {
t.Setenv("METRICS_PORT", "0")
tmpDir := setupSchema(t, `
enum Role { ADMIN USER }
type User { id: Int role: Role }
type Query { user(role: Role): User }
`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("user", func(c *Context) (any, error) {
var args struct {
Role string `json:"role"`
}
err := c.Bind(&args)
if err != nil {
return nil, err
}
return map[string]any{"id": 1, "role": args.Role}, nil
})
reqBody := `{"query": "{ user(role: ADMIN) { id role } }"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
assert.Equal(t, http.StatusOK, resp.Code)
var result struct {
Data struct {
User struct {
ID int `json:"id"`
Role string `json:"role"`
} `json:"user"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, "ADMIN", result.Data.User.Role)
}
func TestGraphQL_OperationName(t *testing.T) {
tmpDir := setupSchema(t, `type Query { a: String, b: String }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("a", func(_ *Context) (any, error) { return "valA", nil })
app.GraphQLQuery("b", func(_ *Context) (any, error) { return "valB", nil })
// Document with multiple named operations
reqBody := `{"query": "query QueryA { a } query QueryB { b }", "operationName": "QueryB"}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
var result struct {
Data struct {
B string `json:"b"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, "valB", result.Data.B)
}
func TestGraphQL_Variables(t *testing.T) {
tmpDir := setupSchema(t, `type Query { user(id: Int): User } type User { id: Int }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("user", func(c *Context) (any, error) {
var args struct {
ID int `json:"id"`
}
err := c.Bind(&args)
if err != nil {
return nil, err
}
return args, nil
})
reqBody := `{"query": "query GetUser($id: Int) { user(id: $id) { id } }", "variables": {"id": 123}}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
var result struct {
Data struct {
User struct {
ID int `json:"id"`
} `json:"user"`
} `json:"data"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.Equal(t, 123, result.Data.User.ID)
}
func TestGraphQL_MalformedQuery(t *testing.T) {
tmpDir := setupSchema(t, `type Query { hello: String }`)
t.Chdir(tmpDir)
app := New()
app.GraphQLQuery("hello", func(_ *Context) (any, error) { return "ok", nil })
reqBody := `{"query": "{ malformed "}`
req := httptest.NewRequest(http.MethodPost, "/graphql", bytes.NewBufferString(reqBody))
req.Header.Set("Content-Type", "application/json")
resp := httptest.NewRecorder()
err := app.graphqlManager.buildSchema()
require.NoError(t, err)
app.graphqlManager.Handle(resp, req)
var result struct {
Errors []any `json:"errors"`
}
err = json.Unmarshal(resp.Body.Bytes(), &result)
require.NoError(t, err)
assert.NotEmpty(t, result.Errors)
}
================================================
FILE: pkg/gofr/grpc/log.go
================================================
// Package grpc provides gRPC-related additions within the GoFr framework.
package grpc
import (
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
statusCodeWidth = 3
responseTimeWidth = 11
nanosecondsPerMillisecond = 1e6
debugMethod = "/grpc.health.v1.Health/SetServingStatus"
healthCheck = "/grpc.health.v1.Health/Check"
clientStreamSuffix = " [CLIENT-STREAM]"
serverStreamSuffix = " [SERVER-STREAM]"
bidirectionalSuffix = " [BI-DIRECTION_STREAM]"
)
type Logger interface {
Info(args ...any)
Errorf(string, ...any)
Debug(...any)
Fatalf(string, ...any)
}
type Metrics interface {
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
}
type gRPCLog struct {
ID string `json:"id"`
StartTime string `json:"startTime"`
ResponseTime int64 `json:"responseTime"`
Method string `json:"method"`
StatusCode int32 `json:"statusCode"`
StreamType string `json:"streamType,omitempty"`
}
//nolint:revive // We intend to keep it non-exported for ease in future changes without any breaking change.
func NewgRPCLogger() gRPCLog {
return gRPCLog{}
}
func (l *gRPCLog) PrettyPrint(writer io.Writer) {
streamInfo := ""
if l.StreamType != "" {
streamInfo = fmt.Sprintf(" [%s]", l.StreamType)
}
fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-*d"+
"\u001B[0m %*d\u001B[38;5;8mµs\u001B[0m %s%s %s\n",
l.ID, colorForGRPCCode(l.StatusCode),
statusCodeWidth, l.StatusCode,
responseTimeWidth, l.ResponseTime,
"GRPC", streamInfo, l.Method)
}
func colorForGRPCCode(s int32) int {
const (
blue = 34
red = 202
)
if s == 0 {
return blue
}
return red
}
func (l gRPCLog) String() string {
line, _ := json.Marshal(l)
return string(line)
}
// StreamObservabilityInterceptor handles logging, metrics, and tracing for streaming RPCs.
func StreamObservabilityInterceptor(logger Logger, metrics Metrics) grpc.StreamServerInterceptor {
tracer := otel.GetTracerProvider().Tracer("gofr-gRPC-stream", trace.WithInstrumentationVersion("v0.1"))
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
start := time.Now()
// Initialize tracing context from incoming metadata
ctx := initializeSpanContext(ss.Context())
ctx, span := tracer.Start(ctx, info.FullMethod)
defer span.End()
// Wrap the stream to propagate context with tracing
wrappedStream := &wrappedServerStream{
ServerStream: ss,
ctx: ctx,
}
// Process the stream
err := handler(srv, wrappedStream)
streamType, grpcMethodName := getStreamTypeAndMethod(info)
// Log and record metrics
logStreamRPC(ctx, logger, metrics, start, err, grpcMethodName, streamType, "app_gRPC-Stream_stats")
return err
}
}
// getStreamTypeAndMethod determines the stream type and formats the method name.
func getStreamTypeAndMethod(info *grpc.StreamServerInfo) (streamType, methodName string) {
methodName = info.FullMethod
switch {
case info.IsClientStream && info.IsServerStream:
streamType = "BIDIRECTIONAL"
methodName += bidirectionalSuffix
case info.IsClientStream:
streamType = "CLIENT_STREAM"
methodName += clientStreamSuffix
case info.IsServerStream:
streamType = "SERVER_STREAM"
methodName += serverStreamSuffix
}
return streamType, methodName
}
// wrappedServerStream propagates context with tracing for streaming RPCs.
type wrappedServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (w *wrappedServerStream) Context() context.Context {
return w.ctx
}
func ObservabilityInterceptor(logger Logger, metrics Metrics) grpc.UnaryServerInterceptor {
tracer := otel.GetTracerProvider().Tracer("gofr", trace.WithInstrumentationVersion("v0.1"))
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
ctx = initializeSpanContext(ctx)
ctx, span := tracer.Start(ctx, info.FullMethod)
resp, err := handler(ctx, req)
if err != nil && isServerError(err) {
logger.Errorf("error while handling gRPC request to method %q: %q", info.FullMethod, err)
}
if info.FullMethod == healthCheck {
service, ok := req.(*grpc_health_v1.HealthCheckRequest)
if ok {
info.FullMethod = fmt.Sprintf("%s Service: %q", healthCheck, service.Service)
}
}
logRPC(ctx, logger, metrics, start, err, info.FullMethod, "app_gRPC-Server_stats")
span.End()
return resp, err
}
}
func initializeSpanContext(ctx context.Context) context.Context {
md, _ := metadata.FromIncomingContext(ctx)
traceIDHex := getMetadataValue(md, "x-gofr-traceid")
spanIDHex := getMetadataValue(md, "x-gofr-spanid")
if traceIDHex == "" || spanIDHex == "" {
return ctx
}
traceID, _ := trace.TraceIDFromHex(traceIDHex)
spanID, _ := trace.SpanIDFromHex(spanIDHex)
spanContext := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
Remote: true,
})
ctx = trace.ContextWithRemoteSpanContext(ctx, spanContext)
return ctx
}
func (gRPCLog) DocumentRPCLog(ctx context.Context, logger Logger, metrics Metrics, start time.Time, err error, method, name string) {
logRPC(ctx, logger, metrics, start, err, method, name)
}
func logStreamRPC(ctx context.Context, logger Logger, metrics Metrics, start time.Time, err error, method, streamType, metricName string) {
duration := time.Since(start)
logEntry := gRPCLog{
ID: getTraceID(ctx),
StartTime: start.Format("2006-01-02T15:04:05.999999999-07:00"),
ResponseTime: duration.Microseconds(),
Method: method,
StreamType: streamType,
}
if err != nil {
statusErr, _ := status.FromError(err)
//nolint:gosec //errorcode is guaranteed to be in safe range
logEntry.StatusCode = int32(statusErr.Code())
} else {
logEntry.StatusCode = int32(codes.OK)
}
logGRPCEntry(logger, &logEntry, method)
recordGRPCMetrics(ctx, metrics, metricName, duration, method, streamType)
}
func logRPC(ctx context.Context, logger Logger, metrics Metrics, start time.Time, err error, method, name string) {
duration := time.Since(start)
logEntry := gRPCLog{
ID: trace.SpanFromContext(ctx).SpanContext().TraceID().String(),
StartTime: start.Format("2006-01-02T15:04:05.999999999-07:00"),
ResponseTime: duration.Microseconds(),
Method: method,
}
if err != nil {
statusErr, _ := status.FromError(err)
//nolint:gosec // gRPC codes are typically under the range.
logEntry.StatusCode = int32(statusErr.Code())
} else {
logEntry.StatusCode = int32(codes.OK)
}
logGRPCEntry(logger, &logEntry, method)
recordGRPCMetrics(ctx, metrics, name, duration, method, "")
}
// Helper function to extract trace ID from context.
func getTraceID(ctx context.Context) string {
if ctx == nil {
return ""
}
span := trace.SpanFromContext(ctx)
if span == nil {
return ""
}
return span.SpanContext().TraceID().String()
}
// logGRPCEntry handles the actual logging with improved logic.
func logGRPCEntry(logger Logger, logEntry *gRPCLog, method string) {
if logger == nil {
return
}
switch {
case method == debugMethod,
strings.Contains(method, "/Send"),
strings.Contains(method, "/Recv"),
strings.Contains(method, "/SendAndClose"):
logger.Debug(logEntry)
default:
logger.Info(logEntry)
}
}
func recordGRPCMetrics(ctx context.Context, metrics Metrics, name string, duration time.Duration, method, streamType string) {
if metrics == nil {
return
}
durationMs := float64(duration.Milliseconds()) + float64(duration.Nanoseconds()%nanosecondsPerMillisecond)/nanosecondsPerMillisecond
labels := []string{"method", method}
if streamType != "" {
labels = append(labels, "stream_type", streamType)
}
metrics.RecordHistogram(ctx, name, durationMs, labels...)
}
// isServerError returns true if the gRPC error represents a server-side error.
// Client errors like ResourceExhausted, InvalidArgument, NotFound, etc. are not
// considered server errors and should not be logged at ERROR level.
func isServerError(err error) bool {
s, ok := status.FromError(err)
if !ok {
return true
}
switch s.Code() {
case codes.InvalidArgument, codes.NotFound, codes.AlreadyExists,
codes.PermissionDenied, codes.Unauthenticated, codes.ResourceExhausted,
codes.FailedPrecondition, codes.OutOfRange, codes.Canceled:
return false
case codes.OK, codes.Unknown, codes.DeadlineExceeded, codes.Aborted,
codes.Unimplemented, codes.Internal, codes.Unavailable, codes.DataLoss:
return true
default:
return true
}
}
// Helper function to safely extract a value from metadata.
func getMetadataValue(md metadata.MD, key string) string {
if values, ok := md[key]; ok && len(values) > 0 {
return values[0]
}
return ""
}
================================================
FILE: pkg/gofr/grpc/log_test.go
================================================
package grpc
import (
"bytes"
"context"
"net/http"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestNewgRPCLogger(t *testing.T) {
logger := NewgRPCLogger()
assert.Equal(t, gRPCLog{}, logger)
}
func TestRPCLog_String(t *testing.T) {
l := gRPCLog{
ID: "123",
StartTime: "2020-01-01T12:12:12",
Method: http.MethodGet,
StatusCode: 0,
}
expLog := `{"id":"123","startTime":"2020-01-01T12:12:12","responseTime":0,"method":"GET","statusCode":0}`
assert.Equal(t, expLog, l.String())
}
func TestRPCLog_StringWithStreamType(t *testing.T) {
l := gRPCLog{
ID: "123",
StartTime: "2020-01-01T12:12:12",
Method: "/test.Service/Method",
StatusCode: 0,
StreamType: "CLIENT_STREAM",
}
expLog := `{"id":"123","startTime":"2020-01-01T12:12:12","responseTime":0,` +
`"method":"/test.Service/Method","statusCode":0,"streamType":"CLIENT_STREAM"}`
assert.Equal(t, expLog, l.String())
}
func Test_colorForGRPCCode(t *testing.T) {
testCases := []struct {
desc string
code int32
colorCode int
}{
{"code 0", 0, 34},
{"negative code", -1, 202},
{"positive code", 1, 202},
}
for i, tc := range testCases {
response := colorForGRPCCode(tc.code)
assert.Equal(t, tc.colorCode, response, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestRPCLog_PrettyPrint(t *testing.T) {
startTime := time.Now().String()
log := testutil.StdoutOutputForFunc(func() {
l := gRPCLog{
ID: "1",
StartTime: startTime,
ResponseTime: 10,
Method: http.MethodGet,
StatusCode: 34,
}
l.PrettyPrint(os.Stdout)
})
// Check if method is coming
assert.Contains(t, log, `GET`)
// Check if responseTime is coming
assert.Contains(t, log, `10`)
// Check if statusCode is coming
assert.Contains(t, log, `34`)
// Check if ID is coming
assert.Contains(t, log, `1`)
}
func TestRPCLog_PrettyPrintWithStreamType(t *testing.T) {
var buf bytes.Buffer
l := gRPCLog{
ID: "1",
StartTime: "2023-01-01T12:00:00Z",
ResponseTime: 100,
Method: "/test.Service/Method",
StatusCode: 0,
StreamType: "SERVER_STREAM",
}
l.PrettyPrint(&buf)
output := buf.String()
assert.Contains(t, output, "[SERVER_STREAM]")
assert.Contains(t, output, "/test.Service/Method")
}
func TestGetStreamTypeAndMethod(t *testing.T) {
testCases := []struct {
desc string
info *grpc.StreamServerInfo
expectedType string
expectedMethod string
}{
{
desc: "bidirectional stream",
info: &grpc.StreamServerInfo{
FullMethod: "/test.Service/Method",
IsClientStream: true,
IsServerStream: true,
},
expectedType: "BIDIRECTIONAL",
expectedMethod: "/test.Service/Method [BI-DIRECTION_STREAM]",
},
{
desc: "client stream",
info: &grpc.StreamServerInfo{
FullMethod: "/test.Service/Method",
IsClientStream: true,
IsServerStream: false,
},
expectedType: "CLIENT_STREAM",
expectedMethod: "/test.Service/Method [CLIENT-STREAM]",
},
{
desc: "server stream",
info: &grpc.StreamServerInfo{
FullMethod: "/test.Service/Method",
IsClientStream: false,
IsServerStream: true,
},
expectedType: "SERVER_STREAM",
expectedMethod: "/test.Service/Method [SERVER-STREAM]",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
streamType, method := getStreamTypeAndMethod(tc.info)
assert.Equal(t, tc.expectedType, streamType)
assert.Equal(t, tc.expectedMethod, method)
})
}
}
func TestGetMetadataValue(t *testing.T) {
md := metadata.Pairs("key1", "value1", "key2", "value2")
assert.Equal(t, "value1", getMetadataValue(md, "key1"))
assert.Equal(t, "value2", getMetadataValue(md, "key2"))
assert.Empty(t, getMetadataValue(md, "nonexistent"))
}
func TestGetTraceID(t *testing.T) {
assert.Equal(t, "00000000000000000000000000000000", getTraceID(t.Context()))
assert.Equal(t, "00000000000000000000000000000000", getTraceID(t.Context()))
}
func TestWrappedServerStream_Context(t *testing.T) {
type contextKey string
originalCtx := t.Context()
newCtx := context.WithValue(originalCtx, contextKey("key"), "value")
wrapped := &wrappedServerStream{
ctx: newCtx,
}
assert.Equal(t, newCtx, wrapped.Context())
assert.Equal(t, "value", wrapped.Context().Value(contextKey("key")))
}
// Helper function to create mock logger and metrics for testing.
func createMocks(t *testing.T) (*container.MockLogger, *container.MockMetrics, *gomock.Controller) {
t.Helper()
ctrl := gomock.NewController(t)
mockLogger := container.NewMockLogger(ctrl)
mockMetrics := container.NewMockMetrics(ctrl)
return mockLogger, mockMetrics, ctrl
}
func TestGRPCLog_DocumentRPCLog(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
log := gRPCLog{}
ctx := t.Context()
start := time.Now()
err := status.Error(codes.Internal, "test error")
method := "test.method"
name := "test_metric"
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
log.DocumentRPCLog(ctx, mockLogger, mockMetrics, start, err, method, name)
}
func TestObservabilityInterceptor(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := ObservabilityInterceptor(mockLogger, mockMetrics)
ctx := t.Context()
req := "test request"
info := &grpc.UnaryServerInfo{
FullMethod: "/test.Service/Method",
}
handler := func(_ context.Context, _ any) (any, error) {
return "test response", nil
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
resp, err := interceptor(ctx, req, info, handler)
require.NoError(t, err)
assert.Equal(t, "test response", resp)
}
func TestObservabilityInterceptor_WithError(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := ObservabilityInterceptor(mockLogger, mockMetrics)
ctx := t.Context()
req := "test request"
info := &grpc.UnaryServerInfo{
FullMethod: "/test.Service/Method",
}
handler := func(_ context.Context, _ any) (any, error) {
return nil, status.Error(codes.Internal, "test error")
}
// Set up expectations - the function logs errors with Errorf and then with Info
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
resp, err := interceptor(ctx, req, info, handler)
require.Error(t, err)
assert.Nil(t, resp)
}
func TestObservabilityInterceptor_HealthCheck(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := ObservabilityInterceptor(mockLogger, mockMetrics)
ctx := t.Context()
req := &grpc_health_v1.HealthCheckRequest{
Service: "test-service",
}
info := &grpc.UnaryServerInfo{
FullMethod: healthCheck,
}
handler := func(_ context.Context, _ any) (any, error) {
return &grpc_health_v1.HealthCheckResponse{}, nil
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
resp, err := interceptor(ctx, req, info, handler)
require.NoError(t, err)
assert.NotNil(t, resp)
}
func TestStreamObservabilityInterceptor(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := StreamObservabilityInterceptor(mockLogger, mockMetrics)
info := &grpc.StreamServerInfo{
FullMethod: "/test.Service/Stream",
IsClientStream: false,
IsServerStream: true,
}
handler := func(_ any, _ grpc.ServerStream) error {
return nil
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
err := interceptor(nil, &mockServerStream{}, info, handler)
require.NoError(t, err)
}
func TestStreamObservabilityInterceptor_ClientStream(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := StreamObservabilityInterceptor(mockLogger, mockMetrics)
info := &grpc.StreamServerInfo{
FullMethod: "/test.Service/Stream",
IsClientStream: true,
IsServerStream: false,
}
handler := func(_ any, _ grpc.ServerStream) error {
return nil
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
err := interceptor(nil, &mockServerStream{}, info, handler)
require.NoError(t, err)
}
func TestStreamObservabilityInterceptor_BidirectionalStream(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := StreamObservabilityInterceptor(mockLogger, mockMetrics)
info := &grpc.StreamServerInfo{
FullMethod: "/test.Service/Stream",
IsClientStream: true,
IsServerStream: true,
}
handler := func(_ any, _ grpc.ServerStream) error {
return nil
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
err := interceptor(nil, &mockServerStream{}, info, handler)
require.NoError(t, err)
}
func TestStreamObservabilityInterceptor_WithError(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := StreamObservabilityInterceptor(mockLogger, mockMetrics)
info := &grpc.StreamServerInfo{
FullMethod: "/test.Service/Stream",
IsClientStream: false,
IsServerStream: true,
}
handler := func(_ any, _ grpc.ServerStream) error {
return status.Error(codes.Internal, "stream error")
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
err := interceptor(nil, &mockServerStream{}, info, handler)
require.Error(t, err)
}
func TestInitializeSpanContext_NoSpanCreated(t *testing.T) {
tests := []struct {
name string
ctx context.Context
}{
{
name: "no metadata",
ctx: t.Context(),
},
{
name: "missing trace id",
ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs(
"x-gofr-spanid", "1234567890123456",
)),
},
{
name: "missing span id",
ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs(
"x-gofr-traceid", "12345678901234567890123456789012",
)),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := initializeSpanContext(tt.ctx)
// When no trace context is created, the result should be the same as input
assert.Equal(t, tt.ctx, result)
})
}
}
func TestInitializeSpanContext_SpanCreated(t *testing.T) {
tests := []struct {
name string
ctx context.Context
expectValid bool
}{
{
name: "valid trace context",
ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs(
"x-gofr-traceid", "12345678901234567890123456789012",
"x-gofr-spanid", "1234567890123456",
)),
expectValid: true,
},
{
name: "invalid trace id",
ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs(
"x-gofr-traceid", "invalid",
"x-gofr-spanid", "1234567890123456",
)),
expectValid: false, // Function creates span context even with invalid hex
},
{
name: "invalid span id",
ctx: metadata.NewIncomingContext(t.Context(), metadata.Pairs(
"x-gofr-traceid", "12345678901234567890123456789012",
"x-gofr-spanid", "invalid",
)),
expectValid: false, // Function creates span context even with invalid hex
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := initializeSpanContext(tt.ctx)
// When trace context is created, the result should be different from input
assert.NotEqual(t, tt.ctx, result)
// Verify that a span context was added
span := trace.SpanFromContext(result)
assert.NotNil(t, span)
// Check if span context is valid based on test expectation
assert.Equal(t, tt.expectValid, span.SpanContext().IsValid())
})
}
}
func TestGetMetadataValue_Comprehensive(t *testing.T) {
tests := []struct {
name string
md metadata.MD
key string
expected string
}{
{
name: "key exists",
md: metadata.Pairs("test-key", "test-value"),
key: "test-key",
expected: "test-value",
},
{
name: "key does not exist",
md: metadata.Pairs("other-key", "other-value"),
key: "test-key",
expected: "",
},
{
name: "empty metadata",
md: metadata.MD{},
key: "test-key",
expected: "",
},
{
name: "multiple values",
md: metadata.Pairs("test-key", "value1", "test-key", "value2"),
key: "test-key",
expected: "value1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := getMetadataValue(tt.md, tt.key)
assert.Equal(t, tt.expected, result)
})
}
}
// Mock server stream for testing.
type mockServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (m *mockServerStream) Context() context.Context {
if m.ctx == nil {
return context.Background()
}
return m.ctx
}
func (*mockServerStream) SendMsg(_ any) error {
return nil
}
func (*mockServerStream) RecvMsg(_ any) error {
return nil
}
func TestWrappedServerStream_Context_Comprehensive(t *testing.T) {
type testKey string
ctx := context.WithValue(t.Context(), testKey("test-key"), "test-value")
wrapped := &wrappedServerStream{
ServerStream: &mockServerStream{},
ctx: ctx,
}
result := wrapped.Context()
assert.Equal(t, ctx, result)
}
// Additional tests to reach 100% coverage.
func TestGetTraceID_WithSpanContext(t *testing.T) {
// Test with context without span - this returns the default trace ID
ctx := t.Context()
traceID := getTraceID(ctx)
assert.Equal(t, "00000000000000000000000000000000", traceID)
}
func TestGetTraceID_WithValidSpan(t *testing.T) {
// Create a context with a valid span
ctx := t.Context()
span := trace.SpanFromContext(ctx)
// Test with valid span context
traceID := getTraceID(ctx)
assert.NotEmpty(t, traceID)
assert.Equal(t, span.SpanContext().TraceID().String(), traceID)
}
func TestGetTraceID_WithNilSpan(t *testing.T) {
// Create a custom context that will make trace.SpanFromContext return nil
// We'll use a context with a custom value that doesn't have a span
type customKey string
ctx := context.WithValue(t.Context(), customKey("custom-key"), "custom-value")
// Test with context that has nil span
traceID := getTraceID(ctx)
assert.Equal(t, "00000000000000000000000000000000", traceID)
}
func TestLogGRPCEntry(t *testing.T) {
mockLogger, _, ctrl := createMocks(t)
defer ctrl.Finish()
// Test logGRPCEntry function
log := &gRPCLog{
ID: "test-id",
StartTime: "2023-01-01T12:00:00Z",
ResponseTime: 100,
Method: "/test.Service/Method",
StatusCode: 0,
}
// Set up expectations
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
logGRPCEntry(mockLogger, log, "/test.Service/Method")
}
func TestLogGRPCEntry_WithDebugMethod(t *testing.T) {
mockLogger, _, ctrl := createMocks(t)
defer ctrl.Finish()
// Test logGRPCEntry function with debug method
log := &gRPCLog{
ID: "test-id",
StartTime: "2023-01-01T12:00:00Z",
ResponseTime: 100,
Method: debugMethod,
StatusCode: 0,
}
// Set up expectations
mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
logGRPCEntry(mockLogger, log, debugMethod)
}
func TestLogGRPCEntry_WithSendMethod(t *testing.T) {
mockLogger, _, ctrl := createMocks(t)
defer ctrl.Finish()
// Test logGRPCEntry function with Send method
log := &gRPCLog{
ID: "test-id",
StartTime: "2023-01-01T12:00:00Z",
ResponseTime: 100,
Method: "/test.Service/Send",
StatusCode: 0,
}
// Set up expectations
mockLogger.EXPECT().Debug(gomock.Any()).Times(1)
logGRPCEntry(mockLogger, log, "/test.Service/Send")
}
func TestLogGRPCEntry_WithNilLogger(_ *testing.T) {
// Test logGRPCEntry function with nil logger
log := &gRPCLog{
ID: "test-id",
StartTime: "2023-01-01T12:00:00Z",
ResponseTime: 100,
Method: "/test.Service/Method",
StatusCode: 0,
}
// This should not panic
logGRPCEntry(nil, log, "/test.Service/Method")
}
func TestRecordGRPCMetrics(t *testing.T) {
_, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
ctx := t.Context()
// Set up expectations
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
// Test recordGRPCMetrics function
recordGRPCMetrics(ctx, mockMetrics, "test_metric", 100*time.Millisecond, "/test.Service/Method", "")
}
func TestRecordGRPCMetrics_WithStreamType(t *testing.T) {
_, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
ctx := t.Context()
// Set up expectations
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
// Test recordGRPCMetrics function with stream type
recordGRPCMetrics(ctx, mockMetrics, "test_metric", 100*time.Millisecond, "/test.Service/Method", "SERVER_STREAM")
}
func TestIsServerError(t *testing.T) {
tests := []struct {
name string
err error
expected bool
}{
{"Internal is server error", status.Error(codes.Internal, "internal"), true},
{"Unknown is server error", status.Error(codes.Unknown, "unknown"), true},
{"Unavailable is server error", status.Error(codes.Unavailable, "unavailable"), true},
{"DataLoss is server error", status.Error(codes.DataLoss, "data loss"), true},
{"DeadlineExceeded is server error", status.Error(codes.DeadlineExceeded, "timeout"), true},
{"Aborted is server error", status.Error(codes.Aborted, "aborted"), true},
{"Unimplemented is server error", status.Error(codes.Unimplemented, "unimplemented"), true},
{"ResourceExhausted is not server error", status.Error(codes.ResourceExhausted, "rate limit"), false},
{"InvalidArgument is not server error", status.Error(codes.InvalidArgument, "bad arg"), false},
{"NotFound is not server error", status.Error(codes.NotFound, "not found"), false},
{"PermissionDenied is not server error", status.Error(codes.PermissionDenied, "denied"), false},
{"Unauthenticated is not server error", status.Error(codes.Unauthenticated, "unauth"), false},
{"AlreadyExists is not server error", status.Error(codes.AlreadyExists, "exists"), false},
{"FailedPrecondition is not server error", status.Error(codes.FailedPrecondition, "precondition"), false},
{"OutOfRange is not server error", status.Error(codes.OutOfRange, "range"), false},
{"Canceled is not server error", status.Error(codes.Canceled, "canceled"), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.expected, isServerError(tt.err))
})
}
}
func TestObservabilityInterceptor_WithClientError(t *testing.T) {
mockLogger, mockMetrics, ctrl := createMocks(t)
defer ctrl.Finish()
interceptor := ObservabilityInterceptor(mockLogger, mockMetrics)
ctx := t.Context()
req := "test request"
info := &grpc.UnaryServerInfo{
FullMethod: "/test.Service/Method",
}
handler := func(_ context.Context, _ any) (any, error) {
return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded")
}
// Errorf should NOT be called for client errors like ResourceExhausted
mockLogger.EXPECT().Info(gomock.Any()).Times(1)
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
resp, err := interceptor(ctx, req, info, handler)
require.Error(t, err)
assert.Nil(t, resp)
}
func TestRecordGRPCMetrics_WithNilMetrics(t *testing.T) {
ctx := t.Context()
// Test recordGRPCMetrics function with nil metrics
// This should not panic
recordGRPCMetrics(ctx, nil, "test_metric", 100*time.Millisecond, "/test.Service/Method", "")
}
================================================
FILE: pkg/gofr/grpc/middleware/apikey_auth.go
================================================
package middleware
import (
"context"
"crypto/subtle"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gofr.dev/pkg/gofr/container"
auth "gofr.dev/pkg/gofr/http/middleware"
)
// APIKeyAuthProvider holds the configuration for API key authentication.
type APIKeyAuthProvider struct {
APIKeys []string
ValidateFunc func(apiKey string) bool
ValidateFuncWithDatasources func(c *container.Container, apiKey string) bool
Container *container.Container
}
// APIKeyAuthUnaryInterceptor returns a gRPC unary server interceptor that validates the API key.
func APIKeyAuthUnaryInterceptor(provider APIKeyAuthProvider) grpc.UnaryServerInterceptor {
return NewAuthUnaryInterceptor(func(ctx context.Context) (any, error) {
return validateAPIKey(ctx, provider)
}, auth.APIKey)
}
// APIKeyAuthStreamInterceptor returns a gRPC stream server interceptor that validates the API key.
func APIKeyAuthStreamInterceptor(provider APIKeyAuthProvider) grpc.StreamServerInterceptor {
return NewAuthStreamInterceptor(func(ctx context.Context) (any, error) {
return validateAPIKey(ctx, provider)
}, auth.APIKey)
}
func validateAPIKey(ctx context.Context, provider APIKeyAuthProvider) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", status.Error(codes.Unauthenticated, "missing metadata")
}
// Check for x-api-key
values, ok := md["x-api-key"]
if !ok || len(values) == 0 {
return "", status.Error(codes.Unauthenticated, "missing x-api-key header")
}
apiKey := values[0]
if !provider.verifyAPIKey(apiKey) {
return "", status.Error(codes.Unauthenticated, "invalid api key")
}
return apiKey, nil
}
func (a APIKeyAuthProvider) verifyAPIKey(apiKey string) bool {
if a.ValidateFuncWithDatasources != nil {
return a.ValidateFuncWithDatasources(a.Container, apiKey)
}
if a.ValidateFunc != nil {
return a.ValidateFunc(apiKey)
}
for _, key := range a.APIKeys {
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(key)) == 1 {
return true
}
}
// Constant time compare with dummy key to mitigate timing attacks
subtle.ConstantTimeCompare([]byte(apiKey), []byte("dummy"))
return false
}
================================================
FILE: pkg/gofr/grpc/middleware/auth_test.go
================================================
package middleware
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gofr.dev/pkg/gofr/container"
auth "gofr.dev/pkg/gofr/http/middleware"
)
type mockKeyProvider struct {
key *rsa.PublicKey
}
func (m *mockKeyProvider) Get(kid string) *rsa.PublicKey {
if kid == "valid-kid" {
return m.key
}
return nil
}
func TestBasicAuthUnaryInterceptor(t *testing.T) {
users := map[string]string{"user": "pass"}
interceptor := BasicAuthUnaryInterceptor(BasicAuthProvider{Users: users})
t.Run("No Metadata", func(t *testing.T) {
ctx := context.Background()
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing metadata"), err)
})
t.Run("No Authorization Header", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing authorization header"), err)
})
t.Run("Invalid Format", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Bearer token"},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid authorization header format"), err)
})
t.Run("Invalid Base64", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic invalid-base64"},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid base64 credentials"), err)
})
t.Run("Invalid Credentials Format", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user"))},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid credentials format"), err)
})
t.Run("Wrong Password", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user:wrong"))},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid credentials"), err)
})
t.Run("Wrong User", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("wrong:pass"))},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid credentials"), err)
})
t.Run("Success", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("user:pass"))},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
username := ctx.Value(auth.Username)
assert.Equal(t, "user", username)
return nil, nil
})
assert.NoError(t, err)
})
}
func TestBasicAuthUnaryInterceptor_Validator(t *testing.T) {
t.Run("Custom Validation Function Success", func(t *testing.T) {
validateFunc := func(username, password string) bool {
return username == "custom" && password == "pass"
}
interceptor := BasicAuthUnaryInterceptor(BasicAuthProvider{ValidateFunc: validateFunc})
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("custom:pass"))},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
username := ctx.Value(auth.Username)
assert.Equal(t, "custom", username)
return nil, nil
})
assert.NoError(t, err)
})
t.Run("Validator with Datasources Success", func(t *testing.T) {
validateFunc := func(_ *container.Container, username, password string) bool {
return username == "validator" && password == "pass"
}
interceptor := BasicAuthUnaryInterceptor(BasicAuthProvider{ValidateFuncWithDatasources: validateFunc})
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Basic " + base64.StdEncoding.EncodeToString([]byte("validator:pass"))},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
username := ctx.Value(auth.Username)
assert.Equal(t, "validator", username)
return nil, nil
})
assert.NoError(t, err)
})
}
func TestAPIKeyAuthUnaryInterceptor(t *testing.T) {
keys := []string{"valid-key"}
interceptor := APIKeyAuthUnaryInterceptor(APIKeyAuthProvider{APIKeys: keys})
t.Run("No Metadata", func(t *testing.T) {
ctx := context.Background()
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing metadata"), err)
})
t.Run("No API Key Header", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing x-api-key header"), err)
})
t.Run("Invalid Key", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"x-api-key": []string{"invalid-key"},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid api key"), err)
})
t.Run("Success", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"x-api-key": []string{"valid-key"},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
apiKey := ctx.Value(auth.APIKey)
assert.Equal(t, "valid-key", apiKey)
return nil, nil
})
assert.NoError(t, err)
})
}
func TestAPIKeyAuthUnaryInterceptor_Validator(t *testing.T) {
t.Run("Custom Validation Function Success", func(t *testing.T) {
validateFunc := func(apiKey string) bool {
return apiKey == "custom-key"
}
interceptor := APIKeyAuthUnaryInterceptor(APIKeyAuthProvider{ValidateFunc: validateFunc})
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"x-api-key": []string{"custom-key"},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
apiKey := ctx.Value(auth.APIKey)
assert.Equal(t, "custom-key", apiKey)
return nil, nil
})
assert.NoError(t, err)
})
t.Run("Validator with Datasources Success", func(t *testing.T) {
validateFunc := func(_ *container.Container, apiKey string) bool {
return apiKey == "validator-key"
}
interceptor := APIKeyAuthUnaryInterceptor(APIKeyAuthProvider{ValidateFuncWithDatasources: validateFunc})
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"x-api-key": []string{"validator-key"},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
apiKey := ctx.Value(auth.APIKey)
assert.Equal(t, "validator-key", apiKey)
return nil, nil
})
assert.NoError(t, err)
})
}
func TestOAuthUnaryInterceptor(t *testing.T) {
// Generate RSA key
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
provider := &mockKeyProvider{key: publicKey}
interceptor := OAuthUnaryInterceptor(provider)
// Create valid token
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"sub": "user",
})
token.Header["kid"] = "valid-kid"
validToken, _ := token.SignedString(privateKey)
t.Run("No Metadata", func(t *testing.T) {
ctx := context.Background()
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing metadata"), err)
})
t.Run("No Authorization Header", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "missing authorization header"), err)
})
t.Run("Invalid Format", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Token " + validToken},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "invalid authorization header format"), err)
})
t.Run("Invalid Token", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Bearer invalid-token"},
})
_, err := interceptor(ctx, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Equal(t, status.Error(codes.Unauthenticated, "jwt expected"), err)
})
t.Run("Success", func(t *testing.T) {
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Bearer " + validToken},
})
_, err := interceptor(ctx, nil, nil, func(ctx context.Context, _ any) (any, error) {
claims := ctx.Value(auth.JWTClaim)
assert.NotNil(t, claims)
return nil, nil
})
assert.NoError(t, err)
})
}
type mockServerStream struct {
grpc.ServerStream
ctx context.Context
}
func (m *mockServerStream) Context() context.Context {
return m.ctx
}
func TestWrappedStream(t *testing.T) {
ctx := context.Background()
m := &mockServerStream{ctx: ctx}
newCtx := context.WithValue(ctx, auth.Username, "user")
w := &wrappedStream{ServerStream: m, ctx: newCtx}
assert.Equal(t, newCtx, w.Context())
}
================================================
FILE: pkg/gofr/grpc/middleware/basic_auth.go
================================================
package middleware
import (
"context"
"crypto/subtle"
"encoding/base64"
"strings"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gofr.dev/pkg/gofr/container"
auth "gofr.dev/pkg/gofr/http/middleware"
)
// BasicAuthProvider holds the configuration for basic authentication.
type BasicAuthProvider struct {
Users map[string]string
ValidateFunc func(username, password string) bool
ValidateFuncWithDatasources func(c *container.Container, username, password string) bool
Container *container.Container
}
// BasicAuthUnaryInterceptor returns a gRPC unary server interceptor that validates the Basic Auth credentials.
func BasicAuthUnaryInterceptor(provider BasicAuthProvider) grpc.UnaryServerInterceptor {
return NewAuthUnaryInterceptor(func(ctx context.Context) (any, error) {
return validateBasicAuth(ctx, provider)
}, auth.Username)
}
// BasicAuthStreamInterceptor returns a gRPC stream server interceptor that validates the Basic Auth credentials.
func BasicAuthStreamInterceptor(provider BasicAuthProvider) grpc.StreamServerInterceptor {
return NewAuthStreamInterceptor(func(ctx context.Context) (any, error) {
return validateBasicAuth(ctx, provider)
}, auth.Username)
}
func validateBasicAuth(ctx context.Context, provider BasicAuthProvider) (string, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", status.Error(codes.Unauthenticated, "missing metadata")
}
authHeader, ok := md["authorization"]
if !ok || len(authHeader) == 0 {
return "", status.Error(codes.Unauthenticated, "missing authorization header")
}
// Basic
parts := strings.SplitN(authHeader[0], " ", headerParts)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Basic") {
return "", status.Error(codes.Unauthenticated, "invalid authorization header format")
}
payload, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return "", status.Error(codes.Unauthenticated, "invalid base64 credentials")
}
username, password, found := strings.Cut(string(payload), ":")
if !found {
return "", status.Error(codes.Unauthenticated, "invalid credentials format")
}
if !provider.verifyCredentials(username, password) {
return "", status.Error(codes.Unauthenticated, "invalid credentials")
}
return username, nil
}
func (b BasicAuthProvider) verifyCredentials(username, password string) bool {
if b.ValidateFuncWithDatasources != nil {
return b.ValidateFuncWithDatasources(b.Container, username, password)
}
if b.ValidateFunc != nil {
return b.ValidateFunc(username, password)
}
expectedPass, ok := b.Users[username]
if !ok {
// Use dummy comparison to prevent timing attacks
subtle.ConstantTimeCompare([]byte(password), []byte("dummy"))
return false
}
return subtle.ConstantTimeCompare([]byte(password), []byte(expectedPass)) == 1
}
================================================
FILE: pkg/gofr/grpc/middleware/common.go
================================================
package middleware
import (
"context"
"google.golang.org/grpc"
)
// headerParts represents the expected number of parts in an authorization header (e.g., "Bearer ").
const headerParts = 2
// wrappedStream wraps a grpc.ServerStream to allow overriding the context.
// This is used to inject authentication information (like username or claims) into the stream's context.
type wrappedStream struct {
grpc.ServerStream
ctx context.Context
}
// Context returns the wrapped context containing authentication information.
func (w *wrappedStream) Context() context.Context {
return w.ctx
}
// AuthValidator defines a function that validates credentials from context and returns the identity and auth method.
type AuthValidator func(ctx context.Context) (any, error)
// NewAuthUnaryInterceptor creates a unary interceptor using the provided validator and auth method.
func NewAuthUnaryInterceptor(validator AuthValidator, method any) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
val, err := validator(ctx)
if err != nil {
return nil, err
}
return handler(context.WithValue(ctx, method, val), req)
}
}
// NewAuthStreamInterceptor creates a stream interceptor using the provided validator and auth method.
func NewAuthStreamInterceptor(validator AuthValidator, method any) grpc.StreamServerInterceptor {
return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
val, err := validator(ss.Context())
if err != nil {
return err
}
wrapped := &wrappedStream{ss, context.WithValue(ss.Context(), method, val)}
return handler(srv, wrapped)
}
}
================================================
FILE: pkg/gofr/grpc/middleware/oauth.go
================================================
package middleware
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
auth "gofr.dev/pkg/gofr/http/middleware"
)
const (
jwtRegexPattern = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"
)
var errKeyNotFound = errors.New("key not found")
// OAuthUnaryInterceptor returns a gRPC unary server interceptor that validates the OAuth token.
func OAuthUnaryInterceptor(key auth.PublicKeyProvider, options ...jwt.ParserOption) grpc.UnaryServerInterceptor {
regex := regexp.MustCompile(jwtRegexPattern)
options = append(options, jwt.WithIssuedAt())
return NewAuthUnaryInterceptor(func(ctx context.Context) (any, error) {
return validateOAuth(ctx, key, regex, options...)
}, auth.JWTClaim)
}
// OAuthStreamInterceptor returns a gRPC stream server interceptor that validates the OAuth token.
func OAuthStreamInterceptor(key auth.PublicKeyProvider, options ...jwt.ParserOption) grpc.StreamServerInterceptor {
regex := regexp.MustCompile(jwtRegexPattern)
options = append(options, jwt.WithIssuedAt())
return NewAuthStreamInterceptor(func(ctx context.Context) (any, error) {
return validateOAuth(ctx, key, regex, options...)
}, auth.JWTClaim)
}
func validateOAuth(ctx context.Context, key auth.PublicKeyProvider, regex *regexp.Regexp,
options ...jwt.ParserOption) (jwt.Claims, error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
authHeader, ok := md["authorization"]
if !ok || len(authHeader) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
}
// Bearer
parts := strings.SplitN(authHeader[0], " ", headerParts)
if len(parts) != headerParts || !strings.EqualFold(parts[0], "Bearer") {
return nil, status.Error(codes.Unauthenticated, "invalid authorization header format")
}
tokenString := parts[1]
if !regex.MatchString(tokenString) {
return nil, status.Error(codes.Unauthenticated, "jwt expected")
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
kid := token.Header["kid"]
jwks := key.Get(fmt.Sprint(kid))
if jwks == nil {
return nil, errKeyNotFound
}
return jwks, nil
}, options...)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
if !token.Valid {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
return token.Claims, nil
}
================================================
FILE: pkg/gofr/grpc/middleware/oauth_integration_test.go
================================================
package middleware
import (
"context"
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"math/big"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/metadata"
auth "gofr.dev/pkg/gofr/http/middleware"
)
func TestOAuthIntegration_MockJWKS(t *testing.T) {
// 1. Setup Mock JWKS Server
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := &privateKey.PublicKey
keyID := "valid-kid"
nBase64 := base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes())
eBase64 := base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes())
jwks := map[string]any{
"keys": []map[string]any{
{
"kty": "RSA",
"kid": keyID,
"n": nBase64,
"e": eBase64,
"use": "sig",
"alg": "RS256",
},
},
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(jwks)
}))
defer server.Close()
// 2. Setup Interceptor with mock PublicKeyProvider
provider := &mockKeyProvider{key: publicKey}
interceptor := OAuthUnaryInterceptor(provider)
// 3. Generate a valid token
claims := jwt.MapClaims{
"sub": "test-user",
"role": "admin",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
token.Header["kid"] = keyID
tokenString, _ := token.SignedString(privateKey)
// 4. Test Success Path
ctx := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Bearer " + tokenString},
})
_, err := interceptor(ctx, nil, nil, func(handlerCtx context.Context, _ any) (any, error) {
// Verify claims are injected
injectedClaims, ok := handlerCtx.Value(auth.JWTClaim).(jwt.MapClaims)
assert.True(t, ok)
assert.Equal(t, "test-user", injectedClaims["sub"])
assert.Equal(t, "admin", injectedClaims["role"])
return nil, nil
})
require.NoError(t, err)
// 5. Test Failure Path (Invalid Token)
ctxInvalid := metadata.NewIncomingContext(context.Background(), metadata.MD{
"authorization": []string{"Bearer invalid.token.here"},
})
_, err = interceptor(ctxInvalid, nil, nil, func(_ context.Context, _ any) (any, error) {
return nil, nil
})
assert.Error(t, err)
}
================================================
FILE: pkg/gofr/grpc/rate_limiter.go
================================================
// Package grpc provides gRPC-related additions within the GoFr framework.
//
// # Rate Limiting
//
// The rate limiter interceptors use a token bucket algorithm (via the shared
// RateLimiterStore) to control request throughput for both unary and streaming
// RPCs. Key implementation details:
//
// - IP extraction priority (when PerIP=true and TrustedProxies=true):
// X-Forwarded-For (first CSV entry) → X-Real-IP → gRPC peer address.
// - normalizeIP strips port/bracket notation and validates via net.ParseIP.
// - Fail-open: if the store returns an error, the request is allowed through
// to avoid self-inflicted denial of service.
// - Health check bypass: requests to /grpc.health.v1.Health/* are never
// rate-limited, preventing probe failures and cascading pod restarts.
// - retry-after: returned as both gRPC response metadata and an errdetails.RetryInfo
// proto in the status details. The unary path uses grpc.SendHeader; the stream
// path uses ss.SendHeader to ensure the header is actually delivered.
package grpc
import (
"context"
"fmt"
"math"
"net"
"strings"
"time"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/durationpb"
httpmw "gofr.dev/pkg/gofr/http/middleware"
)
const (
rateLimitKeyGlobal = "global"
rateLimitKeyUnknown = "unknown"
grpcHealthServicePrefix = "/grpc.health.v1.Health/"
)
// normalizeIP strips port and bracket notation, then validates via net.ParseIP.
// It returns the canonical string form or "" if the input is not a valid IP.
func normalizeIP(s string) string {
if s == "" {
return ""
}
// Fast-path: try SplitHostPort only when a colon is present.
// Pure IPv4 without port (e.g. "10.0.0.1") has no colon; skip the overhead.
if strings.ContainsRune(s, ':') {
if host, _, err := net.SplitHostPort(s); err == nil {
s = host
}
}
// Strip residual brackets from bare bracketed IPv6 like "[::1]".
if len(s) > 1 && s[0] == '[' {
s = s[1 : len(s)-1]
}
ip := net.ParseIP(s)
if ip == nil {
return ""
}
return ip.String()
}
// extractHeaderIP reads a single metadata header value from the incoming context
// and returns the normalized IP. For X-Forwarded-For it takes only the first
// comma-separated entry (the original client IP).
func extractHeaderIP(md metadata.MD, key string, firstCSV bool) string {
vals := md.Get(key)
if len(vals) == 0 {
return ""
}
v := vals[0]
if v == "" {
return ""
}
if firstCSV {
if i := strings.IndexByte(v, ','); i >= 0 {
v = v[:i]
}
}
return normalizeIP(strings.TrimSpace(v))
}
func getIP(ctx context.Context, trustProxy bool) string {
if trustProxy {
return getIPFromProxy(ctx)
}
return getIPFromPeer(ctx)
}
func getIPFromProxy(ctx context.Context) string {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return getIPFromPeer(ctx)
}
if ip := extractHeaderIP(md, "x-forwarded-for", true); ip != "" {
return ip
}
if ip := extractHeaderIP(md, "x-real-ip", false); ip != "" {
return ip
}
return getIPFromPeer(ctx)
}
func getIPFromPeer(ctx context.Context) string {
p, ok := peer.FromContext(ctx)
if !ok || p.Addr == nil {
return ""
}
return normalizeIP(p.Addr.String())
}
func retryAfterSeconds(durSeconds float64) string {
secs := math.Max(1, math.Ceil(durSeconds))
return fmt.Sprintf("%.0f", secs)
}
// buildRateLimitStatus constructs the gRPC status with RetryInfo details.
func buildRateLimitStatus(retryAfter time.Duration) error {
st := status.New(codes.ResourceExhausted, "rate limit exceeded")
retryDetail := &errdetails.RetryInfo{
RetryDelay: durationpb.New(retryAfter),
}
st, _ = st.WithDetails(retryDetail)
return st.Err()
}
// unaryRateLimitExhaustedError sends retry-after header via grpc.SendHeader (works for unary RPCs)
// and returns the ResourceExhausted status.
func unaryRateLimitExhaustedError(ctx context.Context, retryAfter time.Duration) error {
_ = grpc.SendHeader(ctx, metadata.Pairs("retry-after", retryAfterSeconds(retryAfter.Seconds())))
return buildRateLimitStatus(retryAfter)
}
// streamRateLimitExhaustedError sends retry-after header via ss.SendHeader (required for streams,
// since grpc.SendHeader is a no-op in the stream path) and returns the ResourceExhausted status.
func streamRateLimitExhaustedError(ss grpc.ServerStream, retryAfter time.Duration) error {
_ = ss.SendHeader(metadata.Pairs("retry-after", retryAfterSeconds(retryAfter.Seconds())))
return buildRateLimitStatus(retryAfter)
}
// newRateLimiterStore validates the config, initializes a default in-memory store
// if none is provided, and starts the background cleanup goroutine.
func newRateLimiterStore(ctx context.Context, cfg *httpmw.RateLimiterConfig) {
if err := cfg.Validate(); err != nil {
panic(fmt.Sprintf("invalid rate limiter config: %v", err))
}
if cfg.Store == nil {
cfg.Store = httpmw.NewMemoryRateLimiterStore(*cfg)
}
cfg.Store.StartCleanup(ctx)
}
// resolveRateLimitKey determines the rate limit bucket key based on config.
func resolveRateLimitKey(ctx context.Context, cfg httpmw.RateLimiterConfig) string {
if !cfg.PerIP {
return rateLimitKeyGlobal
}
key := getIP(ctx, cfg.TrustedProxies)
if key == "" {
return rateLimitKeyUnknown
}
return key
}
// recordRateLimitViolation logs and increments the counter metric for a rate limit violation.
func recordRateLimitViolation(ctx context.Context, l Logger, m Metrics, key, method, callType string) {
if l != nil {
l.Info(fmt.Sprintf("rate limit exceeded for key: %s, method: %s", key, method))
}
type counterMetrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
if cm, ok := m.(counterMetrics); ok {
cm.IncrementCounter(ctx, "app_grpc_rate_limit_exceeded_total",
"method", method,
"type", callType,
)
}
}
// UnaryRateLimitInterceptor returns a gRPC unary server interceptor that enforces
// rate limiting using the provided configuration. Pass app.Logger() and app.Metrics()
// for logging and Prometheus counter support.
func UnaryRateLimitInterceptor(
ctx context.Context, cfg httpmw.RateLimiterConfig, l Logger, m Metrics,
) grpc.UnaryServerInterceptor {
newRateLimiterStore(ctx, &cfg)
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if strings.HasPrefix(info.FullMethod, grpcHealthServicePrefix) {
return handler(ctx, req)
}
key := resolveRateLimitKey(ctx, cfg)
allowed, retryAfter, err := cfg.Store.Allow(ctx, key, cfg)
if err != nil {
return handler(ctx, req)
}
if !allowed {
recordRateLimitViolation(ctx, l, m, key, info.FullMethod, "unary")
return nil, unaryRateLimitExhaustedError(ctx, retryAfter)
}
return handler(ctx, req)
}
}
// StreamRateLimitInterceptor returns a gRPC stream server interceptor that enforces
// rate limiting on stream creation using the provided configuration. Pass app.Logger()
// and app.Metrics() for logging and Prometheus counter support.
func StreamRateLimitInterceptor(
ctx context.Context, cfg httpmw.RateLimiterConfig, l Logger, m Metrics,
) grpc.StreamServerInterceptor {
newRateLimiterStore(ctx, &cfg)
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if strings.HasPrefix(info.FullMethod, grpcHealthServicePrefix) {
return handler(srv, ss)
}
streamCtx := ss.Context()
key := resolveRateLimitKey(streamCtx, cfg)
allowed, retryAfter, err := cfg.Store.Allow(streamCtx, key, cfg)
if err != nil {
return handler(srv, ss)
}
if !allowed {
recordRateLimitViolation(streamCtx, l, m, key, info.FullMethod, "stream")
return streamRateLimitExhaustedError(ss, retryAfter)
}
return handler(srv, ss)
}
}
================================================
FILE: pkg/gofr/grpc/rate_limiter_test.go
================================================
package grpc
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/genproto/googleapis/rpc/errdetails"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
httpmw "gofr.dev/pkg/gofr/http/middleware"
)
var (
errStoreFailure = errors.New("store failure")
errRedisDown = errors.New("redis down")
)
// rateLimiterMockMetrics implements both Metrics (RecordHistogram) and the
// counterMetrics optional interface (IncrementCounter) used by the rate limiter.
type rateLimiterMockMetrics struct {
mu sync.Mutex
counters map[string]int
}
func newRateLimiterMockMetrics() *rateLimiterMockMetrics {
return &rateLimiterMockMetrics{
counters: make(map[string]int),
}
}
func (m *rateLimiterMockMetrics) IncrementCounter(_ context.Context, name string, _ ...string) {
m.mu.Lock()
defer m.mu.Unlock()
m.counters[name]++
}
func (*rateLimiterMockMetrics) RecordHistogram(context.Context, string, float64, ...string) {}
func (m *rateLimiterMockMetrics) GetCounter(name string) int {
m.mu.Lock()
defer m.mu.Unlock()
return m.counters[name]
}
type infoCapturingLogger struct {
mu sync.Mutex
logs []string
}
func (m *infoCapturingLogger) Info(args ...any) {
m.mu.Lock()
defer m.mu.Unlock()
for _, a := range args {
m.logs = append(m.logs, fmt.Sprintf("%v", a))
}
}
func (*infoCapturingLogger) Debug(_ ...any) {}
func (*infoCapturingLogger) Fatalf(_ string, _ ...any) {}
func (*infoCapturingLogger) Errorf(_ string, _ ...any) {}
func (m *infoCapturingLogger) logCount() int {
m.mu.Lock()
defer m.mu.Unlock()
return len(m.logs)
}
type rateLimitMockStream struct {
grpc.ServerStream
ctx context.Context
headerMD metadata.MD
}
func (s *rateLimitMockStream) Context() context.Context {
if s.ctx == nil {
return context.Background()
}
return s.ctx
}
func (s *rateLimitMockStream) SetHeader(md metadata.MD) error {
s.headerMD = md
return nil
}
func (s *rateLimitMockStream) SendHeader(md metadata.MD) error {
s.headerMD = md
return nil
}
func (*rateLimitMockStream) SendMsg(any) error { return nil }
func (*rateLimitMockStream) RecvMsg(any) error { return nil }
type fakeStore struct {
allowed bool
retryAfter time.Duration
err error
}
func (f *fakeStore) Allow(context.Context, string, httpmw.RateLimiterConfig) (bool, time.Duration, error) {
return f.allowed, f.retryAfter, f.err
}
func (*fakeStore) StartCleanup(context.Context) {}
func (*fakeStore) StopCleanup() {}
type fakeAddr string
func (a fakeAddr) Network() string {
_ = a
return "tcp"
}
func (a fakeAddr) String() string { return string(a) }
func Test_normalizeIP(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{name: "empty string", input: "", want: ""},
{name: "valid IPv4", input: "10.0.0.1", want: "10.0.0.1"},
{name: "IPv4 with port", input: "10.0.0.1:8080", want: "10.0.0.1"},
{name: "valid IPv6 loopback", input: "::1", want: "::1"},
{name: "bracketed IPv6", input: "[::1]", want: "::1"},
{name: "IPv6 with port", input: "[::1]:8080", want: "::1"},
{name: "invalid IP", input: "not-an-ip", want: ""},
{name: "full IPv6 address", input: "2001:db8::1", want: "2001:db8::1"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, normalizeIP(tc.input))
})
}
}
func Test_extractHeaderIP(t *testing.T) {
tests := []struct {
name string
md metadata.MD
key string
firstCSV bool
want string
}{
{name: "no header present", md: metadata.MD{}, key: "x-forwarded-for", firstCSV: true, want: ""},
{name: "empty value", md: metadata.Pairs("x-forwarded-for", ""), key: "x-forwarded-for", firstCSV: true, want: ""},
{name: "single valid IP", md: metadata.Pairs("x-forwarded-for", "10.0.0.1"), key: "x-forwarded-for", firstCSV: true, want: "10.0.0.1"},
{
name: "multiple IPs takes first", key: "x-forwarded-for", firstCSV: true, want: "10.0.0.1",
md: metadata.Pairs("x-forwarded-for", "10.0.0.1, 10.0.0.2, 10.0.0.3"),
},
{name: "invalid IP returns empty", md: metadata.Pairs("x-forwarded-for", "bad-ip"), key: "x-forwarded-for", firstCSV: true, want: ""},
{
name: "whitespace trimmed", key: "x-forwarded-for", firstCSV: true, want: "10.0.0.1",
md: metadata.Pairs("x-forwarded-for", " 10.0.0.1 "),
},
{name: "x-real-ip valid", md: metadata.Pairs("x-real-ip", "10.0.0.5"), key: "x-real-ip", firstCSV: false, want: "10.0.0.5"},
{name: "x-real-ip whitespace", md: metadata.Pairs("x-real-ip", " 10.0.0.5 "), key: "x-real-ip", firstCSV: false, want: "10.0.0.5"},
{name: "x-real-ip invalid", md: metadata.Pairs("x-real-ip", "bogus"), key: "x-real-ip", firstCSV: false, want: ""},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, extractHeaderIP(tc.md, tc.key, tc.firstCSV))
})
}
}
func Test_getIP_XForwardedFor(t *testing.T) {
md := metadata.Pairs("x-forwarded-for", "203.0.113.1, 198.51.100.1")
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = peer.NewContext(ctx, &peer.Peer{Addr: fakeAddr("192.168.1.1:12345")})
ip := getIP(ctx, true)
assert.Equal(t, "203.0.113.1", ip, "Should extract first IP from X-Forwarded-For when trusting proxies")
ip = getIP(ctx, false)
assert.Equal(t, "192.168.1.1", ip, "Should use peer addr when not trusting proxies")
}
func Test_getIP_XRealIP(t *testing.T) {
md := metadata.Pairs("x-real-ip", "203.0.113.5")
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = peer.NewContext(ctx, &peer.Peer{Addr: fakeAddr("192.168.1.1:12345")})
ip := getIP(ctx, true)
assert.Equal(t, "203.0.113.5", ip, "Should extract IP from X-Real-IP when trusting proxies")
ip = getIP(ctx, false)
assert.Equal(t, "192.168.1.1", ip, "Should use peer addr when not trusting proxies")
}
func Test_getIP_Priority(t *testing.T) {
md := metadata.Pairs("x-forwarded-for", "203.0.113.1", "x-real-ip", "203.0.113.2")
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = peer.NewContext(ctx, &peer.Peer{Addr: fakeAddr("192.168.1.1:12345")})
ip := getIP(ctx, true)
assert.Equal(t, "203.0.113.1", ip, "X-Forwarded-For should have highest priority")
}
func Test_getIP_PeerAddr(t *testing.T) {
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: fakeAddr("192.168.1.1:12345")})
ip := getIP(ctx, false)
assert.Equal(t, "192.168.1.1", ip, "Should extract IP from peer address")
}
func Test_getIP_PeerAddrWithoutPort(t *testing.T) {
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: fakeAddr("192.168.1.1")})
ip := getIP(ctx, false)
assert.Equal(t, "192.168.1.1", ip, "Should handle peer address without port")
}
func Test_getIP_NoPeer(t *testing.T) {
ip := getIP(context.Background(), false)
assert.Empty(t, ip, "Should return empty when no peer in context")
}
func Test_getIP_NilPeerAddr(t *testing.T) {
ctx := peer.NewContext(context.Background(), &peer.Peer{Addr: nil})
ip := getIP(ctx, false)
assert.Empty(t, ip, "Should return empty when peer address is nil")
}
func Test_retryAfterSeconds(t *testing.T) {
tests := []struct {
name string
dur float64
want string
}{
{name: "zero returns 1", dur: 0, want: "1"},
{name: "sub-second rounds up", dur: 0.3, want: "1"},
{name: "exactly 1", dur: 1.0, want: "1"},
{name: "fractional rounds up", dur: 2.1, want: "3"},
{name: "whole number", dur: 5.0, want: "5"},
{name: "negative returns 1", dur: -2, want: "1"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, retryAfterSeconds(tc.dur))
})
}
}
func TestUnaryRateLimitInterceptor_PanicsOnInvalidConfig(t *testing.T) {
tests := []struct {
name string
config httpmw.RateLimiterConfig
}{
{
name: "zero values",
config: httpmw.RateLimiterConfig{},
},
{
name: "negative RequestsPerSecond",
config: httpmw.RateLimiterConfig{RequestsPerSecond: -1, Burst: 5},
},
{
name: "zero Burst",
config: httpmw.RateLimiterConfig{RequestsPerSecond: 10, Burst: 0},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Panics(t, func() {
UnaryRateLimitInterceptor(context.Background(), tc.config, nil, nil)
})
})
}
}
func TestUnaryRateLimitInterceptor_DefaultStore(t *testing.T) {
cfg := httpmw.RateLimiterConfig{RequestsPerSecond: 10, Burst: 5}
interceptor := UnaryRateLimitInterceptor(context.Background(), cfg, nil, nil)
require.NotNil(t, interceptor)
}
func TestUnaryRateLimitInterceptor_GlobalLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: false,
}
logger := &infoCapturingLogger{}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, logger, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
ctx := grpc.NewContextWithServerTransportStream(context.Background(), nil)
for i := 0; i < 2; i++ {
resp, err := interceptor(ctx, "req", info, handler)
require.NoError(t, err, "Request %d should succeed", i+1)
assert.Equal(t, "ok", resp)
}
resp, err := interceptor(ctx, "req", info, handler)
assert.Nil(t, resp)
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.ResourceExhausted, st.Code())
assert.Equal(t, 1, metrics.GetCounter("app_grpc_rate_limit_exceeded_total"))
assert.Equal(t, 1, logger.logCount(), "Logger should have recorded one rate-limit violation")
}
func TestUnaryRateLimitInterceptor_PerIPLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: true,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
ctxForIP := func(ip string) context.Context {
md := metadata.Pairs("x-forwarded-for", ip)
ctx := metadata.NewIncomingContext(context.Background(), md)
return grpc.NewContextWithServerTransportStream(ctx, nil)
}
for i := 0; i < 2; i++ {
resp, err := interceptor(ctxForIP("10.0.0.1"), "req", info, handler)
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
_, err := interceptor(ctxForIP("10.0.0.1"), "req", info, handler)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
resp, err := interceptor(ctxForIP("10.0.0.2"), "req", info, handler)
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestUnaryRateLimitInterceptor_EmptyIPFallback(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: false,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
ctx := grpc.NewContextWithServerTransportStream(context.Background(), nil)
for i := 0; i < 2; i++ {
resp, err := interceptor(ctx, "req", info, handler)
require.NoError(t, err, "Request %d should succeed under 'unknown' key", i+1)
assert.Equal(t, "ok", resp)
}
_, err := interceptor(ctx, "req", info, handler)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
}
func TestUnaryRateLimitInterceptor_StoreError_FailsOpen(t *testing.T) {
store := &fakeStore{err: errStoreFailure}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), cfg, nil, nil)
resp, err := interceptor(context.Background(), "req",
&grpc.UnaryServerInfo{FullMethod: "/svc/Method"},
func(context.Context, any) (any, error) { return "ok", nil })
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestUnaryRateLimitInterceptor_DeniedNilMetrics(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 2 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), cfg, nil, nil)
ctx := grpc.NewContextWithServerTransportStream(context.Background(), nil)
resp, err := interceptor(ctx, "req",
&grpc.UnaryServerInfo{FullMethod: "/svc/Method"},
func(context.Context, any) (any, error) { return "no", nil })
assert.Nil(t, resp)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
}
func TestUnaryRateLimitInterceptor_TokenRefill(t *testing.T) {
if testing.Short() {
t.Skip("Skipping time-based test in short mode")
}
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 5,
Burst: 2,
PerIP: false,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
ctx := grpc.NewContextWithServerTransportStream(context.Background(), nil)
for i := 0; i < 2; i++ {
resp, err := interceptor(ctx, "req", info, handler)
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
_, err := interceptor(ctx, "req", info, handler)
require.Error(t, err)
time.Sleep(220 * time.Millisecond)
resp, err := interceptor(ctx, "req", info, handler)
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
func TestUnaryRateLimitInterceptor_ConcurrentRequests(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 10,
PerIP: true,
TrustedProxies: true,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
var (
wg sync.WaitGroup
mu sync.Mutex
successCount int
rateLimitedCount int
)
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
md := metadata.Pairs("x-forwarded-for", "192.168.1.1")
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = grpc.NewContextWithServerTransportStream(ctx, nil)
_, err := interceptor(ctx, "req", info, handler)
mu.Lock()
defer mu.Unlock()
if err == nil {
successCount++
} else {
rateLimitedCount++
}
}()
}
wg.Wait()
assert.GreaterOrEqual(t, successCount, 9, "Should allow approximately burst size requests")
assert.LessOrEqual(t, successCount, 11, "Should not allow significantly more than burst size")
assert.Positive(t, rateLimitedCount, "Should have some rate limited requests")
assert.Equal(t, 20, successCount+rateLimitedCount, "Total requests should be 20")
}
func TestUnaryRateLimitInterceptor_TrustedProxiesDisabled(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: false,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.UnaryServerInfo{FullMethod: "/svc/Method"}
handler := func(_ context.Context, _ any) (any, error) { return "ok", nil }
makeCtx := func(spoofedIP string) context.Context {
md := metadata.Pairs("x-forwarded-for", spoofedIP)
ctx := metadata.NewIncomingContext(context.Background(), md)
ctx = peer.NewContext(ctx, &peer.Peer{Addr: fakeAddr("127.0.0.1:12345")})
return grpc.NewContextWithServerTransportStream(ctx, nil)
}
for i := 0; i < 2; i++ {
resp, err := interceptor(makeCtx("203.0.113."+string(rune('1'+i))), "req", info, handler)
require.NoError(t, err)
assert.Equal(t, "ok", resp)
}
_, err := interceptor(makeCtx("203.0.113.99"), "req", info, handler)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
}
func TestUnaryRateLimitInterceptor_RetryAfterHeader(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 3 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), cfg, nil, nil)
ctx := grpc.NewContextWithServerTransportStream(context.Background(), nil)
_, err := interceptor(ctx, "req",
&grpc.UnaryServerInfo{FullMethod: "/svc/Method"},
func(context.Context, any) (any, error) { return "no", nil })
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.ResourceExhausted, st.Code())
// Assert RetryInfo is present in error details
details := st.Details()
require.Len(t, details, 1, "Error should contain exactly one detail")
retryInfo, ok := details[0].(*errdetails.RetryInfo)
require.True(t, ok, "Detail should be RetryInfo")
assert.Equal(t, 3*time.Second, retryInfo.GetRetryDelay().AsDuration(), "RetryDelay should match retryAfter")
}
func TestUnaryRateLimitInterceptor_SkipsHealthCheck(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 1 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := UnaryRateLimitInterceptor(context.Background(), cfg, nil, nil)
resp, err := interceptor(context.Background(), "req",
&grpc.UnaryServerInfo{FullMethod: "/grpc.health.v1.Health/Check"},
func(context.Context, any) (any, error) { return "healthy", nil })
require.NoError(t, err)
assert.Equal(t, "healthy", resp)
}
func TestStreamRateLimitInterceptor_PanicsOnInvalidConfig(t *testing.T) {
tests := []struct {
name string
config httpmw.RateLimiterConfig
}{
{
name: "zero values",
config: httpmw.RateLimiterConfig{},
},
{
name: "negative RequestsPerSecond",
config: httpmw.RateLimiterConfig{RequestsPerSecond: -1, Burst: 5},
},
{
name: "zero Burst",
config: httpmw.RateLimiterConfig{RequestsPerSecond: 10, Burst: 0},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Panics(t, func() {
StreamRateLimitInterceptor(context.Background(), tc.config, nil, nil)
})
})
}
}
func TestStreamRateLimitInterceptor_DefaultStore(t *testing.T) {
cfg := httpmw.RateLimiterConfig{RequestsPerSecond: 10, Burst: 5}
interceptor := StreamRateLimitInterceptor(context.Background(), cfg, nil, nil)
require.NotNil(t, interceptor)
}
func TestStreamRateLimitInterceptor_GlobalLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
logger := &infoCapturingLogger{}
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: false,
}
interceptor := StreamRateLimitInterceptor(context.Background(), config, logger, metrics)
info := &grpc.StreamServerInfo{FullMethod: "/svc/Stream"}
handler := func(any, grpc.ServerStream) error { return nil }
for i := 0; i < 2; i++ {
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.NoError(t, err, "Request %d should succeed", i+1)
}
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.ResourceExhausted, st.Code())
assert.Equal(t, 1, metrics.GetCounter("app_grpc_rate_limit_exceeded_total"))
assert.Equal(t, 1, logger.logCount(), "Logger should have recorded one rate-limit violation")
}
func TestStreamRateLimitInterceptor_PerIPLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: true,
}
interceptor := StreamRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.StreamServerInfo{FullMethod: "/svc/Stream"}
handler := func(any, grpc.ServerStream) error { return nil }
streamForIP := func(ip string) *rateLimitMockStream {
md := metadata.Pairs("x-forwarded-for", ip)
ctx := metadata.NewIncomingContext(context.Background(), md)
return &rateLimitMockStream{ctx: ctx}
}
for i := 0; i < 2; i++ {
err := interceptor(nil, streamForIP("10.0.0.1"), info, handler)
require.NoError(t, err)
}
err := interceptor(nil, streamForIP("10.0.0.1"), info, handler)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
err = interceptor(nil, streamForIP("10.0.0.2"), info, handler)
require.NoError(t, err)
}
func TestStreamRateLimitInterceptor_EmptyIPFallback(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: false,
}
interceptor := StreamRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.StreamServerInfo{FullMethod: "/svc/Stream"}
handler := func(any, grpc.ServerStream) error { return nil }
for i := 0; i < 2; i++ {
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.NoError(t, err, "Request %d should succeed under 'unknown' key", i+1)
}
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
}
func TestStreamRateLimitInterceptor_StoreError_FailsOpen(t *testing.T) {
store := &fakeStore{err: errRedisDown}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := StreamRateLimitInterceptor(context.Background(), cfg, nil, nil)
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, &grpc.StreamServerInfo{FullMethod: "/svc/Stream"},
func(any, grpc.ServerStream) error { return nil })
require.NoError(t, err)
}
func TestStreamRateLimitInterceptor_DeniedNilMetrics(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 1 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := StreamRateLimitInterceptor(context.Background(), cfg, nil, nil)
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, &grpc.StreamServerInfo{FullMethod: "/svc/Stream"},
func(any, grpc.ServerStream) error { return nil })
require.Error(t, err)
st, _ := status.FromError(err)
assert.Equal(t, codes.ResourceExhausted, st.Code())
}
func TestStreamRateLimitInterceptor_RetryAfterHeader(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 5 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
metrics := newRateLimiterMockMetrics()
interceptor := StreamRateLimitInterceptor(context.Background(), cfg, nil, metrics)
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, &grpc.StreamServerInfo{FullMethod: "/svc/Stream"},
func(any, grpc.ServerStream) error { return nil })
require.Error(t, err)
assert.Equal(t, 1, metrics.GetCounter("app_grpc_rate_limit_exceeded_total"))
// Assert RetryInfo is present in error details
st, ok := status.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.ResourceExhausted, st.Code())
details := st.Details()
require.Len(t, details, 1, "Error should contain exactly one detail")
retryInfo, ok := details[0].(*errdetails.RetryInfo)
require.True(t, ok, "Detail should be RetryInfo")
assert.Equal(t, 5*time.Second, retryInfo.GetRetryDelay().AsDuration(), "RetryDelay should match retryAfter")
// Assert retry-after metadata was sent via ss.SendHeader (not grpc.SendHeader)
require.NotNil(t, ss.headerMD, "Stream should have received retry-after header via SendHeader")
retryAfterVals := ss.headerMD.Get("retry-after")
require.Len(t, retryAfterVals, 1, "Should have exactly one retry-after value")
assert.Equal(t, "5", retryAfterVals[0], "retry-after should be 5 seconds")
}
func TestStreamRateLimitInterceptor_SkipsHealthCheck(t *testing.T) {
store := &fakeStore{allowed: false, retryAfter: 1 * time.Second}
cfg := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
Store: store,
}
interceptor := StreamRateLimitInterceptor(context.Background(), cfg, nil, nil)
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, &grpc.StreamServerInfo{FullMethod: "/grpc.health.v1.Health/Watch"},
func(any, grpc.ServerStream) error { return nil })
require.NoError(t, err, "Health check streams should bypass rate limiting")
}
func TestStreamRateLimitInterceptor_TokenRefill(t *testing.T) {
if testing.Short() {
t.Skip("Skipping time-based test in short mode")
}
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 5,
Burst: 2,
PerIP: false,
}
interceptor := StreamRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.StreamServerInfo{FullMethod: "/svc/Stream"}
handler := func(any, grpc.ServerStream) error { return nil }
// Exhaust burst
for i := 0; i < 2; i++ {
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.NoError(t, err)
}
ss := &rateLimitMockStream{ctx: context.Background()}
err := interceptor(nil, ss, info, handler)
require.Error(t, err)
time.Sleep(220 * time.Millisecond)
ss = &rateLimitMockStream{ctx: context.Background()}
err = interceptor(nil, ss, info, handler)
require.NoError(t, err)
}
func TestStreamRateLimitInterceptor_ConcurrentRequests(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := httpmw.RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 10,
PerIP: true,
TrustedProxies: true,
}
interceptor := StreamRateLimitInterceptor(context.Background(), config, nil, metrics)
info := &grpc.StreamServerInfo{FullMethod: "/svc/Stream"}
var (
wg sync.WaitGroup
successCount int64
rateLimitedCount int64
)
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
md := metadata.Pairs("x-forwarded-for", "192.168.1.1")
ctx := metadata.NewIncomingContext(context.Background(), md)
ss := &rateLimitMockStream{ctx: ctx}
err := interceptor(nil, ss, info, func(any, grpc.ServerStream) error {
return nil
})
if err == nil {
atomic.AddInt64(&successCount, 1)
} else {
atomic.AddInt64(&rateLimitedCount, 1)
}
}()
}
wg.Wait()
assert.GreaterOrEqual(t, successCount, int64(9), "Should allow approximately burst size requests")
assert.LessOrEqual(t, successCount, int64(11), "Should not allow significantly more than burst size")
assert.Positive(t, rateLimitedCount, "Should have some rate limited requests")
assert.Equal(t, int64(20), successCount+rateLimitedCount, "Total requests should be 20")
}
================================================
FILE: pkg/gofr/grpc.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"net"
"reflect"
"strconv"
"strings"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofr_grpc "gofr.dev/pkg/gofr/grpc"
)
type grpcServer struct {
server *grpc.Server
interceptors []grpc.UnaryServerInterceptor
streamInterceptors []grpc.StreamServerInterceptor
options []grpc.ServerOption
port int
config config.Config
}
var (
errNonAddressable = errors.New("cannot inject container as it is not addressable or is nil")
errInvalidPort = errors.New("invalid port number")
errFailedCreateServer = errors.New("failed to create gRPC server")
)
// AddGRPCServerOptions allows users to add custom gRPC server options such as TLS configuration,
// timeouts, interceptors, and other server-specific settings in a single call.
//
// Example:
//
// // Add TLS credentials and connection timeout in one call
// creds, _ := credentials.NewServerTLSFromFile("server-cert.pem", "server-key.pem")
// app.AddGRPCServerOptions(
// grpc.Creds(creds),
// grpc.ConnectionTimeout(10 * time.Second),
// )
//
// This function accepts a variadic list of gRPC server options (grpc.ServerOption) and appends them
// to the server's configuration. It allows fine-tuning of the gRPC server's behavior during its initialization.
func (a *App) AddGRPCServerOptions(grpcOpts ...grpc.ServerOption) {
if len(grpcOpts) == 0 {
a.container.Logger.Debug("no gRPC server options provided")
return
}
a.container.Logger.Debugf("adding %d gRPC server options", len(grpcOpts))
a.grpcServer.options = append(a.grpcServer.options, grpcOpts...)
}
// AddGRPCUnaryInterceptors allows users to add custom gRPC interceptors.
// Example:
//
// func loggingInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
// handler grpc.UnaryHandler) (interface{}, error) {
// log.Printf("Received gRPC request: %s", info.FullMethod)
// return handler(ctx, req)
// }
// app.AddGRPCUnaryInterceptors(loggingInterceptor)
func (a *App) AddGRPCUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) {
if len(interceptors) == 0 {
a.container.Logger.Debug("no unary interceptors provided")
return
}
a.container.Logger.Debugf("adding %d valid unary interceptors", len(interceptors))
a.grpcServer.interceptors = append(a.grpcServer.interceptors, interceptors...)
}
func (a *App) AddGRPCServerStreamInterceptors(interceptors ...grpc.StreamServerInterceptor) {
if len(interceptors) == 0 {
a.container.Logger.Debug("no stream interceptors provided")
return
}
a.container.Logger.Debugf("adding %d stream interceptors", len(interceptors))
a.grpcServer.streamInterceptors = append(a.grpcServer.streamInterceptors, interceptors...)
}
func newGRPCServer(c *container.Container, port int, cfg config.Config) (*grpcServer, error) {
if port <= 0 || port > 65535 {
return nil, fmt.Errorf("%w: %d", errInvalidPort, port)
}
registerGRPCMetrics(c)
middleware := make([]grpc.UnaryServerInterceptor, 0)
middleware = append(middleware,
grpc_recovery.UnaryServerInterceptor(),
gofr_grpc.ObservabilityInterceptor(c.Logger, c.Metrics()))
streamMiddleware := make([]grpc.StreamServerInterceptor, 0)
streamMiddleware = append(streamMiddleware,
grpc_recovery.StreamServerInterceptor(),
gofr_grpc.StreamObservabilityInterceptor(c.Logger, c.Metrics()))
return &grpcServer{
port: port,
interceptors: middleware,
streamInterceptors: streamMiddleware,
config: cfg,
}, nil
}
// registerGRPCMetrics registers essential gRPC metrics.
func registerGRPCMetrics(c *container.Container) {
c.Metrics().NewGauge("grpc_server_status", "gRPC server status (1=running, 0=stopped)")
c.Metrics().NewCounter("grpc_server_errors_total", "Total gRPC server errors")
c.Metrics().NewCounter("grpc_services_registered_total", "Total gRPC services registered")
c.Metrics().NewCounter("app_grpc_rate_limit_exceeded_total", "Total gRPC requests rejected by rate limiter")
}
func (g *grpcServer) createServer() error {
interceptorOption := grpc.ChainUnaryInterceptor(g.interceptors...)
streamOpt := grpc.ChainStreamInterceptor(g.streamInterceptors...)
g.options = append(g.options, interceptorOption, streamOpt)
g.server = grpc.NewServer(g.options...)
if g.server == nil {
return errFailedCreateServer
}
enabled := strings.ToLower(g.config.GetOrDefault("GRPC_ENABLE_REFLECTION", "false"))
if enabled == "true" { //nolint:goconst // standard boolean string
reflection.Register(g.server)
}
return nil
}
func (g *grpcServer) Run(c *container.Container) {
if g.server == nil {
if err := g.createServer(); err != nil {
c.Logger.Fatalf("failed to create gRPC server: %v", err)
c.Metrics().IncrementCounter(context.Background(), "grpc_server_errors_total")
return
}
}
if !isPortAvailable(g.port) {
c.Logger.Fatalf("gRPC port %d is blocked or unreachable", g.port)
c.Metrics().IncrementCounter(context.Background(), "grpc_server_errors_total")
c.Metrics().SetGauge("grpc_server_status", 0)
return
}
addr := ":" + strconv.Itoa(g.port)
c.Logger.Infof("starting gRPC server at %s", addr)
listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", addr)
if err != nil {
c.Logger.Errorf("error in starting gRPC server at %s: %s", addr, err)
c.Metrics().IncrementCounter(context.Background(), "grpc_server_errors_total")
c.Metrics().SetGauge("grpc_server_status", 0)
return
}
c.Metrics().SetGauge("grpc_server_status", 1)
c.Logger.Infof("gRPC server started successfully on %s", addr)
if err := g.server.Serve(listener); err != nil {
c.Logger.Errorf("error in starting gRPC server at %s: %s", addr, err)
c.Metrics().IncrementCounter(context.Background(), "grpc_server_errors_total")
c.Metrics().SetGauge("grpc_server_status", 0)
return
}
c.Logger.Infof("gRPC server stopped on %s", addr)
c.Metrics().SetGauge("grpc_server_status", 0)
}
func (g *grpcServer) Shutdown(ctx context.Context) error {
return ShutdownWithContext(ctx, func(_ context.Context) error {
if g.server != nil {
g.server.GracefulStop()
}
return nil
}, func() error {
g.server.Stop()
return nil
})
}
// RegisterService adds a gRPC service to the GoFr application.
func (a *App) RegisterService(desc *grpc.ServiceDesc, impl any) {
if !a.grpcRegistered {
if err := a.grpcServer.createServer(); err != nil {
a.container.Logger.Errorf("failed to create gRPC server for service %s: %v", desc.ServiceName, err)
return
}
}
a.container.Logger.Infof("registering gRPC Service: %s", desc.ServiceName)
a.grpcServer.server.RegisterService(desc, impl)
a.container.Metrics().IncrementCounter(context.Background(), "grpc_services_registered_total")
err := injectContainer(impl, a.container)
if err != nil {
a.container.Logger.Fatalf("failed to inject container into gRPC service %s: %v", desc.ServiceName, err)
}
a.grpcRegistered = true
a.container.Logger.Infof("successfully registered gRPC service: %s", desc.ServiceName)
}
func injectContainer(impl any, c *container.Container) error {
val := reflect.ValueOf(impl)
// Note: returning nil for the cases where user does not want to inject the container altogether and
// not to break any existing implementation for the users that are using gRPC server. If users are
// expecting the container to be injected and are passing non-addressable server struct, we have the
// DEBUG log for the same.
if val.Kind() != reflect.Pointer {
c.Logger.Debugf("cannot inject container into non-addressable implementation of `%s`, consider using pointer",
val.Type().Name())
return nil
}
val = val.Elem()
tVal := val.Type()
for i := 0; i < val.NumField(); i++ {
f := tVal.Field(i)
v := val.Field(i)
if f.Type == reflect.TypeOf(c) {
if !v.CanSet() {
c.Logger.Error(errNonAddressable)
return errNonAddressable
}
v.Set(reflect.ValueOf(c))
// early return expecting only one container field necessary for one gRPC implementation
return nil
}
if f.Type == reflect.TypeOf(*c) {
if !v.CanSet() {
c.Logger.Error(errNonAddressable)
return errNonAddressable
}
v.Set(reflect.ValueOf(*c))
// early return expecting only one container field necessary for one gRPC implementation
return nil
}
}
return nil
}
func (g *grpcServer) addServerOptions(opts ...grpc.ServerOption) {
g.options = append(g.options, opts...)
}
func (g *grpcServer) addUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) {
g.interceptors = append(g.interceptors, interceptors...)
}
func (g *grpcServer) addStreamInterceptors(interceptors ...grpc.StreamServerInterceptor) {
g.streamInterceptors = append(g.streamInterceptors, interceptors...)
}
================================================
FILE: pkg/gofr/grpc_test.go
================================================
package gofr
import (
"context"
"fmt"
"net"
"strconv"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc"
"google.golang.org/grpc/health"
"google.golang.org/grpc/health/grpc_health_v1"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
// nonExitingMockLogger embeds MockLogger but overrides Fatal methods to not exit.
type nonExitingMockLogger struct {
*logging.MockLogger
}
func (n *nonExitingMockLogger) Fatal(args ...any) {
// Just log as error instead of exiting
n.MockLogger.Error(args...)
}
func (n *nonExitingMockLogger) Fatalf(format string, args ...any) {
// Just log as error instead of exiting
n.MockLogger.Errorf(format, args...)
}
// setupGRPCMetricExpectations sets up mock expectations for gRPC metrics.
func setupGRPCMetricExpectations(mockMetrics *container.MockMetrics) {
mockMetrics.EXPECT().NewGauge("grpc_server_status", "gRPC server status (1=running, 0=stopped)").AnyTimes()
mockMetrics.EXPECT().NewCounter("grpc_server_errors_total", "Total gRPC server errors").AnyTimes()
mockMetrics.EXPECT().NewCounter("grpc_services_registered_total", "Total gRPC services registered").AnyTimes()
mockMetrics.EXPECT().NewCounter("app_grpc_rate_limit_exceeded_total", "Total gRPC requests rejected by rate limiter").AnyTimes()
mockMetrics.EXPECT().SetGauge("grpc_server_status", gomock.Any()).AnyTimes()
mockMetrics.EXPECT().IncrementCounter(gomock.Any(), "grpc_server_errors_total").AnyTimes()
mockMetrics.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
}
// setupTestGRPCServer creates a mock container and gRPC server for testing.
func setupTestGRPCServer(t *testing.T, port int, enableReflection bool) (*container.Container, *container.Mocks, *grpcServer) {
t.Helper()
c, mocks := container.NewMockContainer(t)
setupGRPCMetricExpectations(mocks.Metrics)
cfg := createMockGRPCConfig(t, port, enableReflection)
g, err := newGRPCServer(c, port, cfg)
require.NoError(t, err)
return c, mocks, g
}
// createTestInterceptors creates sample interceptors for testing.
func createTestInterceptors() []grpc.UnaryServerInterceptor {
return []grpc.UnaryServerInterceptor{
func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
return handler(ctx, req)
},
func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
return handler(ctx, req)
},
}
}
//nolint:thelper // linter is suppressed to avoid SA5011 (possible nil pointer dereference) warnings from staticcheck
func createMockGRPCConfig(t *testing.T, port int, enableReflection bool) config.Config {
// Use a default metrics port if t is nil (for cases where we don't need a real free port)
metricsPort := 8080
if t != nil {
metricsPort = testutil.GetFreePort(t)
}
configMap := map[string]string{
"GRPC_PORT": strconv.Itoa(port),
"METRICS_PORT": strconv.Itoa(metricsPort),
}
if enableReflection {
configMap["GRPC_ENABLE_REFLECTION"] = "true"
} else {
configMap["GRPC_ENABLE_REFLECTION"] = "false"
}
return config.NewMockConfig(configMap)
}
func TestNewGRPCServer(t *testing.T) {
c, mocks, g := setupTestGRPCServer(t, 9999, false)
assert.NotNil(t, g, "TEST Failed.\n")
assert.NotNil(t, c, "Container should not be nil")
assert.NotNil(t, mocks, "Mocks should not be nil")
}
func TestGRPCServer_AddServerOptions(t *testing.T) {
_, _, g := setupTestGRPCServer(t, 9999, false)
option1 := grpc.ConnectionTimeout(5 * time.Second)
option2 := grpc.MaxRecvMsgSize(1024 * 1024)
g.addServerOptions(option1, option2)
assert.Len(t, g.options, 2)
}
func TestGRPCServer_AddUnaryInterceptors(t *testing.T) {
_, _, g := setupTestGRPCServer(t, 9999, false)
interceptors := createTestInterceptors()
g.addUnaryInterceptors(interceptors...)
assert.Len(t, g.interceptors, 4) // 2 default + 2 test interceptors
}
func TestGRPCServer_CreateServer(t *testing.T) {
_, _, g := setupTestGRPCServer(t, 9999, false)
err := g.createServer()
require.NoError(t, err)
assert.NotNil(t, g.server)
}
func TestGRPCServer_RegisterService(t *testing.T) {
_, _, g := setupTestGRPCServer(t, 9999, false)
err := g.createServer()
require.NoError(t, err)
healthServer := health.NewServer()
desc := &grpc_health_v1.Health_ServiceDesc
g.server.RegisterService(desc, healthServer)
services := g.server.GetServiceInfo()
_, ok := services["grpc.health.v1.Health"]
assert.True(t, ok, "health service should be registered")
}
func TestGRPC_ServerRun(t *testing.T) {
// Test invalid port case
t.Run("net.Listen() error", func(t *testing.T) {
out := testutil.StderrOutputForFunc(func() {
c, mocks := container.NewMockContainer(t)
setupGRPCMetricExpectations(mocks.Metrics)
// Add expectations for error scenarios
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "grpc_server_errors_total").AnyTimes()
mocks.Metrics.EXPECT().SetGauge("grpc_server_status", gomock.Any()).AnyTimes()
cfg := createMockGRPCConfig(t, 99999, false) // Invalid port
g := &grpcServer{
port: 99999, // Invalid port
config: cfg,
}
// Create the server first
err := g.createServer()
if err != nil {
t.Fatalf("Failed to create server: %v", err)
}
// Run the server in a goroutine
done := make(chan bool)
serverRoutine := func() {
defer func() {
if r := recover(); r != nil {
t.Logf("Server panicked: %v", r)
}
done <- true
}()
g.Run(c)
}
go serverRoutine()
// Give some time for the server to attempt startup
time.Sleep(500 * time.Millisecond)
// Shutdown the server
_ = g.Shutdown(t.Context())
// Wait for the goroutine to finish
<-done
})
// Assert that the expected log message was captured
assert.Contains(t, out, "error in starting gRPC server", "Expected log message not found for invalid port test")
})
// Test port occupied case
t.Run("server.Serve() error", func(t *testing.T) {
// First, occupy a port
occupiedPort := testutil.GetFreePort(t)
listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", fmt.Sprintf(":%d", occupiedPort))
require.NoError(t, err)
defer listener.Close()
out := testutil.StderrOutputForFunc(func() {
c, mocks := container.NewMockContainer(t)
setupGRPCMetricExpectations(mocks.Metrics)
// Add expectations for error scenarios
mocks.Metrics.EXPECT().IncrementCounter(gomock.Any(), "grpc_server_errors_total").AnyTimes()
mocks.Metrics.EXPECT().SetGauge("grpc_server_status", gomock.Any()).AnyTimes()
// Replace the logger with our custom logger that doesn't exit on Fatal
mockLogger := &nonExitingMockLogger{MockLogger: logging.NewMockLogger(logging.DEBUG).(*logging.MockLogger)}
c.Logger = mockLogger
cfg := createMockGRPCConfig(t, occupiedPort, false) // Use the occupied port
g := &grpcServer{
port: occupiedPort, // Use the occupied port
config: cfg,
}
// Create the server first
err := g.createServer()
if err != nil {
t.Fatalf("Failed to create server: %v", err)
}
// Run the server - this should call Fatalf but not exit
g.Run(c)
})
// Assert that the expected log message was captured
assert.Contains(t, out, "gRPC port", "Expected log message not found for occupied port test")
})
}
func TestGRPC_ServerShutdown(t *testing.T) {
c, _, g := setupTestGRPCServer(t, 9999, false)
go g.Run(c)
// Wait for the server to start
time.Sleep(10 * time.Millisecond)
// Create a context with a timeout to test the shutdown
ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
defer cancel()
err := g.Shutdown(ctx)
require.NoError(t, err, "TestGRPC_ServerShutdown Failed.\n")
}
func TestGRPC_ServerShutdown_ContextCanceled(t *testing.T) {
c, _, g := setupTestGRPCServer(t, 9999, false)
go g.Run(c)
// Wait for the server to start
time.Sleep(10 * time.Millisecond)
// Create a context that can be canceled
ctx, cancel := context.WithCancel(t.Context())
errChan := make(chan error, 1)
go func() {
errChan <- g.Shutdown(ctx)
}()
// Cancel the context immediately
cancel()
err := <-errChan
require.ErrorContains(t, err, "context canceled", "Expected error due to context cancellation")
}
func Test_injectContainer_Fails(t *testing.T) {
// Case: container is an unaddressable or unexported field
type fail struct {
c1 *container.Container
}
c, _ := container.NewMockContainer(t)
srv1 := &fail{}
err := injectContainer(srv1, c)
require.ErrorIs(t, err, errNonAddressable)
require.Nil(t, srv1.c1)
// Case: server is passed as unadressable(non-pointer)
srv3 := fail{}
out := testutil.StdoutOutputForFunc(func() {
cont, _ := container.NewMockContainer(t)
err = injectContainer(srv3, cont)
assert.NoError(t, err)
})
assert.Contains(t, out, "cannot inject container into non-addressable implementation of `fail`, consider using pointer")
}
func Test_injectContainer(t *testing.T) {
c, _ := container.NewMockContainer(t)
// embedded container
type success1 struct {
*container.Container
}
srv1 := &success1{}
err := injectContainer(srv1, c)
require.NoError(t, err)
require.NotNil(t, srv1.Container)
// pointer type container
type success2 struct {
C *container.Container
}
srv2 := &success2{}
err = injectContainer(srv2, c)
require.NoError(t, err)
require.NotNil(t, srv2.C)
// non pointer type container
type success3 struct {
C container.Container
}
srv3 := &success3{}
err = injectContainer(srv3, c)
require.NoError(t, err)
require.NotNil(t, srv3.C)
}
func TestGRPC_Shutdown_BeforeStart(t *testing.T) {
_, _, g := setupTestGRPCServer(t, 9999, false)
ctx, cancel := context.WithTimeout(t.Context(), 500*time.Millisecond)
defer cancel()
err := g.Shutdown(ctx)
assert.NoError(t, err, "Expected shutdown to succeed even if server was not started")
}
func TestGRPC_ServerRun_WithInterceptorAndOptions(t *testing.T) {
freePort := testutil.GetFreePort(t)
c, _, g := setupTestGRPCServer(t, freePort, false)
var interceptorExecutions []string
// Define interceptors
interceptor1 := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
interceptorExecutions = append(interceptorExecutions, "interceptor1")
return handler(ctx, req)
}
interceptor2 := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
interceptorExecutions = append(interceptorExecutions, "interceptor2")
return handler(ctx, req)
}
app := New()
app.container = c
app.grpcServer = g
// Add the server options and interceptors to the app
app.AddGRPCServerOptions(
grpc.ConnectionTimeout(5*time.Second),
grpc.MaxRecvMsgSize(1024*1024))
// Set interceptors
app.AddGRPCUnaryInterceptors(interceptor1, interceptor2)
// Create the server first
err := app.grpcServer.createServer()
require.NoError(t, err)
// Start the server in a goroutine
go app.grpcServer.Run(c)
// Wait for the server to start
time.Sleep(100 * time.Millisecond)
// Shutdown the server immediately to avoid timeout
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err = app.grpcServer.Shutdown(ctx)
require.NoError(t, err)
// Verify that the server was created with the interceptors and options
assert.NotNil(t, app.grpcServer.server)
assert.Len(t, app.grpcServer.interceptors, 4) // 2 default + 2 test interceptors
assert.Len(t, app.grpcServer.options, 4) // 2 test options + 2 default (interceptor) options
}
func TestApp_WithReflection(t *testing.T) {
c, _, g := setupTestGRPCServer(t, 9999, true) // Enable reflection
app := New()
app.container = c
app.grpcServer = g
err := app.grpcServer.createServer()
require.NoError(t, err)
services := app.grpcServer.server.GetServiceInfo()
_, ok := services["grpc.reflection.v1alpha.ServerReflection"]
assert.True(t, ok, "reflection service should be registered")
}
================================================
FILE: pkg/gofr/handler.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"runtime/debug"
"time"
"github.com/gorilla/websocket"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/http/response"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/static"
)
const colorCodeError = 202 // 202 is red color code
type Handler func(c *Context) (any, error)
/*
Developer Note: There is an implementation where we do not need this internal handler struct
and directly use Handler. However, in that case the container dependency is not injected and
has to be created inside ServeHTTP method, which will result in multiple unnecessary calls.
This is what we implemented first.
There is another possibility where we write our own Router implementation and let httpServer
use that router which will return a Handler and httpServer will then create the context with
injecting container and call that Handler with the new context. A similar implementation is
done in CMD. Since this will require us to write our own router - we are not taking that path
for now. In the future, this can be considered as well if we are writing our own HTTP router.
*/
type handler struct {
function Handler
container *container.Container
requestTimeout time.Duration
}
type ErrorLogEntry struct {
TraceID string `json:"trace_id,omitempty"`
Error string `json:"error,omitempty"`
}
func (el *ErrorLogEntry) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%s \n", el.TraceID, colorCodeError, el.Error)
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := newContext(gofrHTTP.NewResponder(w, r.Method), gofrHTTP.NewRequest(r), h.container)
traceID := trace.SpanFromContext(r.Context()).SpanContext().TraceID().String()
if websocket.IsWebSocketUpgrade(r) {
// If the request is a WebSocket upgrade, do not apply the timeout
c.Context = r.Context()
} else if h.requestTimeout != 0 {
ctx, cancel := context.WithTimeout(r.Context(), h.requestTimeout)
defer cancel()
c.Context = ctx
}
done := make(chan struct{})
panicked := make(chan struct{})
var (
result any
err error
)
go func() {
defer func() {
panicRecoveryHandler(recover(), h.container.Logger, panicked)
}()
// Execute the handler function
result, err = h.function(c)
h.logError(traceID, err)
close(done)
}()
select {
case <-c.Context.Done():
// Handle different context cancellation scenarios
ctxErr := c.Context.Err()
// Server-side timeout occurred && fallback for other context errors
err = gofrHTTP.ErrorRequestTimeout{}
if errors.Is(ctxErr, context.Canceled) {
// Client canceled the request (e.g., closed browser tab)
err = gofrHTTP.ErrorClientClosedRequest{}
}
case <-done:
handleWebSocketUpgrade(r)
case <-panicked:
err = gofrHTTP.ErrorPanicRecovery{}
}
// Handle custom headers if 'result' is a 'Response'.
if resp, ok := result.(response.Response); ok {
resp.SetCustomHeaders(w)
}
// Handler function completed
c.responder.Respond(result, err)
}
func healthHandler(c *Context) (any, error) {
return c.Health(c), nil
}
func liveHandler(*Context) (any, error) {
return struct {
Status string `json:"status"`
}{Status: "UP"}, nil
}
func faviconHandler(*Context) (any, error) {
data, err := os.ReadFile("./static/favicon.ico")
if err != nil {
data, err = static.Files.ReadFile("favicon.ico")
}
return response.File{
Content: data,
ContentType: "image/x-icon",
}, err
}
func catchAllHandler(*Context) (any, error) {
return nil, gofrHTTP.ErrorInvalidRoute{}
}
func panicRecoveryHandler(re any, log logging.Logger, panicked chan struct{}) {
if re == nil {
return
}
close(panicked)
log.Error(panicLog{
Error: fmt.Sprint(re),
StackTrace: string(debug.Stack()),
})
}
// Log the error(if any) with traceID and errorMessage.
func (h handler) logError(traceID string, err error) {
if err != nil {
errorLog := &ErrorLogEntry{TraceID: traceID, Error: err.Error()}
// define the default log level for error
loggerHelper := h.container.Logger.Error
switch logging.GetLogLevelForError(err) {
case logging.ERROR:
// we use the default log level for error
case logging.INFO:
loggerHelper = h.container.Logger.Info
case logging.NOTICE:
loggerHelper = h.container.Logger.Notice
case logging.DEBUG:
loggerHelper = h.container.Logger.Debug
case logging.WARN:
loggerHelper = h.container.Logger.Warn
case logging.FATAL:
loggerHelper = h.container.Logger.Fatal
}
loggerHelper(errorLog)
}
}
func handleWebSocketUpgrade(r *http.Request) {
if websocket.IsWebSocketUpgrade(r) {
// Do not respond with HTTP headers since this is a WebSocket request
return
}
}
================================================
FILE: pkg/gofr/handler_test.go
================================================
package gofr
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/http/response"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
var (
errTest = errors.New("some error")
)
func TestHandler_ServeHTTP(t *testing.T) {
testCases := []struct {
desc string
method string
data any
err error
statusCode int
body string
}{
{"method is get, data is nil and error is nil", http.MethodGet, nil, nil, http.StatusOK,
`{}`},
{"method is get, data is mil, error is not nil", http.MethodGet, nil, errTest, http.StatusInternalServerError,
`{"error":{"message":"some error"}}`},
{"method is get, data is mil, error is http error", http.MethodGet, nil, gofrHTTP.ErrorEntityNotFound{}, http.StatusNotFound,
`{"error":{"message":"No entity found with : "}}`},
{"method is post, data is nil and error is nil", http.MethodPost, "Created", nil, http.StatusCreated,
`{"data":"Created"}`},
{"method is delete, data is nil and error is nil", http.MethodDelete, nil, nil, http.StatusNoContent,
`{}`},
}
for i, tc := range testCases {
w := httptest.NewRecorder()
r := httptest.NewRequest(tc.method, "/", http.NoBody)
c := &container.Container{
Logger: logging.NewLogger(logging.FATAL),
}
handler{
function: func(*Context) (any, error) {
return tc.data, tc.err
},
container: c,
}.ServeHTTP(w, r)
assert.Containsf(t, w.Body.String(), tc.body, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, w.Code, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestHandler_ServeHTTP_Timeout(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
h := handler{requestTimeout: 100 * time.Millisecond}
h.container = &container.Container{Logger: logging.NewLogger(logging.FATAL)}
h.function = func(*Context) (any, error) {
time.Sleep(200 * time.Millisecond)
return "hey", nil
}
h.ServeHTTP(w, r)
assert.Equal(t, http.StatusRequestTimeout, w.Code, "TestHandler_ServeHTTP_Timeout Failed")
assert.Contains(t, w.Body.String(), "request timed out", "TestHandler_ServeHTTP_Timeout Failed")
}
func TestHandler_ServeHTTP_Panic(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
h := handler{}
h.container = &container.Container{Logger: logging.NewLogger(logging.FATAL)}
h.function = func(*Context) (any, error) {
panic("runtime panic")
}
h.ServeHTTP(w, r)
assert.Equal(t, http.StatusInternalServerError, w.Code, "TestHandler_ServeHTTP_Panic Failed")
assert.Contains(t, w.Body.String(), http.StatusText(http.StatusInternalServerError), "TestHandler_ServeHTTP_Panic Failed")
}
func TestHandler_ServeHTTP_WithHeaders(t *testing.T) {
testCases := []struct {
desc string
method string
data any
headers map[string]string
err error
statusCode int
body string
}{
{
desc: "Response with headers, method is GET, no error",
method: http.MethodGet,
data: response.Response{
Headers: map[string]string{
"X-Custom-Header": "custom-value",
"Content-Type": "application/json",
},
Data: map[string]string{
"message": "Hello, World!",
},
},
headers: map[string]string{
"X-Custom-Header": "custom-value",
"Content-Type": "application/json",
},
statusCode: http.StatusOK,
body: `{"message":"Hello, World!"}`,
},
{
desc: "No headers, method is GET, data is simple string, no error",
method: http.MethodGet,
data: "simple string",
statusCode: http.StatusOK,
body: `"simple string"`,
},
}
for i, tc := range testCases {
w := httptest.NewRecorder()
r := httptest.NewRequest(tc.method, "/", http.NoBody)
c := &container.Container{
Logger: logging.NewLogger(logging.FATAL),
}
handler{
function: func(*Context) (any, error) {
return tc.data, tc.err
},
container: c,
}.ServeHTTP(w, r)
assert.Containsf(t, w.Body.String(), tc.body, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.statusCode, w.Code, "TEST[%d], Failed.\n%s", i, tc.desc)
for key, expectedValue := range tc.headers {
assert.Equal(t, expectedValue, w.Header().Get(key), "TEST[%d], Failed. Header mismatch: %s", i, key)
}
}
}
func TestHandler_faviconHandlerError(t *testing.T) {
c := Context{
Context: t.Context(),
}
d, _ := os.ReadFile("static/favicon.ico")
// renaming the file to produce the error case and rename it back to original after completion of test.
_, err := os.Stat("static/favicon.ico")
if err != nil {
t.Errorf("favicon.ico file not found in static directory")
return
}
err = os.Rename("static/favicon.ico", "static/newFavicon.ico")
if err != nil {
t.Errorf("error in renaming favicon.ico!")
}
defer func() {
err = os.Rename("static/newFavicon.ico", "static/favicon.ico")
if err != nil {
t.Errorf("error in renaming file back to favicon.ico")
}
}()
data, err := faviconHandler(&c)
require.NoError(t, err, "TEST Failed.\n")
assert.Equal(t, response.File{
Content: d,
ContentType: "image/x-icon",
}, data, "TEST Failed.\n")
}
func TestHandler_faviconHandler(t *testing.T) {
c := Context{
Context: t.Context(),
}
d, _ := os.ReadFile("static/favicon.ico")
data, err := faviconHandler(&c)
require.NoError(t, err, "TEST Failed.\n")
assert.Equal(t, response.File{
Content: d,
ContentType: "image/x-icon",
}, data, "TEST Failed.\n")
}
func TestHandler_catchAllHandler(t *testing.T) {
c := Context{
Context: t.Context(),
}
data, err := catchAllHandler(&c)
assert.Nil(t, data, "TEST Failed.\n")
assert.Equal(t, gofrHTTP.ErrorInvalidRoute{}, err, "TEST Failed.\n")
}
func TestHandler_livelinessHandler(t *testing.T) {
resp, err := liveHandler(&Context{})
require.NoError(t, err)
assert.Contains(t, fmt.Sprint(resp), "UP")
}
func TestHandler_healthHandler(t *testing.T) {
testutil.NewServerConfigs(t)
a := New()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/.well-known/alive", r.URL.Path)
w.WriteHeader(http.StatusOK)
}))
a.AddHTTPService("test-service", server.URL)
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "", http.NoBody)
r := gofrHTTP.NewRequest(req)
ctx := newContext(nil, r, a.container)
h, err := healthHandler(ctx)
require.NoError(t, err)
assert.NotNil(t, h)
}
func TestHandler_ServeHTTP_ContextCanceled(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
// Create a context that's already canceled
ctx, cancel := context.WithCancel(r.Context())
cancel() // Cancel immediately
r = r.WithContext(ctx)
h := handler{
container: &container.Container{Logger: logging.NewLogger(logging.FATAL)},
}
h.function = func(*Context) (any, error) {
time.Sleep(100 * time.Millisecond)
return "should not reach", nil
}
h.ServeHTTP(w, r)
assert.Equal(t, 499, w.Code, "Should return HTTP 499 for client closed request")
assert.Contains(t, w.Body.String(), "client closed request", "Should contain error message")
}
func TestHandler_ServeHTTP_ContextTimeout(t *testing.T) {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
// Create context with 50ms timeout
ctx, cancel := context.WithTimeout(r.Context(), 1*time.Millisecond)
defer cancel()
r = r.WithContext(ctx)
h := handler{
container: &container.Container{Logger: logging.NewLogger(logging.FATAL)},
}
h.function = func(*Context) (any, error) {
// Sleep longer than timeout to trigger deadline exceeded
time.Sleep(10 * time.Millisecond)
return "should timeout", nil
}
h.ServeHTTP(w, r)
assert.Equal(t, http.StatusRequestTimeout, w.Code, "Should return HTTP 408 for context timeout")
assert.Contains(t, w.Body.String(), "request timed out")
}
func TestIntegration_ConcurrentClientCancellations(t *testing.T) {
ports := testutil.NewServerConfigs(t)
t.Setenv("METRICS_PORT", strconv.Itoa(ports.MetricsPort))
t.Setenv("HTTP_PORT", strconv.Itoa(ports.HTTPPort))
var requestCount atomic.Int64
var completedCount atomic.Int64
app := New()
app.GET("/concurrent", func(_ *Context) (any, error) {
requestCount.Add(1)
// Simulate work
time.Sleep(10 * time.Millisecond)
completedCount.Add(1)
return map[string]string{"status": "completed"}, nil
})
go func() {
app.Run()
}()
time.Sleep(5 * time.Millisecond)
// Launch multiple concurrent requests with early cancellation
const numRequests = 10
var wg sync.WaitGroup
var canceledCount atomic.Int64
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func() {
defer wg.Done()
ctx, cancel := context.WithCancel(t.Context())
// Cancel after short delay
go func() {
time.Sleep(5 * time.Millisecond)
cancel()
}()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprint("http://localhost:", ports.HTTPPort, "/concurrent"), http.NoBody)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
if strings.Contains(err.Error(), "canceled") {
canceledCount.Add(1)
}
return
}
if resp != nil {
resp.Body.Close()
}
}()
}
wg.Wait()
time.Sleep(50 * time.Millisecond) // Let remaining requests complete
// Verify some requests were canceled
canceled := canceledCount.Load()
started := requestCount.Load()
completed := completedCount.Load()
t.Logf("Started: %d, Completed: %d, Canceled: %d", started, completed, canceled)
assert.Positive(t, canceled, "Some requests should have been canceled")
assert.LessOrEqual(t, completed, started, "Completed should not exceed started")
}
func TestIntegration_ServerTimeout(t *testing.T) {
ports := testutil.NewServerConfigs(t)
t.Setenv("METRICS_PORT", strconv.Itoa(ports.MetricsPort))
t.Setenv("HTTP_PORT", strconv.Itoa(ports.HTTPPort))
// Set GoFr's built-in request timeout to 1 second
t.Setenv("REQUEST_TIMEOUT", "1")
app := New()
// Handler that takes longer than server timeout
app.GET("/timeout-test", func(*Context) (any, error) {
// Sleep longer than REQUEST_TIMEOUT (1 second)
time.Sleep(2 * time.Second)
return map[string]string{"message": "should timeout"}, nil
})
go func() {
app.Run()
}()
// Wait for server to be ready
testURL := fmt.Sprintf("http://localhost:%d/timeout-test", ports.HTTPPort)
client := &http.Client{Timeout: 10 * time.Second} // Client timeout longer than server
ready := false
for i := 0; i < 50; i++ {
time.Sleep(10 * time.Millisecond)
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, testURL, http.NoBody)
require.NoError(t, err)
resp, err := client.Do(req)
if err == nil {
ready = true
resp.Body.Close()
break
}
}
require.True(t, ready, "Server should be ready")
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, testURL, http.NoBody)
require.NoError(t, err)
resp, err := client.Do(req)
require.NoError(t, err, "HTTP request should complete")
defer resp.Body.Close()
// GoFr should return 408 Request Timeout
assert.Equal(t, http.StatusRequestTimeout, resp.StatusCode,
"Server should return 408 for request timeout")
body, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var errorResponse map[string]any
err = json.Unmarshal(body, &errorResponse)
require.NoError(t, err)
errorObj := errorResponse["error"].(map[string]any)
assert.Equal(t, "request timed out", errorObj["message"])
}
================================================
FILE: pkg/gofr/http/errors.go
================================================
// Package http provides a set of utilities for handling HTTP requests and responses within the GoFr framework.
package http
import (
"fmt"
"net/http"
"strings"
"gofr.dev/pkg/gofr/logging"
)
const (
alreadyExistsMessage = "entity already exists"
StatusClientClosedRequest = 499
)
// ErrorEntityNotFound represents an error for when an entity is not found in the system.
type ErrorEntityNotFound struct {
Name string
Value string
}
func (e ErrorEntityNotFound) Error() string {
// For ex: "No entity found with id: 2"
return fmt.Sprintf("No entity found with %s: %s", e.Name, e.Value)
}
func (ErrorEntityNotFound) StatusCode() int {
return http.StatusNotFound
}
func (ErrorEntityNotFound) LogLevel() logging.Level {
return logging.INFO
}
// ErrorEntityAlreadyExist represents an error for when entity is already present in the storage and we are trying to make duplicate entry.
type ErrorEntityAlreadyExist struct{}
func (ErrorEntityAlreadyExist) Error() string {
return alreadyExistsMessage
}
func (ErrorEntityAlreadyExist) StatusCode() int {
return http.StatusConflict
}
func (ErrorEntityAlreadyExist) LogLevel() logging.Level {
return logging.WARN
}
// ErrorInvalidParam represents an error for invalid parameter values.
type ErrorInvalidParam struct {
Params []string `json:"param,omitempty"` // Params contains the list of invalid parameter names.
}
func (e ErrorInvalidParam) Error() string {
return fmt.Sprintf("'%d' invalid parameter(s): %s", len(e.Params), strings.Join(e.Params, ", "))
}
func (ErrorInvalidParam) StatusCode() int {
return http.StatusBadRequest
}
func (ErrorInvalidParam) LogLevel() logging.Level {
return logging.INFO
}
// ErrorMissingParam represents an error for missing parameters in a request.
type ErrorMissingParam struct {
Params []string `json:"param,omitempty"`
}
func (e ErrorMissingParam) Error() string {
return fmt.Sprintf("'%d' missing parameter(s): %s", len(e.Params), strings.Join(e.Params, ", "))
}
func (ErrorMissingParam) LogLevel() logging.Level {
return logging.INFO
}
func (ErrorMissingParam) StatusCode() int {
return http.StatusBadRequest
}
// ErrorInvalidRoute represents an error for invalid route in a request.
type ErrorInvalidRoute struct{}
func (ErrorInvalidRoute) Error() string {
return "route not registered"
}
func (ErrorInvalidRoute) LogLevel() logging.Level {
return logging.INFO
}
func (ErrorInvalidRoute) StatusCode() int {
return http.StatusNotFound
}
// ErrorRequestTimeout represents an error for request which timed out.
type ErrorRequestTimeout struct{}
func (ErrorRequestTimeout) Error() string {
return "request timed out"
}
func (ErrorRequestTimeout) StatusCode() int {
return http.StatusRequestTimeout // 408 is correct for request timeouts
}
func (ErrorRequestTimeout) LogLevel() logging.Level {
return logging.INFO // Server timeouts are informational
}
// ErrorClientClosedRequest represents when client cancels the request.
type ErrorClientClosedRequest struct{}
func (ErrorClientClosedRequest) Error() string {
return "client closed request"
}
func (ErrorClientClosedRequest) StatusCode() int {
return StatusClientClosedRequest // Non-standard but widely used by Nginx
}
func (ErrorClientClosedRequest) LogLevel() logging.Level {
return logging.DEBUG // Client cancellations aren't server errors
}
type ErrorServiceUnavailable struct {
Dependency string
ErrorMessage string
}
func (e ErrorServiceUnavailable) Error() string {
if e.ErrorMessage != "" && e.Dependency != "" {
return fmt.Sprintf("Service unavailable due to error: %v from dependency %v", e.ErrorMessage, e.Dependency)
}
return http.StatusText(http.StatusServiceUnavailable)
}
func (ErrorServiceUnavailable) StatusCode() int {
return http.StatusServiceUnavailable
}
func (ErrorServiceUnavailable) LogLevel() logging.Level {
return logging.ERROR
}
// ErrorPanicRecovery represents an error for request which panicked.
type ErrorPanicRecovery struct{}
func (ErrorPanicRecovery) Error() string {
return http.StatusText(http.StatusInternalServerError)
}
func (ErrorPanicRecovery) StatusCode() int {
return http.StatusInternalServerError
}
func (ErrorPanicRecovery) LogLevel() logging.Level {
return logging.ERROR
}
// ErrorTooManyRequests represents an error when rate limit is exceeded.
type ErrorTooManyRequests struct{}
func (ErrorTooManyRequests) Error() string {
return "rate limit exceeded"
}
func (ErrorTooManyRequests) StatusCode() int {
return http.StatusTooManyRequests
}
func (ErrorTooManyRequests) LogLevel() logging.Level {
return logging.WARN
}
// validate the errors satisfy the underlying interfaces they depend on.
var (
_ StatusCodeResponder = ErrorEntityNotFound{}
_ StatusCodeResponder = ErrorEntityAlreadyExist{}
_ StatusCodeResponder = ErrorInvalidParam{}
_ StatusCodeResponder = ErrorMissingParam{}
_ StatusCodeResponder = ErrorInvalidRoute{}
_ StatusCodeResponder = ErrorRequestTimeout{}
_ StatusCodeResponder = ErrorPanicRecovery{}
_ StatusCodeResponder = ErrorServiceUnavailable{}
_ StatusCodeResponder = ErrorClientClosedRequest{}
_ StatusCodeResponder = ErrorTooManyRequests{}
_ logging.LogLevelResponder = ErrorClientClosedRequest{}
_ logging.LogLevelResponder = ErrorEntityNotFound{}
_ logging.LogLevelResponder = ErrorEntityAlreadyExist{}
_ logging.LogLevelResponder = ErrorInvalidParam{}
_ logging.LogLevelResponder = ErrorMissingParam{}
_ logging.LogLevelResponder = ErrorInvalidRoute{}
_ logging.LogLevelResponder = ErrorRequestTimeout{}
_ logging.LogLevelResponder = ErrorPanicRecovery{}
_ logging.LogLevelResponder = ErrorServiceUnavailable{}
_ logging.LogLevelResponder = ErrorTooManyRequests{}
)
================================================
FILE: pkg/gofr/http/errors_test.go
================================================
package http
import (
"fmt"
"net/http"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/logging"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestErrorEntityNotFound(t *testing.T) {
fieldName := "id"
fieldValue := "2"
err := ErrorEntityNotFound{Name: fieldName, Value: fieldValue}
expectedMsg := fmt.Sprintf("No entity found with %s: %s", fieldName, fieldValue)
require.ErrorContainsf(t, err, expectedMsg, "TEST Failed.\n")
}
func TestErrorEntityNotFound_StatusCode(t *testing.T) {
err := ErrorEntityNotFound{}
expectedCode := http.StatusNotFound
assert.Equal(t, expectedCode, err.StatusCode(), "TEST Failed.\n")
}
func TestErrorEntityAlreadyExist(t *testing.T) {
err := ErrorEntityAlreadyExist{}
require.ErrorContains(t, err, alreadyExistsMessage, "TEST Failed.\n")
}
func TestErrorEntityAlreadyExist_StatusCode(t *testing.T) {
err := ErrorEntityAlreadyExist{}
expectedCode := http.StatusConflict
assert.Equal(t, expectedCode, err.StatusCode(), "TEST Failed.\n")
}
func TestErrorInvalidParam(t *testing.T) {
tests := []struct {
desc string
params []string
expectedMsg string
}{
{"no parameter", make([]string, 0), "'0' invalid parameter(s): "},
{"single parameter", []string{"uuid"}, "'1' invalid parameter(s): uuid"},
{"list of params", []string{"id", "name", "age"}, "'3' invalid parameter(s): id, name, age"},
}
for i, tc := range tests {
err := ErrorInvalidParam{Params: tc.params}
require.ErrorContainsf(t, err, tc.expectedMsg, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestInvalidParameter_StatusCode(t *testing.T) {
err := ErrorInvalidParam{}
expectedCode := http.StatusBadRequest
assert.Equal(t, expectedCode, err.StatusCode(), "TestErrorInvalidParam_StatusCode Failed!")
}
func TestErrorMissingParam(t *testing.T) {
tests := []struct {
desc string
params []string
expectedMsg string
}{
{"no parameter", make([]string, 0), "'0' missing parameter(s): "},
{"single parameter", []string{"uuid"}, "'1' missing parameter(s): uuid"},
{"list of params", []string{"id", "name", "age"}, "'3' missing parameter(s): id, name, age"},
}
for i, tc := range tests {
err := ErrorMissingParam{Params: tc.params}
require.ErrorContainsf(t, err, tc.expectedMsg, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestMissingParameter_StatusCode(t *testing.T) {
err := ErrorMissingParam{}
expectedCode := http.StatusBadRequest
assert.Equal(t, expectedCode, err.StatusCode(), "TEST Failed.\n")
}
func TestErrorInvalidRoute(t *testing.T) {
err := ErrorInvalidRoute{}
require.ErrorContainsf(t, err, "route not registered", "TEST Failed.\n")
assert.Equal(t, http.StatusNotFound, err.StatusCode(), "TEST Failed.\n")
}
func Test_ErrorRequestTimeout(t *testing.T) {
err := ErrorRequestTimeout{}
require.ErrorContainsf(t, err, "request timed out", "TEST Failed.\n")
assert.Equal(t, http.StatusRequestTimeout, err.StatusCode(), "TEST Failed.\n")
}
func Test_ErrorErrorPanicRecovery(t *testing.T) {
err := ErrorPanicRecovery{}
require.ErrorContainsf(t, err, http.StatusText(http.StatusInternalServerError), "TEST Failed.\n")
assert.Equal(t, http.StatusInternalServerError, err.StatusCode(), "TEST Failed.\n")
}
func Test_ServiceUnavailable(t *testing.T) {
code503 := http.StatusServiceUnavailable
testCases := []struct {
message string
dependency string
errorMessage string
statusCode int
logLevel logging.Level
}{
{"", "", http.StatusText(code503), code503, logging.ERROR},
{"Connection Error", "", http.StatusText(code503), code503, logging.ERROR},
{"", "DB", http.StatusText(code503), code503, logging.ERROR},
{"Connection Error", "DB", "Service unavailable due to error: Connection Error from dependency DB", code503, logging.ERROR},
}
for _, tc := range testCases {
err := ErrorServiceUnavailable{
Dependency: tc.dependency,
ErrorMessage: tc.message,
}
assert.Equal(t, tc.statusCode, err.StatusCode())
assert.Equal(t, tc.errorMessage, err.Error())
assert.Equal(t, tc.logLevel, err.LogLevel())
}
}
func TestErrorClientClosedRequest(t *testing.T) {
err := ErrorClientClosedRequest{}
assert.Equal(t, "client closed request", err.Error())
assert.Equal(t, StatusClientClosedRequest, err.StatusCode())
assert.Equal(t, logging.DEBUG, err.LogLevel())
}
================================================
FILE: pkg/gofr/http/form_data_binder.go
================================================
package http
import (
"encoding/json"
"fmt"
"reflect"
"strings"
)
func (*formData) setInterfaceValue(value reflect.Value, data any) (bool, error) {
if !value.CanSet() {
return false, fmt.Errorf("%w: %s", errUnsupportedInterfaceType, value.Kind())
}
value.Set(reflect.ValueOf(data))
return true, nil
}
func (uf *formData) setSliceOrArrayValue(value reflect.Value, data string) (bool, error) {
if value.Kind() != reflect.Slice && value.Kind() != reflect.Array {
return false, fmt.Errorf("%w: %s", errUnsupportedKind, value.Kind())
}
elemType := value.Type().Elem()
elements := strings.Split(data, ",")
// Create a new slice/array with appropriate length and capacity
var newSlice reflect.Value
if value.Kind() == reflect.Slice {
newSlice = reflect.MakeSlice(value.Type(), len(elements), len(elements))
} else if len(elements) > value.Len() {
return false, errDataLengthExceeded
} else {
newSlice = reflect.New(value.Type()).Elem()
}
// Create a reusable element value to avoid unnecessary allocations
elemValue := reflect.New(elemType).Elem()
// Set the elements of the slice/array
for i, strVal := range elements {
// Update the reusable element value
if _, err := uf.setFieldValue(elemValue, strVal); err != nil {
return false, fmt.Errorf("%w %d: %w", errSettingValueFailure, i, err)
}
newSlice.Index(i).Set(elemValue)
}
value.Set(newSlice)
return true, nil
}
func (*formData) setStructValue(value reflect.Value, data string) (bool, error) {
if value.Kind() != reflect.Struct {
return false, errNotAStruct
}
dataMap, err := parseStringToMap(data)
if err != nil {
return false, err
}
if len(dataMap) == 0 {
return false, errFieldsNotSet
}
numFieldsSet := 0
var multiErr error
// Create a map for case-insensitive lookups
caseInsensitiveMap := make(map[string]any)
for key, val := range dataMap {
caseInsensitiveMap[strings.ToLower(key)] = val
}
for i := 0; i < value.NumField(); i++ {
fieldType := value.Type().Field(i)
fieldValue := value.Field(i)
fieldName := fieldType.Name
// Perform case-insensitive lookup for the key in dataMap
val, exists := caseInsensitiveMap[strings.ToLower(fieldName)]
if !exists {
continue
}
if !fieldValue.CanSet() {
multiErr = fmt.Errorf("%w: %s", errUnexportedField, fieldName)
continue
}
if err := setFieldValueFromData(fieldValue, val); err != nil {
multiErr = fmt.Errorf("%w; %w", multiErr, err)
continue
}
numFieldsSet++
}
if numFieldsSet == 0 {
return false, errFieldsNotSet
}
return true, multiErr
}
// setFieldValueFromData sets the field's value based on the provided data.
func setFieldValueFromData(field reflect.Value, data any) error {
switch field.Kind() {
case reflect.String:
return setStringField(field, data)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return setIntField(field, data)
case reflect.Float32, reflect.Float64:
return setFloatField(field, data)
case reflect.Bool:
return setBoolField(field, data)
case reflect.Invalid, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Complex64, reflect.Complex128, reflect.Array, reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
reflect.Pointer, reflect.Slice, reflect.Struct, reflect.UnsafePointer:
return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data)
default:
return fmt.Errorf("%w: %s, %T", errUnsupportedFieldType, field.Type().Name(), data)
}
}
type customUnmarshaller struct {
dataMap map[string]any
}
// UnmarshalJSON is a custom unmarshaller because json package in Go unmarshal numbers to float64 by default.
func (c *customUnmarshaller) UnmarshalJSON(data []byte) error {
var rawData map[string]any
err := json.Unmarshal(data, &rawData)
if err != nil {
return err
}
dataMap := make(map[string]any, len(rawData))
for key, val := range rawData {
if valFloat, ok := val.(float64); ok {
valInt := int(valFloat)
if valFloat == float64(valInt) {
val = valInt
}
}
dataMap[key] = val
}
*c = customUnmarshaller{dataMap}
return nil
}
func parseStringToMap(data string) (map[string]any, error) {
var c customUnmarshaller
err := json.Unmarshal([]byte(data), &c)
return c.dataMap, err
}
func setStringField(field reflect.Value, data any) error {
if val, ok := data.(string); ok {
field.SetString(val)
return nil
}
return fmt.Errorf("%w: expected string but got %T", errUnsupportedFieldType, data)
}
func setIntField(field reflect.Value, data any) error {
if val, ok := data.(int); ok {
field.SetInt(int64(val))
return nil
}
return fmt.Errorf("%w: expected int but got %T", errUnsupportedFieldType, data)
}
func setFloatField(field reflect.Value, data any) error {
if val, ok := data.(float64); ok {
field.SetFloat(val)
return nil
}
return fmt.Errorf("%w: expected float64 but got %T", errUnsupportedFieldType, data)
}
func setBoolField(field reflect.Value, data any) error {
if val, ok := data.(bool); ok {
field.SetBool(val)
return nil
}
return fmt.Errorf("%w: expected bool but got %T", errUnsupportedFieldType, data)
}
================================================
FILE: pkg/gofr/http/form_data_binder_test.go
================================================
package http
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_setFieldValueFromData(t *testing.T) {
t.Run("String Field", func(t *testing.T) {
var str string
field := reflect.ValueOf(&str).Elem()
err := setFieldValueFromData(field, "hello")
require.NoError(t, err)
assert.Equal(t, "hello", str)
})
t.Run("Int Field", func(t *testing.T) {
var num int
field := reflect.ValueOf(&num).Elem()
err := setFieldValueFromData(field, 42)
require.NoError(t, err)
assert.Equal(t, 42, num)
})
t.Run("Float Field", func(t *testing.T) {
var f float64
field := reflect.ValueOf(&f).Elem()
err := setFieldValueFromData(field, 3.14)
require.NoError(t, err)
assert.InEpsilon(t, 3.14, f, 0.001)
})
t.Run("Bool Field", func(t *testing.T) {
var b bool
field := reflect.ValueOf(&b).Elem()
err := setFieldValueFromData(field, true)
require.NoError(t, err)
assert.True(t, b)
})
t.Run("Unsupported Kind", func(t *testing.T) {
var m map[string]string
field := reflect.ValueOf(&m).Elem()
err := setFieldValueFromData(field, map[string]string{"a": "b"})
require.Error(t, err)
assert.Contains(t, err.Error(), "unsupported type for field")
})
}
================================================
FILE: pkg/gofr/http/metrics.go
================================================
package http
import "context"
// Metrics represents an interface for registering the default metrics in GoFr framework.
type Metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
}
================================================
FILE: pkg/gofr/http/middleware/apikey_auth.go
================================================
// Package middleware provides a collection of middleware functions that handles various aspects of request handling,
// such as authentication, logging, tracing, and metrics collection.
package middleware
import (
"crypto/subtle"
"errors"
"net/http"
"gofr.dev/pkg/gofr/container"
)
var (
errAPIKeyEmpty = errors.New("api keys list is empty")
)
// APIKeyAuthProvider represents a basic authentication provider.
type APIKeyAuthProvider struct {
ValidateFunc func(apiKey string) bool
ValidateFuncWithDatasources func(c *container.Container, apiKey string) bool
Container *container.Container
APIKeys []string
}
// NewAPIKeyAuthProvider instantiates an instance of type AuthProvider interface.
func NewAPIKeyAuthProvider(apiKeys []string) (AuthProvider, error) {
if len(apiKeys) == 0 {
return nil, errAPIKeyEmpty
}
return &APIKeyAuthProvider{APIKeys: apiKeys}, nil
}
// NewAPIKeyAuthProviderWithValidateFunc instantiates an instance of type AuthProvider interface.
func NewAPIKeyAuthProviderWithValidateFunc(c *container.Container,
validateFunc func(*container.Container, string) bool) (AuthProvider, error) {
switch {
case c == nil:
return nil, errContainerNil
case validateFunc == nil:
return nil, errValidateFuncEmpty
default:
return &APIKeyAuthProvider{Container: c, ValidateFuncWithDatasources: validateFunc}, nil
}
}
func (a *APIKeyAuthProvider) ExtractAuthHeader(r *http.Request) (any, ErrorHTTP) {
header, err := getAuthHeaderFromRequest(r, headerXAPIKey, "")
if err != nil {
return nil, err
}
if !a.validateAPIKey(header) {
return nil, NewInvalidAuthorizationHeaderError(headerXAPIKey)
}
return header, nil
}
func (*APIKeyAuthProvider) GetAuthMethod() AuthMethod {
return APIKey
}
// validateAPIKey verifies the given apiKey as per the configured APIKeyAuthProvider.
func (a *APIKeyAuthProvider) validateAPIKey(apiKey string) bool {
switch {
case a.ValidateFuncWithDatasources != nil:
return a.ValidateFuncWithDatasources(a.Container, apiKey)
case a.ValidateFunc != nil:
return a.ValidateFunc(apiKey)
default:
for _, key := range a.APIKeys {
// Use constant time compare to mitigate timing attacks
if subtle.ConstantTimeCompare([]byte(apiKey), []byte(key)) == 1 {
return true
}
}
// constant time compare with dummy key for timing attack mitigation
subtle.ConstantTimeCompare([]byte(apiKey), []byte(dummyValue))
return false
}
}
// APIKeyAuthMiddleware creates a middleware function that enforces API key authentication based on the provided API
// keys or a validation function.
func APIKeyAuthMiddleware(a APIKeyAuthProvider, apiKeys ...string) func(handler http.Handler) http.Handler {
a.APIKeys = apiKeys
return AuthMiddleware(&a)
}
================================================
FILE: pkg/gofr/http/middleware/apikey_auth_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/container"
)
const (
validKey1 string = "valid-key-1"
validKey2 string = "valid-key-2"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_NewAPIKeyAuthProvider(t *testing.T) {
testCases := []struct {
apiKeys []string
provider AuthProvider
err error
}{
{err: errAPIKeyEmpty},
{apiKeys: make([]string, 0), err: errAPIKeyEmpty},
{apiKeys: []string{validKey1}, provider: &APIKeyAuthProvider{APIKeys: []string{validKey1}}, err: nil},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
provider, err := NewAPIKeyAuthProvider(tc.apiKeys)
assert.Equal(t, tc.provider, provider)
assert.Equal(t, tc.err, err)
})
}
}
func Test_NewAPIKeyAuthProviderWithValidateFunc(t *testing.T) {
validateFunc := func(_ *container.Container, _ string) bool {
return true
}
c := container.Container{}
provider := APIKeyAuthProvider{
ValidateFuncWithDatasources: validateFunc,
Container: &c,
}
testCases := []struct {
validateFunc func(*container.Container, string) bool
container *container.Container
provider AuthProvider
err error
}{
{err: errContainerNil},
{validateFunc: validateFunc, err: errContainerNil},
{validateFunc: validateFunc, container: &c, provider: &provider},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
authProvider, err := NewAPIKeyAuthProviderWithValidateFunc(tc.container, tc.validateFunc)
assert.Equal(t, tc.err, err)
if err != nil {
return
}
apiAuthProvider, ok := authProvider.(*APIKeyAuthProvider)
require.True(t, ok)
expected, ok := tc.provider.(*APIKeyAuthProvider)
require.True(t, ok)
assert.Equal(t, expected.Container, apiAuthProvider.Container)
assert.NotNil(t, apiAuthProvider.ValidateFuncWithDatasources)
assert.Nil(t, apiAuthProvider.ValidateFunc)
assert.Empty(t, apiAuthProvider.APIKeys)
})
}
}
func Test_extractAuthHeader(t *testing.T) {
provider := APIKeyAuthProvider{APIKeys: []string{validKey1, validKey2}}
testCases := []struct {
header string
err ErrorHTTP
response any
}{
{err: ErrorMissingAuthHeader{key: headerXAPIKey}},
{header: "some-value", err: ErrorInvalidAuthorizationHeader{key: headerXAPIKey}},
{header: validKey1, response: validKey1},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set(headerXAPIKey, tc.header)
response, err := provider.ExtractAuthHeader(req)
assert.Equal(t, tc.response, response)
assert.Equal(t, tc.err, err)
})
}
}
func Test_authMethod(t *testing.T) {
authProvider := APIKeyAuthProvider{}
assert.Equal(t, APIKey, authProvider.GetAuthMethod())
}
func Test_validateAPIKey(t *testing.T) {
validateFuncSuccess := func(_ string) bool {
return true
}
validateFuncFail := func(_ string) bool {
return false
}
validateFuncDataSourcesPass := func(_ *container.Container, _ string) bool {
return true
}
validateFuncDataSourcesFail := func(_ *container.Container, _ string) bool {
return false
}
apiKeys := []string{validKey1, validKey2}
testCases := []struct {
validateFuncDS func(*container.Container, string) bool // 3 possible values
validateFunc func(string) bool // 3 possible values
apiKeys []string // 2 possible values
apiKey string // 2 possible values
result bool
}{
{validateFuncDS: validateFuncDataSourcesPass, validateFunc: validateFuncFail, apiKeys: apiKeys, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesPass, validateFunc: validateFuncFail, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesPass, validateFunc: validateFuncSuccess, apiKeys: apiKeys, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesPass, validateFunc: validateFuncSuccess, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesPass, apiKeys: apiKeys, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesPass, apiKey: validKey1, result: true},
{validateFuncDS: validateFuncDataSourcesFail, validateFunc: validateFuncFail, apiKeys: apiKeys, apiKey: validKey1, result: false},
{validateFuncDS: validateFuncDataSourcesFail, validateFunc: validateFuncFail, apiKey: validKey1, result: false},
{validateFuncDS: validateFuncDataSourcesFail, validateFunc: validateFuncSuccess, apiKeys: apiKeys, apiKey: validKey1, result: false},
{validateFuncDS: validateFuncDataSourcesFail, validateFunc: validateFuncSuccess, apiKey: validKey1, result: false},
{validateFuncDS: validateFuncDataSourcesFail, apiKeys: apiKeys, apiKey: validKey1, result: false},
{validateFuncDS: validateFuncDataSourcesFail, apiKey: validKey1, result: false},
{validateFunc: validateFuncSuccess, apiKeys: apiKeys, apiKey: validKey1, result: true},
{validateFunc: validateFuncFail, apiKeys: apiKeys, apiKey: validKey1, result: false},
{validateFunc: validateFuncSuccess, apiKey: validKey1, result: true},
{validateFunc: validateFuncFail, apiKey: validKey1, result: false},
{apiKeys: apiKeys, apiKey: validKey1, result: true},
{apiKeys: apiKeys, apiKey: "wrong-key", result: false},
{apiKey: validKey1, result: false},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
authProvider := APIKeyAuthProvider{
ValidateFunc: tc.validateFunc,
ValidateFuncWithDatasources: tc.validateFuncDS,
APIKeys: tc.apiKeys,
}
result := authProvider.validateAPIKey(tc.apiKey)
assert.Equal(t, tc.result, result)
})
}
}
func Test_APIKeyAuthMiddleware(t *testing.T) {
t.Logf("Test_APIKeyAuthMiddleware")
}
================================================
FILE: pkg/gofr/http/middleware/auth.go
================================================
package middleware
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
gofrHttp "gofr.dev/pkg/gofr/http"
)
// AuthMethod represents a custom type to define the different authentication methods supported.
type AuthMethod int
const (
JWTClaim AuthMethod = iota // JWTClaim represents the key used to store JWT claims within the request context.
Username
APIKey
// #nosec G101
headerXAPIKey = "X-Api-Key"
headerAuthorization = "Authorization"
dummyValue = "dummy"
)
var (
errContainerNil = errors.New("container is nil")
errValidateFuncEmpty = errors.New("validate func is empty")
)
type AuthProvider interface {
GetAuthMethod() AuthMethod
ExtractAuthHeader(r *http.Request) (any, ErrorHTTP)
}
// AuthMiddleware creates a middleware function that enforces authentication based on the method provided.
func AuthMiddleware(a AuthProvider) func(handler http.Handler) http.Handler {
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if isWellKnown(r.URL.Path) {
handler.ServeHTTP(w, r)
return
}
authHeader, err := a.ExtractAuthHeader(r)
if err != nil {
responder := gofrHttp.NewResponder(w, r.Method)
responder.Respond(nil, err)
return
}
ctx := context.WithValue(r.Context(), a.GetAuthMethod(), authHeader)
*r = *r.Clone(ctx)
handler.ServeHTTP(w, r)
})
}
}
// getAuthHeaderFromRequest returns the auth value from header.
func getAuthHeaderFromRequest(r *http.Request, key, prefix string) (string, ErrorHTTP) {
header := r.Header.Get(key)
if header == "" {
return header, NewMissingAuthHeaderError(key)
}
if prefix == "" {
return header, nil
}
parts := strings.Split(header, " ")
if len(parts) != 2 || parts[0] != prefix || parts[1] == "" {
return "", NewInvalidAuthorizationHeaderFormatError(key, fmt.Sprintf("header should be `%s `", prefix))
}
return parts[1], nil
}
================================================
FILE: pkg/gofr/http/middleware/auth_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestAuthMiddleware(t *testing.T) {
errBody := `{"error":{"message":"invalid auth header in key 'Authorization'"}}`
testCases := []struct {
url string
success bool
statusCode int
expectedHeader any
expectedBody string
}{
{url: "/.well-known/health", success: true, statusCode: http.StatusOK, expectedBody: `OK`},
{url: "/.well-known/health", statusCode: http.StatusOK, expectedBody: `OK`},
{url: "/", success: true, statusCode: http.StatusOK, expectedHeader: "user-header-string", expectedBody: `OK`},
{url: "/", success: false, statusCode: http.StatusUnauthorized, expectedBody: errBody},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
authProvider := &MockAuthProvider{
success: tc.success,
method: Username,
authHeader: "user-header-string",
}
mockHandler := &MockHandler{t: t, authMethod: authProvider.method, authHeader: tc.expectedHeader}
middleware := AuthMiddleware(authProvider)
handler := middleware(mockHandler)
req := httptest.NewRequest(http.MethodGet, tc.url, http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, tc.statusCode, rr.Code)
assert.Equal(t, tc.statusCode == http.StatusOK, mockHandler.handlerCalled)
assert.Equal(t, tc.expectedBody, strings.TrimSuffix(rr.Body.String(), "\n"))
if strings.HasPrefix(tc.url, "/.well-known") {
return
}
assert.True(t, authProvider.extractAuthHeaderCalled)
assert.Equal(t, tc.statusCode == http.StatusOK, authProvider.getAuthMethodCalled)
})
}
}
func Test_getAuthHeaderValue(t *testing.T) {
testCases := []struct {
header string
key string
prefix string
result string
err ErrorHTTP
}{
{err: ErrorMissingAuthHeader{}},
{key: headerAuthorization, prefix: "Bearer", err: ErrorMissingAuthHeader{key: headerAuthorization}},
{key: headerAuthorization, prefix: "", err: ErrorMissingAuthHeader{key: headerAuthorization}},
{key: headerAuthorization, prefix: "", header: "some value", result: "some value"},
{key: headerAuthorization, prefix: "Bearer", err: ErrorMissingAuthHeader{key: headerAuthorization}},
{header: "Basic", key: headerAuthorization, prefix: "Bearer", err: ErrorInvalidAuthorizationHeaderFormat{
key: headerAuthorization,
errMessage: "header should be `Bearer `",
}},
{header: "Bearer", key: headerAuthorization, prefix: "Bearer", err: ErrorInvalidAuthorizationHeaderFormat{
key: headerAuthorization,
errMessage: "header should be `Bearer `",
}},
{header: "Bearer ", key: headerAuthorization, prefix: "Bearer", err: ErrorInvalidAuthorizationHeaderFormat{
key: headerAuthorization,
errMessage: "header should be `Bearer `",
}},
{header: "Bearer some-value", key: headerAuthorization, prefix: "Bearer", result: "some-value"},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set("Authorization", tc.header)
result, err := getAuthHeaderFromRequest(req, tc.key, tc.prefix)
assert.Equal(t, tc.result, result)
assert.Equal(t, tc.err, err)
})
}
}
type MockAuthProvider struct {
success bool
method AuthMethod
authHeader any
extractAuthHeaderCalled bool
getAuthMethodCalled bool
}
func (p *MockAuthProvider) GetAuthMethod() AuthMethod {
p.getAuthMethodCalled = true
return p.method
}
func (p *MockAuthProvider) ExtractAuthHeader(_ *http.Request) (any, ErrorHTTP) {
p.extractAuthHeaderCalled = true
if p.success {
return p.authHeader, nil
}
return nil, ErrorInvalidAuthorizationHeader{key: headerAuthorization}
}
type MockHandler struct {
t *testing.T
handlerCalled bool
authMethod AuthMethod
authHeader any
}
func (h *MockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.t.Helper()
h.handlerCalled = true
authHeader := r.Context().Value(h.authMethod)
assert.Equal(h.t, h.authHeader, authHeader)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}
================================================
FILE: pkg/gofr/http/middleware/basic_auth.go
================================================
package middleware
import (
"crypto/subtle"
"encoding/base64"
"errors"
"net/http"
"strings"
"gofr.dev/pkg/gofr/container"
)
// BasicAuthProvider represents a basic authentication provider.
type BasicAuthProvider struct {
Users map[string]string
ValidateFunc func(username, password string) bool
ValidateFuncWithDatasources func(c *container.Container, username, password string) bool
Container *container.Container
}
var (
errUserListEmpty = errors.New("user list is empty")
)
// NewBasicAuthProvider returns an instance of type AuthProvider interface.
func NewBasicAuthProvider(users map[string]string) (AuthProvider, error) {
if len(users) == 0 {
return nil, errUserListEmpty
}
return &BasicAuthProvider{Users: users}, nil
}
// NewBasicAuthProviderWithValidateFunc returns an instance of type AuthProvider interface.
func NewBasicAuthProviderWithValidateFunc(c *container.Container,
validateFunc func(c *container.Container, username, password string) bool) (AuthProvider, error) {
if validateFunc == nil {
return nil, errValidateFuncEmpty
}
if c == nil {
return nil, errContainerNil
}
return &BasicAuthProvider{ValidateFuncWithDatasources: validateFunc, Container: c}, nil
}
// ExtractAuthHeader retrieves & returns validated value from auth header.
func (a *BasicAuthProvider) ExtractAuthHeader(r *http.Request) (any, ErrorHTTP) {
header, err := getAuthHeaderFromRequest(r, headerAuthorization, "Basic")
if err != nil {
return nil, err
}
userName, password, ok := parseBasicAuth(header)
if !ok {
return "", NewInvalidAuthorizationHeaderFormatError(headerAuthorization,
"credentials should be in the format base64(username:password)")
}
if !a.validateCredentials(userName, password) {
return nil, NewInvalidAuthorizationHeaderError(headerAuthorization)
}
return userName, nil
}
// GetAuthMethod returns authMethod Username.
func (*BasicAuthProvider) GetAuthMethod() AuthMethod {
return Username
}
// parseBasicAuth extracts and decodes the username and password from the Authorization header.
func parseBasicAuth(authHeader string) (username, password string, ok bool) {
if authHeader == "" {
return "", "", false
}
payload, err := base64.StdEncoding.DecodeString(authHeader)
if err != nil {
return "", "", false
}
username, password, found := strings.Cut(string(payload), ":")
if !found { // Ensure both username and password are returned as empty if colon separator is missing
return "", "", false
}
return username, password, true
}
// validateCredentials checks the provided username and password against the BasicAuthProvider.
func (a *BasicAuthProvider) validateCredentials(username, password string) bool {
switch {
case a.ValidateFuncWithDatasources != nil:
return a.ValidateFuncWithDatasources(a.Container, username, password)
case a.ValidateFunc != nil:
return a.ValidateFunc(username, password)
default:
storedPass, ok := a.Users[username]
if !ok {
// Use dummyValue constant
subtle.ConstantTimeCompare([]byte(password), []byte(dummyValue))
// Add exactly one blank line before return
return false
}
// constant time compare for password comparison
return subtle.ConstantTimeCompare([]byte(password), []byte(storedPass)) == 1
}
}
// BasicAuthMiddleware creates a middleware function that enforces basic authentication using the provided BasicAuthProvider.
func BasicAuthMiddleware(basicAuthProvider BasicAuthProvider) func(handler http.Handler) http.Handler {
return AuthMiddleware(&basicAuthProvider)
}
================================================
FILE: pkg/gofr/http/middleware/basic_auth_test.go
================================================
package middleware
import (
"encoding/base64"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/container"
)
func TestNewBasicAuthProvider(t *testing.T) {
testCases := []struct {
users map[string]string
provider AuthProvider
err error
}{
{err: errUserListEmpty},
{users: map[string]string{"user": "password"}, provider: &BasicAuthProvider{Users: map[string]string{"user": "password"}}},
}
for _, tc := range testCases {
authProvider, err := NewBasicAuthProvider(tc.users)
assert.Equal(t, tc.provider, authProvider)
assert.Equal(t, tc.err, err)
}
}
func TestNewBasicAuthProviderWithValidateFunc(t *testing.T) {
validateFunc := func(_ *container.Container, _, _ string) bool { return true }
testCases := []struct {
c *container.Container
validateFunc func(c *container.Container, username, password string) bool
err error
}{
{err: errValidateFuncEmpty},
{validateFunc: validateFunc, err: errContainerNil},
{c: &container.Container{}, validateFunc: validateFunc},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
authProvider, err := NewBasicAuthProviderWithValidateFunc(tc.c, tc.validateFunc)
assert.Equal(t, tc.err, err)
if err != nil {
return
}
assert.NotNil(t, authProvider)
})
}
}
func TestBasicAuthMiddleware_extractAuthHeader(t *testing.T) {
users := map[string]string{"storedUser": "storedPass", "user": "password", "newUser": ""}
testCases := []struct {
header string
response any
err error
}{
{
err: ErrorMissingAuthHeader{key: headerAuthorization},
},
{
header: "Basic wrong-header",
err: ErrorInvalidAuthorizationHeaderFormat{
key: headerAuthorization,
errMessage: "credentials should be in the format base64(username:password)",
},
response: "",
},
{
header: "Basic " + base64.StdEncoding.EncodeToString([]byte("storedUser:storedPass")),
response: "storedUser",
},
}
provider := &BasicAuthProvider{Users: users}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set(headerAuthorization, tc.header)
response, err := provider.ExtractAuthHeader(req)
assert.Equal(t, tc.response, response)
assert.Equal(t, tc.err, err)
})
}
}
func TestBasicAuthProvider_getAuthMethod(t *testing.T) {
provider := BasicAuthProvider{}
assert.Equal(t, Username, provider.GetAuthMethod())
}
func TestParseBasicAuth(t *testing.T) {
validHeader := base64.StdEncoding.EncodeToString([]byte("user:password"))
invalidHeader := base64.StdEncoding.EncodeToString([]byte("userpassword"))
testCases := []struct {
name string
authHeader string
expectedUser string
expectedPass string
expectedOk bool
}{
{name: "Valid Basic Auth", authHeader: validHeader, expectedUser: "user", expectedPass: "password", expectedOk: true},
{name: "Invalid Encoding", authHeader: "invalid_base64", expectedUser: "", expectedPass: "", expectedOk: false},
{name: "Missing Colon Separator", authHeader: invalidHeader, expectedUser: "", expectedPass: "", expectedOk: false},
{name: "Empty Authorization Header", authHeader: "", expectedUser: "", expectedPass: "", expectedOk: false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
username, password, ok := parseBasicAuth(tc.authHeader)
assert.Equal(t, tc.expectedOk, ok)
assert.Equal(t, tc.expectedUser, username)
assert.Equal(t, tc.expectedPass, password)
})
}
}
func TestBasicAuthMiddleware_validateCredentials(t *testing.T) {
users := map[string]string{"storedUser": "storedPass", "user": "password", "newUser": ""}
validateFuncPass := func(_, _ string) bool { return true }
validateFuncFail := func(_, _ string) bool { return false }
validateFuncDataStorePass := func(_ *container.Container, _, _ string) bool { return true }
validateFuncDataStoreFail := func(_ *container.Container, _, _ string) bool { return false }
testCases := []struct {
username string
password string
validateFunc func(username, password string) bool
validateFuncWithDatasources func(c *container.Container, username, password string) bool
users map[string]string
success bool
}{
{
username: "storedUser",
password: "storedPass",
validateFunc: validateFuncPass,
validateFuncWithDatasources: validateFuncDataStoreFail,
users: users,
success: false,
},
{
username: "storedUser",
password: "storedPass",
validateFunc: validateFuncFail,
validateFuncWithDatasources: validateFuncDataStorePass,
users: users,
success: true,
},
{
username: "storedUser",
password: "storedPass",
validateFunc: validateFuncPass,
validateFuncWithDatasources: nil,
users: users,
success: true,
},
{
username: "storedUser",
password: "storedPass",
validateFunc: validateFuncFail,
validateFuncWithDatasources: nil,
users: users,
success: false,
},
{
username: "storedUser",
password: "storedPass",
users: users,
success: true,
},
{
username: "storedUser",
password: "wrongPass",
users: users,
success: false,
},
{
username: "newUser",
password: "",
users: users,
success: true,
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test Case #%d", i), func(t *testing.T) {
provider := BasicAuthProvider{
Users: tc.users,
ValidateFunc: tc.validateFunc,
ValidateFuncWithDatasources: tc.validateFuncWithDatasources,
Container: nil,
}
success := provider.validateCredentials(tc.username, tc.password)
assert.Equal(t, tc.success, success)
})
}
}
================================================
FILE: pkg/gofr/http/middleware/config.go
================================================
package middleware
import (
"strconv"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/service"
)
type Config struct {
CorsHeaders map[string]string
LogProbes LogProbes
}
type LogProbes struct {
Disabled bool
Paths []string
}
func GetConfigs(c config.Config) Config {
middlewareConfigs := Config{
CorsHeaders: make(map[string]string),
}
allowedCORSHeaders := []string{
"ACCESS_CONTROL_ALLOW_ORIGIN",
"ACCESS_CONTROL_ALLOW_HEADERS",
"ACCESS_CONTROL_ALLOW_CREDENTIALS",
"ACCESS_CONTROL_EXPOSE_HEADERS",
"ACCESS_CONTROL_MAX_AGE",
}
for _, v := range allowedCORSHeaders {
if val := c.Get(v); val != "" {
middlewareConfigs.CorsHeaders[convertHeaderNames(v)] = val
}
}
// Config values for Log Probes
logDisableProbes := c.GetOrDefault("LOG_DISABLE_PROBES", "false")
middlewareConfigs.LogProbes.Paths = []string{service.HealthPath, service.AlivePath}
// Convert the string value to a boolean
value, err := strconv.ParseBool(logDisableProbes)
if err == nil {
middlewareConfigs.LogProbes.Disabled = value
}
return middlewareConfigs
}
func convertHeaderNames(header string) string {
words := strings.Split(header, "_")
titleCaser := cases.Title(language.Und)
for i, v := range words {
words[i] = titleCaser.String(strings.ToLower(v))
}
return strings.Join(words, "-")
}
================================================
FILE: pkg/gofr/http/middleware/config_test.go
================================================
package middleware
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/config"
)
func TestGetConfigs(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"ACCESS_CONTROL_ALLOW_ORIGIN": "*",
"ACCESS_CONTROL_ALLOW_HEADERS": "Authorization, Content-Type",
"ACCESS_CONTROL_ALLOW_CREDENTIALS": "true",
"ACCESS_CONTROL_ALLOW_CUSTOMHEADER": "abc",
})
middlewareConfigs := GetConfigs(mockConfig)
expectedConfigs := map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Allow-Credentials": "true",
}
assert.Equal(t, expectedConfigs, middlewareConfigs.CorsHeaders, "TestGetConfigs Failed!")
assert.NotContains(t, middlewareConfigs.CorsHeaders, "Access-Control-Allow-CustomHeader", "TestGetConfigs Failed!")
}
func TestLogDisableProbesConfig(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"LOG_DISABLE_PROBES": "true",
})
middlewareConfigs := GetConfigs(mockConfig)
assert.True(t, middlewareConfigs.LogProbes.Disabled, "TestLogDisableProbesConfig Failed!")
}
================================================
FILE: pkg/gofr/http/middleware/cors.go
================================================
package middleware
import (
"net/http"
"strings"
)
const (
allowedHeaders = "Authorization, Content-Type, x-requested-with, origin, true-client-ip, X-Correlation-ID"
)
// CORS is a middleware that adds CORS (Cross-Origin Resource Sharing) headers to the response.
func CORS(middlewareConfigs map[string]string, routes *[]string) func(inner http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
setMiddlewareHeaders(middlewareConfigs, *routes, w)
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
inner.ServeHTTP(w, r)
})
}
}
func setMiddlewareHeaders(middlewareConfigs map[string]string, routes []string, w http.ResponseWriter) {
routes = append(routes, "OPTIONS")
// Set default headers
defaultHeaders := map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": strings.Join(routes, ", "),
"Access-Control-Allow-Headers": allowedHeaders,
}
// Add custom headers to the default headers
for header, defaultValue := range defaultHeaders {
if customValue, ok := middlewareConfigs[header]; ok && customValue != "" {
if header == "Access-Control-Allow-Headers" {
w.Header().Set(header, defaultValue+", "+customValue)
} else {
w.Header().Set(header, customValue)
}
} else {
w.Header().Set(header, defaultValue)
}
}
// Handle additional custom headers (not part of defaultHeaders)
for header, customValue := range middlewareConfigs {
if _, ok := defaultHeaders[header]; !ok {
w.Header().Set(header, customValue)
}
}
}
================================================
FILE: pkg/gofr/http/middleware/cors_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
type MockHandlerForCORS struct {
statusCode int
response string
}
// ServeHTTP is used for testing different panic recovery cases.
func (r *MockHandlerForCORS) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(r.statusCode)
_, _ = w.Write([]byte(r.response))
}
func Test_CORS(t *testing.T) {
tests := []struct {
method string
registeredRoutes *[]string
respBody string
respCode int
expHeaders int
}{
{http.MethodGet, &[]string{"GET,POST"}, "Sample Response", http.StatusFound, 3},
{http.MethodOptions, &[]string{"PUT,DELETE,GET,POST"}, "", http.StatusOK, 3},
}
for i, tc := range tests {
handler := CORS(nil, tc.registeredRoutes)(&MockHandlerForCORS{statusCode: http.StatusFound, response: "Sample Response"})
req := httptest.NewRequest(tc.method, "/hello", http.NoBody)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin"), "TEST[%d], Failed.\n", i)
assert.Equal(t, strings.Join(*tc.registeredRoutes, ", ")+", OPTIONS",
w.Header().Get("Access-Control-Allow-Methods"), "TEST[%d], Failed.\n", i)
assert.Len(t, w.Header(), tc.expHeaders, "TEST[%d], Failed.\n", i)
assert.Equal(t, tc.respCode, w.Code, "TEST[%d], Failed.\n", i)
assert.Equal(t, tc.respBody, w.Body.String(), "TEST[%d], Failed.\n", i)
}
}
func TestSetMiddlewareHeaders(t *testing.T) {
testCases := []struct {
environmentConfig map[string]string
registeredRoutes []string
expectedHeaders map[string]string
}{
{
environmentConfig: map[string]string{},
registeredRoutes: []string{"GET"},
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": allowedHeaders,
"Access-Control-Allow-Methods": "GET, OPTIONS",
},
},
{
environmentConfig: map[string]string{"Access-Control-Allow-Headers": "clientid"},
registeredRoutes: []string{"POST, PUT"},
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": allowedHeaders + ", clientid",
"Access-Control-Allow-Methods": "POST, PUT, OPTIONS",
},
},
{
environmentConfig: map[string]string{
"Access-Control-Max-Age": strconv.Itoa(600),
"Access-Control-Allow-Origin": "same-origin",
},
registeredRoutes: []string{},
expectedHeaders: map[string]string{
"Access-Control-Max-Age": strconv.Itoa(600),
"Access-Control-Allow-Origin": "same-origin",
"Access-Control-Allow-Headers": allowedHeaders,
"Access-Control-Allow-Methods": "OPTIONS",
},
},
}
for _, tc := range testCases {
w := httptest.NewRecorder()
setMiddlewareHeaders(tc.environmentConfig, tc.registeredRoutes, w)
// Check if the actual headers match the expected headers
for header, expectedValue := range tc.expectedHeaders {
actualValue := w.Header().Get(header)
if actualValue != expectedValue {
t.Errorf("Header %s: expected %s, got %s", header, expectedValue, actualValue)
}
}
}
}
================================================
FILE: pkg/gofr/http/middleware/errors.go
================================================
package middleware
import (
"fmt"
"net/http"
)
// ErrorHTTP represents an error specific to HTTP operations.
type ErrorHTTP interface {
StatusCode() int
error
}
// ErrorMissingAuthHeader represents the scenario where the auth header is missing from the request.
type ErrorMissingAuthHeader struct {
key string
}
func NewMissingAuthHeaderError(key string) ErrorMissingAuthHeader {
return ErrorMissingAuthHeader{key: key}
}
func (e ErrorMissingAuthHeader) Error() string {
return fmt.Sprintf("missing auth header in key '%s'", e.key)
}
func (ErrorMissingAuthHeader) StatusCode() int {
return http.StatusUnauthorized
}
// ErrorInvalidAuthorizationHeader represents the scenario where the auth header errMessage is invalid.
type ErrorInvalidAuthorizationHeader struct {
key string
}
func NewInvalidAuthorizationHeaderError(key string) ErrorInvalidAuthorizationHeader {
return ErrorInvalidAuthorizationHeader{key: key}
}
func (e ErrorInvalidAuthorizationHeader) Error() string {
return fmt.Sprintf("invalid auth header in key '%s'", e.key)
}
func (ErrorInvalidAuthorizationHeader) StatusCode() int {
return http.StatusUnauthorized
}
// ErrorInvalidAuthorizationHeaderFormat represents the scenario where the auth header errMessage is invalid.
type ErrorInvalidAuthorizationHeaderFormat struct {
key string
errMessage string
}
func NewInvalidAuthorizationHeaderFormatError(key, format string) ErrorInvalidAuthorizationHeaderFormat {
return ErrorInvalidAuthorizationHeaderFormat{key: key, errMessage: format}
}
func (e ErrorInvalidAuthorizationHeaderFormat) Error() string {
return fmt.Sprintf("invalid value in '%s' header - %s", e.key, e.errMessage)
}
func (ErrorInvalidAuthorizationHeaderFormat) StatusCode() int {
return http.StatusUnauthorized
}
type ErrorForbidden struct {
message string
}
func NewUnauthorized(message string) ErrorForbidden {
return ErrorForbidden{message: message}
}
func (e ErrorForbidden) Error() string {
if e.message != "" {
return e.message
}
return http.StatusText(http.StatusForbidden)
}
func (ErrorForbidden) StatusCode() int {
return http.StatusForbidden
}
type Field struct {
key string
format string
}
type ErrorBadRequest struct {
fields []Field
}
func NewBadRequest(fields []Field) ErrorBadRequest {
return ErrorBadRequest{fields: fields}
}
func (e ErrorBadRequest) Error() string {
return fmt.Sprintf("bad request, invalid value in %d fields", len(e.fields))
}
func (ErrorBadRequest) StatusCode() int {
return http.StatusBadRequest
}
type ErrorInvalidConfiguration struct {
message string
}
func NewInvalidConfigurationError(message string) ErrorInvalidConfiguration {
return ErrorInvalidConfiguration{message: message}
}
func (e ErrorInvalidConfiguration) Error() string {
return fmt.Sprintf("invalid configuration %s - please contact administrator", e.message)
}
func (ErrorInvalidConfiguration) StatusCode() int {
return http.StatusInternalServerError
}
================================================
FILE: pkg/gofr/http/middleware/errors_test.go
================================================
package middleware
import (
"fmt"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHTTPError(t *testing.T) {
testCases := []struct {
err ErrorHTTP
statusCode int
message string
}{
{
err: ErrorMissingAuthHeader{key: "X-Api-Key"},
statusCode: http.StatusUnauthorized,
message: "missing auth header in key 'X-Api-Key'",
},
{
err: ErrorInvalidAuthorizationHeaderFormat{key: "Authorization", errMessage: "Bearer {value}"},
statusCode: http.StatusUnauthorized,
message: "invalid value in 'Authorization' header - Bearer {value}",
},
{
err: ErrorForbidden{message: "operation forbidden"},
statusCode: http.StatusForbidden,
message: "operation forbidden",
},
{
err: ErrorForbidden{},
statusCode: http.StatusForbidden,
message: "Forbidden",
},
{
err: ErrorBadRequest{fields: []Field{{key: "name", format: "uppercase"}, {key: "value", format: "uppercase"}}},
statusCode: http.StatusBadRequest,
message: "bad request, invalid value in 2 fields",
},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test Case #%d", i), func(t *testing.T) {
assert.Equal(t, tc.statusCode, tc.err.StatusCode())
assert.Equal(t, tc.message, tc.err.Error())
})
}
}
================================================
FILE: pkg/gofr/http/middleware/logger.go
================================================
package middleware
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"runtime/debug"
"strings"
"time"
"go.opentelemetry.io/otel/trace"
)
var errHijackNotSupported = errors.New("response writer does not support hijacking")
// StatusResponseWriter Defines own Response Writer to be used for logging of status - as http.ResponseWriter does not let us read status.
type StatusResponseWriter struct {
http.ResponseWriter
status int
// wroteHeader keeps a flag to keep a check that the framework do not attempt to write the header again. This was previously causing
// `superfluous response.WriteHeader call`. This is particularly helpful in scenarios where the developer has already written header
// in any custom middlewares.
wroteHeader bool
}
func (w *StatusResponseWriter) WriteHeader(status int) {
if w.wroteHeader { // Prevent duplicate calls
return
}
w.status = status
w.wroteHeader = true
w.ResponseWriter.WriteHeader(status)
}
// Hijack implements the http.Hijacker interface. So that we are able to upgrade to a websocket
// connection that requires the responseWriter implementation to implement this method.
func (w *StatusResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hijacker, ok := w.ResponseWriter.(http.Hijacker); ok {
return hijacker.Hijack()
}
return nil, nil, fmt.Errorf("%w: cannot hijack connection", errHijackNotSupported)
}
// RequestLog represents a log entry for HTTP requests.
type RequestLog struct {
TraceID string `json:"trace_id,omitempty"`
SpanID string `json:"span_id,omitempty"`
StartTime string `json:"start_time,omitempty"`
ResponseTime int64 `json:"response_time,omitempty"`
Method string `json:"method,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
IP string `json:"ip,omitempty"`
URI string `json:"uri,omitempty"`
Response int `json:"response,omitempty"`
}
func (rl *RequestLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m "+
"%8d\u001B[38;5;8mµs\u001B[0m %s %s \n", rl.TraceID, colorForStatusCode(rl.Response), rl.Response, rl.ResponseTime, rl.Method, rl.URI)
}
func colorForStatusCode(status int) int {
const (
blue = 34
red = 202
yellow = 220
)
switch {
case status >= 200 && status < 300:
return blue
case status >= 400 && status < 500:
return yellow
case status >= 500 && status < 600:
return red
}
return 0
}
type logger interface {
Log(...any)
Error(...any)
}
// Logging is a middleware which logs response status and time in milliseconds along with other data.
func Logging(probes LogProbes, logger logger) func(inner http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
srw := &StatusResponseWriter{ResponseWriter: w}
traceID := trace.SpanFromContext(r.Context()).SpanContext().TraceID().String()
spanID := trace.SpanFromContext(r.Context()).SpanContext().SpanID().String()
srw.Header().Set("X-Correlation-ID", traceID)
defer func() { panicRecovery(recover(), srw, logger) }()
// Skip logging for default probe paths if log probes are disabled
if isLogProbeDisabled(probes, r.URL.Path) {
inner.ServeHTTP(w, r)
return
}
defer handleRequestLog(srw, r, start, traceID, spanID, logger)
inner.ServeHTTP(srw, r)
})
}
}
func handleRequestLog(srw *StatusResponseWriter, r *http.Request, start time.Time, traceID, spanID string, logger logger) {
l := &RequestLog{
TraceID: traceID,
SpanID: spanID,
StartTime: start.Format("2006-01-02T15:04:05.999999999-07:00"),
ResponseTime: time.Since(start).Nanoseconds() / 1000,
Method: r.Method,
UserAgent: r.UserAgent(),
IP: getIPAddress(r),
URI: r.RequestURI,
Response: srw.status,
}
if logger != nil {
if srw.status >= http.StatusInternalServerError {
logger.Error(l)
} else {
logger.Log(l)
}
}
}
// isLogProbeDisabled checks if probes are disabled to skip logging for default probe paths
// and additional health check paths of services.
func isLogProbeDisabled(probes LogProbes, urlPath string) bool {
// if probes is not disabled, dont need to check for default probe paths
if !probes.Disabled {
return false
}
// check if urlPath is in the list of default probe paths and matches any of the values in the map
for _, path := range probes.Paths {
if urlPath == path && probes.Disabled {
return true
}
}
return false
}
func getIPAddress(r *http.Request) string {
ips := strings.Split(r.Header.Get("X-Forwarded-For"), ",")
// According to GCLB Documentation (https://cloud.google.com/load-balancing/docs/https/), IPs are added in following sequence.
// X-Forwarded-For: , , ,
ipAddress := ips[0]
if ipAddress == "" {
ipAddress = r.RemoteAddr
}
return strings.TrimSpace(ipAddress)
}
type panicLog struct {
Error string `json:"error,omitempty"`
StackTrace string `json:"stack_trace,omitempty"`
}
func panicRecovery(re any, w http.ResponseWriter, logger logger) {
if re == nil {
return
}
var e string
switch t := re.(type) {
case string:
e = t
case error:
e = t.Error()
default:
e = "Unknown panic type"
}
logger.Error(panicLog{
Error: e,
StackTrace: string(debug.Stack()),
})
w.WriteHeader(http.StatusInternalServerError)
res := map[string]any{"code": http.StatusInternalServerError, "status": "ERROR", "message": "Some unexpected error has occurred"}
_ = json.NewEncoder(w).Encode(res)
}
================================================
FILE: pkg/gofr/http/middleware/logger_test.go
================================================
package middleware
import (
"bufio"
"bytes"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func Test_getIPAddress(t *testing.T) {
{
// When RemoteAddr is set
addr := "0.0.0.0:8080"
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
require.NoError(t, err, "TEST Failed.\n")
req.RemoteAddr = addr
ip := getIPAddress(req)
assert.Equal(t, addr, ip, "TEST Failed.\n")
}
{
// When `X-Forwarded-For` header is set
addr := "192.168.0.1:8080"
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
require.NoError(t, err, "TEST Failed.\n")
req.Header.Set("X-Forwarded-For", addr)
ip := getIPAddress(req)
assert.Equal(t, addr, ip, "TEST Failed.\n")
}
}
func Test_LoggingMiddleware(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testHandler))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "GET 200")
}
func Test_LoggingMiddlewareProbesEnable(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy/.well-known/alive", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
Paths: []string{"/.well-known/alive", "/.well-known/health"},
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testHandler))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "GET 200")
}
func Test_LoggingMiddlewareProbesDisable(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy/.well-known/alive", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: true,
Paths: []string{"/.well-known/alive", "/.well-known/health"},
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testHandler))
handler.ServeHTTP(rr, req)
})
assert.Empty(t, logs, "TEST Failed.\n")
}
func Test_LoggingMiddlewareError(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
}
handler := Logging(probes, logging.NewMockLogger(logging.ERROR))(http.HandlerFunc(testHandlerError))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "GET 500")
}
// Test handler that uses the middleware.
func testHandler(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("Test Handler"))
}
// Test handler for internalServerErrors that uses the middleware.
func testHandlerError(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte("error"))
}
func Test_LoggingMiddlewareStringPanicHandling(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testStringPanicHandler))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "gofr.dev/pkg/gofr/http/middleware.testStringPanicHandler")
}
// Test handler that uses the middleware.
func testStringPanicHandler(_ http.ResponseWriter, r *http.Request) {
panic(r.URL.Path)
}
func Test_LoggingMiddlewareErrorPanicHandling(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testErrorPanicHandler))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "gofr.dev/pkg/gofr/http/middleware.testErrorPanicHandler")
}
// Test handler that uses the middleware.
func testErrorPanicHandler(http.ResponseWriter, *http.Request) {
panic(testutil.CustomError{ErrorMessage: "panic"})
}
func Test_LoggingMiddlewareUnknownPanicHandling(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://dummy", http.NoBody)
rr := httptest.NewRecorder()
probes := LogProbes{
Disabled: false,
}
handler := Logging(probes, logging.NewMockLogger(logging.DEBUG))(http.HandlerFunc(testUnknownPanicHandler))
handler.ServeHTTP(rr, req)
})
assert.Contains(t, logs, "gofr.dev/pkg/gofr/http/middleware.testUnknownPanicHandler")
}
// Test handler that uses the middleware.
func testUnknownPanicHandler(w http.ResponseWriter, _ *http.Request) {
panic(w)
}
func TestRequestLog_PrettyPrint(t *testing.T) {
rl := &RequestLog{
TraceID: "7e5c0e9a58839071d4d006dd1d0f4f3a",
SpanID: "b19d9aa6323b29bb",
StartTime: "2024-04-16T13:34:35.761893+05:30",
ResponseTime: 1432,
Method: "GET",
UserAgent: "",
IP: "[::1]:59614",
URI: "/test",
Response: 200,
}
w := new(bytes.Buffer)
rl.PrettyPrint(w)
assert.Equal(t, "\u001B[38;5;8m7e5c0e9a58839071d4d006dd1d0f4f3a \u001B[38;5;34m200 \u001B[0m"+
" 1432\u001B[38;5;8mµs\u001B[0m GET /test \n", w.String())
}
func Test_ColorForStatusCode(t *testing.T) {
testCases := []struct {
desc string
code int
expOut int
}{
{desc: "200 OK", code: 200, expOut: 34},
{desc: "201 Created", code: 201, expOut: 34},
{desc: "400 Bad Request", code: 400, expOut: 220},
{desc: "409 Conflict", code: 409, expOut: 220},
{desc: "500 Internal Srv Error", code: 500, expOut: 202},
{desc: "unknown status code", code: 0, expOut: 0},
}
for _, tc := range testCases {
out := colorForStatusCode(tc.code)
assert.Equal(t, tc.expOut, out)
}
}
func Test_StatusResponseWriter_WriteHeader(t *testing.T) {
tests := []struct {
name string
status int
expectedStatus int
}{
{"WriteHeader 200", 200, 200},
{"WriteHeader 404", 404, 404},
{"WriteHeader 500", 500, 500},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
srw := &StatusResponseWriter{ResponseWriter: rr}
srw.WriteHeader(tt.status)
require.Equal(t, tt.expectedStatus, srw.status, "status mismatch")
require.True(t, srw.wroteHeader, "expected wroteHeader to be true")
require.Equal(t, tt.expectedStatus, rr.Code, "recorder status mismatch")
})
}
}
func Test_StatusResponseWriter_WriteHeader_DuplicateCalls(t *testing.T) {
rr := httptest.NewRecorder()
srw := &StatusResponseWriter{ResponseWriter: rr}
srw.WriteHeader(http.StatusOK)
srw.WriteHeader(http.StatusNotFound) // This should be ignored
require.Equal(t, http.StatusOK, srw.status, "expected status 200")
require.Equal(t, http.StatusOK, rr.Code, "expected recorder status 200")
}
func Test_StatusResponseWriter_Hijack_Supported(t *testing.T) {
rr := httptest.NewRecorder()
srw := &StatusResponseWriter{ResponseWriter: rr}
// Wrap the recorder in a type that supports Hijack
hijacker := &hijackableResponseRecorder{rr}
srw.ResponseWriter = hijacker
conn, rw, err := srw.Hijack()
require.NoError(t, err, "expected no error during Hijack")
require.NotNil(t, conn, "expected conn to be non-nil")
require.NotNil(t, rw, "expected rw to be non-nil")
}
func Test_StatusResponseWriter_Hijack_NotSupported(t *testing.T) {
rr := httptest.NewRecorder()
srw := &StatusResponseWriter{ResponseWriter: rr}
_, _, err := srw.Hijack()
require.Error(t, err, "expected an error during Hijack")
require.ErrorIs(t, err, errHijackNotSupported, "expected error to be errHijackNotSupported")
}
// hijackableResponseRecorder is a custom ResponseRecorder that supports the Hijack method.
type hijackableResponseRecorder struct {
*httptest.ResponseRecorder
}
func (*hijackableResponseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
conn := &mockConn{}
rw := bufio.NewReadWriter(bufio.NewReader(bytes.NewReader(nil)), bufio.NewWriter(bytes.NewBuffer(nil)))
return conn, rw, nil
}
// mockConn is a mock implementation of net.Conn for testing purposes.
type mockConn struct{}
func (*mockConn) Read([]byte) (n int, err error) { return 0, nil }
func (*mockConn) Write([]byte) (n int, err error) { return 0, nil }
func (*mockConn) Close() error { return nil }
func (*mockConn) LocalAddr() net.Addr { return &mockAddr{} }
func (*mockConn) RemoteAddr() net.Addr { return &mockAddr{} }
func (*mockConn) SetDeadline(time.Time) error { return nil }
func (*mockConn) SetReadDeadline(time.Time) error { return nil }
func (*mockConn) SetWriteDeadline(time.Time) error { return nil }
// mockAddr is a mock implementation of net.Addr for testing purposes.
type mockAddr struct{}
func (*mockAddr) Network() string { return "tcp" }
func (*mockAddr) String() string { return "127.0.0.1:8080" }
================================================
FILE: pkg/gofr/http/middleware/metrics.go
================================================
package middleware
import (
"context"
"fmt"
"net/http"
"path/filepath"
"strings"
"time"
"github.com/gorilla/mux"
)
type metrics interface {
IncrementCounter(ctx context.Context, name string, labels ...string)
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
// Metrics is a middleware that records request response time metrics using the provided metrics interface.
func Metrics(metrics metrics) func(inner http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
srw := &StatusResponseWriter{ResponseWriter: w}
path, _ := mux.CurrentRoute(r).GetPathTemplate()
ext := strings.ToLower(filepath.Ext(r.URL.Path))
switch ext {
case ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".txt", ".html", ".json", ".woff", ".woff2", ".ttf", ".eot", ".pdf":
path = r.URL.Path
}
if path == "/" || strings.HasPrefix(path, "/static") {
path = r.URL.Path
}
path = strings.TrimSuffix(path, "/")
// this has to be called in the end so that status code is populated
defer func(res *StatusResponseWriter, req *http.Request) {
// Skip recording for /graphql — it has its own dedicated metrics (app_graphql_*)
if path == "/graphql" {
return
}
duration := time.Since(start)
metrics.RecordHistogram(context.Background(), "app_http_response", duration.Seconds(),
"path", path, "method", req.Method, "status", fmt.Sprintf("%d", res.status))
}(srw, r)
inner.ServeHTTP(srw, r)
})
}
}
================================================
FILE: pkg/gofr/http/middleware/metrics_test.go
================================================
package middleware
import (
"context"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/mock"
)
type mockMetrics struct {
mock.Mock
}
func (m *mockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.Called(ctx, name, labels)
}
func (m *mockMetrics) DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) {
m.Called(ctx, name, value, labels)
}
func (m *mockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.Called(ctx, name, value, labels)
}
func (m *mockMetrics) SetGauge(name string, value float64, _ ...string) {
m.Called(name, value)
}
func TestMetrics(t *testing.T) {
mockMetrics := &mockMetrics{}
mockMetrics.On("RecordHistogram", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Return(nil)
router := mux.NewRouter()
router.HandleFunc("/test", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}).Methods(http.MethodGet).Name("/test")
route := router.NewRoute()
route.Path("/test").Name("/test")
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
router.Use(Metrics(mockMetrics))
router.ServeHTTP(rr, req)
mockMetrics.AssertCalled(t, "RecordHistogram", mock.Anything, "app_http_response", mock.Anything,
[]string{"path", "/test", "method", "GET", "status", "200"})
}
func TestMetrics_StaticFile(t *testing.T) {
mockMetrics := &mockMetrics{}
mockMetrics.On("RecordHistogram", mock.Anything, "app_http_response", mock.Anything,
[]string{"path", "/static/example.js", "method", "GET", "status", "200"}).Return(nil)
// Create a temporary static file for the test
tempDir := t.TempDir()
staticFilePath := tempDir + "/example.js"
err := os.WriteFile(staticFilePath, []byte("console.log('test');"), 0600)
if err != nil {
t.Errorf("failed to create temporary static file: %v", err)
}
router := mux.NewRouter()
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(tempDir)))).Name("/static/")
router.Use(Metrics(mockMetrics))
req := httptest.NewRequest(http.MethodGet, "/static/example.js", http.NoBody)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
mockMetrics.AssertCalled(t, "RecordHistogram", mock.Anything, "app_http_response", mock.Anything,
[]string{"path", "/static/example.js", "method", "GET", "status", "200"})
}
func TestMetrics_StaticFileWithQueryParam(t *testing.T) {
mockMetrics := &mockMetrics{}
mockMetrics.On("RecordHistogram", mock.Anything, "app_http_response", mock.Anything,
[]string{"path", "/static/example.js", "method", "GET", "status", "200"}).Return(nil)
// Create a temporary static file for the test
tempDir := t.TempDir()
staticFilePath := tempDir + "/example.js"
err := os.WriteFile(staticFilePath, []byte("console.log('test');"), 0600)
if err != nil {
t.Errorf("failed to create temporary static file: %v", err)
}
router := mux.NewRouter()
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(tempDir)))).Name("/static/")
router.Use(Metrics(mockMetrics))
req := httptest.NewRequest(http.MethodGet, "/static/example.js?v=42", http.NoBody)
rr := httptest.NewRecorder()
router.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
}
mockMetrics.AssertCalled(t, "RecordHistogram", mock.Anything, "app_http_response", mock.Anything,
[]string{"path", "/static/example.js", "method", "GET", "status", "200"})
}
================================================
FILE: pkg/gofr/http/middleware/oauth.go
================================================
package middleware
import (
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
errEmptyProvider = errors.New("require non-empty provider")
errInvalidInterval = errors.New("invalid interval, require a value greater than 1 second")
errEmptyModulus = errors.New("modulus is empty")
errEmptyPublicExponent = errors.New("public exponent is empty")
errEmptyResponseBody = errors.New("response body is empty")
errInvalidURL = errors.New("invalid URL")
)
const jwtRegexPattern = "^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$"
// PublicKeys stores a map of public keys identified by their key ID (kid).
type PublicKeys struct {
mu sync.RWMutex
keys map[string]*rsa.PublicKey
}
// JWKNotFound is an error type indicating a missing JSON Web Key Set (JWKS).
type JWKNotFound struct {
}
func (JWKNotFound) Error() string {
return "JWKS Not Found"
}
// Get retrieves a public key from the PublicKeys map by its key ID.
func (p *PublicKeys) Get(kid string) *rsa.PublicKey {
p.mu.RLock()
defer p.mu.RUnlock()
key := p.keys[strings.TrimSpace(kid)]
return key
}
type JWKSProvider interface {
GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error)
}
// OauthConfigs holds configuration for OAuth middleware.
type OauthConfigs struct {
Provider JWKSProvider
RefreshInterval time.Duration
Path string
}
// NewOAuth creates a PublicKeyProvider that periodically fetches and updates public keys from a JWKS endpoint.
func NewOAuth(config OauthConfigs) PublicKeyProvider {
var publicKeys PublicKeys
go func() {
publicKeys.updateKeys(config)
ticker := time.NewTicker(config.RefreshInterval)
defer ticker.Stop()
for range ticker.C {
publicKeys.updateKeys(config)
}
}()
return &publicKeys
}
// updateKeys updates keys using PublicKeyProvider.
func (p *PublicKeys) updateKeys(config OauthConfigs) {
jwks, err := getPublicKeys(context.Background(), config.Provider, config.Path)
if err != nil {
return
}
keys := publicKeyFromJWKS(jwks)
if len(keys) == 0 {
return
}
p.mu.Lock()
p.keys = keys
p.mu.Unlock()
}
// getPublicKeys fetches the public keys from JWKSProvider and returns JWKS.
func getPublicKeys(ctx context.Context, provider JWKSProvider, path string) (JWKS, error) {
var keys JWKS
resp, err := provider.GetWithHeaders(ctx, path, nil, nil)
if err != nil || resp == nil {
return keys, err
}
if resp.StatusCode != http.StatusOK {
return keys, errInvalidURL
}
if resp.Body == nil {
return keys, errEmptyResponseBody
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return keys, err
}
resp.Body.Close()
err = json.Unmarshal(body, &keys)
return keys, err
}
// PublicKeyProvider defines an interface for retrieving a public key by its key ID.
type PublicKeyProvider interface {
Get(kid string) *rsa.PublicKey
}
// OAuth is a middleware function that validates JWT access tokens using a provided PublicKeyProvider.
func OAuth(key PublicKeyProvider, options ...jwt.ParserOption) func(http.Handler) http.Handler {
// error being ignored is not the right behavior, this function should be deprecated and use NewOAuthProvider() instead.
function, _ := getPublicKeyFunc(key)
provider := OAuthProvider{
publicKeyFunc: function,
options: append(options, jwt.WithIssuedAt()),
regex: regexp.MustCompile(jwtRegexPattern),
}
return AuthMiddleware(&provider)
}
// JWKS represents a JSON Web Key Set.
type JWKS struct {
Keys []JSONWebKey `json:"keys"`
}
// JSONWebKey represents a JSON Web Key.
type JSONWebKey struct {
ID string `json:"kid"`
Type string `json:"kty"`
Modulus string `json:"n"`
PublicExponent string `json:"e"`
PrivateExponent string `json:"d"`
}
// publicKeyFromJWKS creates a public key from a JWKS and returns it in string .
func publicKeyFromJWKS(jwks JWKS) map[string]*rsa.PublicKey {
if len(jwks.Keys) == 0 {
return nil
}
keys := make(map[string]*rsa.PublicKey)
for _, jwk := range jwks.Keys {
if key, err := jwk.rsaPublicKey(); err == nil {
keys[jwk.ID] = key
}
}
return keys
}
// rsaPublicKey returns the rsa.PublicKey value for JSONWebKey.
func (jwk *JSONWebKey) rsaPublicKey() (*rsa.PublicKey, error) {
if jwk.Modulus == "" {
return nil, errEmptyModulus
}
if jwk.PublicExponent == "" {
return nil, errEmptyPublicExponent
}
n, err := base64.RawURLEncoding.DecodeString(jwk.Modulus)
if err != nil {
return nil, err
}
e, err := base64.RawURLEncoding.DecodeString(jwk.PublicExponent)
if err != nil {
return nil, err
}
nInt := new(big.Int).SetBytes(n)
eInt := new(big.Int).SetBytes(e)
rsaPublicKey := &rsa.PublicKey{
N: nInt,
E: int(eInt.Int64()),
}
return rsaPublicKey, nil
}
type OAuthProvider struct {
publicKeyFunc func(token *jwt.Token) (any, error)
// keyProvider PublicKeyProvider
options []jwt.ParserOption
regex *regexp.Regexp
}
// NewOAuthProvider generates a OAuthProvider for the given OauthConfigs and jwt.ParserOption.
func NewOAuthProvider(config OauthConfigs, options ...jwt.ParserOption) (AuthProvider, error) {
// Validate configuration before proceeding
if config.Provider == nil {
return nil, errEmptyProvider
}
if config.RefreshInterval <= time.Second {
return nil, errInvalidInterval
}
function, err := getPublicKeyFunc(NewOAuth(config))
if err != nil {
return nil, err
}
return &OAuthProvider{
publicKeyFunc: function,
regex: regexp.MustCompile(jwtRegexPattern),
options: append(options, jwt.WithIssuedAt()),
}, nil
}
func (p *OAuthProvider) ExtractAuthHeader(r *http.Request) (any, ErrorHTTP) {
header, err := getAuthHeaderFromRequest(r, headerAuthorization, "Bearer")
if err != nil {
return nil, err
}
if !p.regex.MatchString(header) {
return nil, NewInvalidAuthorizationHeaderFormatError(headerAuthorization, "jwt expected")
}
token, parseErr := jwt.Parse(header, p.publicKeyFunc, p.options...)
if parseErr != nil {
if strings.Contains(parseErr.Error(), "token is malformed") {
return nil, NewInvalidAuthorizationHeaderFormatError(headerAuthorization, "token is malformed")
}
if errors.Is(parseErr, errEmptyProvider) {
return nil, NewInvalidConfigurationError("jwks configuration issue")
}
return nil, NewInvalidAuthorizationHeaderError(headerAuthorization)
}
// Verify if this typecasting is really required, it may be unnecessary
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, NewInvalidAuthorizationHeaderFormatError(headerAuthorization, jwt.ErrTokenInvalidClaims.Error())
}
return claims, nil
}
// GetAuthMethod returns JWTClaim authMethod.
func (*OAuthProvider) GetAuthMethod() AuthMethod {
return JWTClaim
}
// getPublicKeyFunc returns keyFunc to be used in jwt.Parse().
// In case given PublicKeyProvider is nil, nil keyFunc is returned along with errEmptyProvider error.
func getPublicKeyFunc(provider PublicKeyProvider) (func(token *jwt.Token) (any, error), error) {
if provider == nil {
return nil, errEmptyProvider
}
return func(token *jwt.Token) (any, error) {
kid := token.Header["kid"]
jwks := provider.Get(fmt.Sprint(kid))
if jwks == nil {
return nil, JWKNotFound{}
}
return jwks, nil
}, nil
}
================================================
FILE: pkg/gofr/http/middleware/oauth_test.go
================================================
package middleware
import (
"bytes"
"context"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"io"
"math/big"
"net"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/service"
)
func TestOAuthProvider_extractAuthHeader(t *testing.T) {
regex := regexp.MustCompile(jwtRegexPattern)
validHeader := `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.` +
`eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.` +
`KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30`
testCases := []struct {
publicKeyFunc func(token *jwt.Token) (any, error) // public key matching, not matching
options []jwt.ParserOption
header string // missing, malformed, valid
response any
err ErrorHTTP
}{
{
err: ErrorMissingAuthHeader{key: headerAuthorization},
},
{
header: "Bearer some-value",
err: ErrorInvalidAuthorizationHeaderFormat{key: headerAuthorization, errMessage: "jwt expected"},
},
{
header: "Bearer a.b.c",
err: ErrorInvalidAuthorizationHeaderFormat{key: headerAuthorization, errMessage: "token is malformed"},
},
{
publicKeyFunc: notFoundPublicKeyFunc(),
header: validHeader,
err: ErrorInvalidAuthorizationHeader{key: headerAuthorization},
},
{
publicKeyFunc: emptyProviderPublicKeyFunc(),
header: validHeader,
err: ErrorInvalidConfiguration{message: "jwks configuration issue"},
},
{
publicKeyFunc: validPublicKeyFunc(),
header: validHeader,
response: jwt.MapClaims{"admin": true, "iat": 1.516239022e+09, "name": "John Doe", "sub": "1234567890"},
},
{
publicKeyFunc: validPublicKeyFunc(),
header: validHeader,
options: []jwt.ParserOption{jwt.WithExpirationRequired()},
err: ErrorInvalidAuthorizationHeader{key: headerAuthorization},
},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
req.Header.Set(headerAuthorization, tc.header)
provider := &OAuthProvider{
publicKeyFunc: tc.publicKeyFunc,
options: tc.options,
regex: regex,
}
response, err := provider.ExtractAuthHeader(req)
assert.Equal(t, tc.response, response)
assert.Equal(t, tc.err, err)
})
}
}
func Test_NewOAuthProvider(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
testCases := []struct {
jwks JWKSProvider
interval time.Duration
options []jwt.ParserOption
expectedOptions int
err error
}{
{err: errEmptyProvider},
{interval: 10 * time.Second, err: errEmptyProvider},
{jwks: service.NewMockHTTP(ctrl), interval: 0 * time.Second, err: errInvalidInterval},
{jwks: service.NewMockHTTP(ctrl), interval: -2 * time.Second, err: errInvalidInterval},
{jwks: service.NewMockHTTP(ctrl), interval: 10, err: errInvalidInterval},
{jwks: service.NewMockHTTP(ctrl), interval: 10 * time.Second, expectedOptions: 1},
}
for i, testCase := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
config := OauthConfigs{
Provider: testCase.jwks,
RefreshInterval: testCase.interval,
Path: "/.well-known/jwks.json", // Set a default path
}
// Set up mock expectations for successful cases
if testCase.err == nil && testCase.jwks != nil {
mockHTTP := testCase.jwks.(*service.MockHTTP)
mockHTTP.EXPECT().GetWithHeaders(gomock.Any(), "/.well-known/jwks.json", nil, nil).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: http.NoBody,
}, nil).AnyTimes()
}
response, err := NewOAuthProvider(config, testCase.options...)
assert.Equal(t, testCase.err, err)
if testCase.err != nil {
return
}
oAuthProvider, ok := response.(*OAuthProvider)
require.True(t, ok)
assert.NotNil(t, oAuthProvider.publicKeyFunc)
assert.Len(t, oAuthProvider.options, testCase.expectedOptions)
})
}
}
func TestOAuthProvider_getAuthMethod(t *testing.T) {
assert.Equal(t, JWTClaim, (&OAuthProvider{}).GetAuthMethod())
}
func TestJSONWebKey_rsaPublicKey(t *testing.T) {
testCases := []struct {
modulus string
publicExponent string
response *rsa.PublicKey
err error
}{
{err: errEmptyModulus},
{modulus: `AQAB`, err: errEmptyPublicExponent},
{modulus: `jw=`, publicExponent: `lorem-ipsum`, err: base64.CorruptInputError(2)},
{modulus: `AQAB`, publicExponent: `jw====`, err: base64.CorruptInputError(2)},
{modulus: `AQAB`, publicExponent: `AQAB`, response: &rsa.PublicKey{N: big.NewInt(65537), E: 65537}},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
key := &JSONWebKey{Modulus: tc.modulus, PublicExponent: tc.publicExponent}
response, err := key.rsaPublicKey()
assert.Equal(t, tc.response, response)
assert.Equal(t, tc.err, err)
})
}
}
func Test_publicKeyFromJWKS(t *testing.T) {
validJWKS := JWKS{Keys: []JSONWebKey{
{ID: "id-1", Modulus: `AQAB`, PublicExponent: `AQAB`},
{ID: "id-2", Modulus: `AQAB`, PublicExponent: `AQAB`},
}}
emptyJWKS := JWKS{}
partialValidJWKS := JWKS{Keys: []JSONWebKey{
{ID: "id-1", Modulus: `AQAB`, PublicExponent: `AQAB`},
{ID: "id-2", Modulus: `AQAB`, PublicExponent: `AQAB`},
{ID: "id-2", Modulus: ``, PublicExponent: `AQAB`},
}}
response := map[string]*rsa.PublicKey{
"id-1": {N: big.NewInt(65537), E: 65537},
"id-2": {N: big.NewInt(65537), E: 65537},
}
testCases := []struct {
jwks JWKS
response map[string]*rsa.PublicKey
}{
{jwks: emptyJWKS},
{jwks: validJWKS, response: response},
{jwks: partialValidJWKS, response: response},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
result := publicKeyFromJWKS(tc.jwks)
assert.Equal(t, tc.response, result)
})
}
}
func TestJWKNotFound_Error(t *testing.T) {
assert.Equal(t, "JWKS Not Found", JWKNotFound{}.Error())
}
func TestPublicKeys_Get(t *testing.T) {
keySet := map[string]*rsa.PublicKey{
"id-1": {N: nil, E: 0},
"id-2": {N: nil, E: 1},
"id-3": {N: nil, E: 2},
}
testCases := []struct {
keys map[string]*rsa.PublicKey
keyID string
response *rsa.PublicKey
}{
{keys: keySet, keyID: "id-1", response: &rsa.PublicKey{E: 0}},
{keys: keySet, keyID: "id-2", response: &rsa.PublicKey{E: 1}},
{keyID: "id-1"},
{keys: keySet, keyID: "id-0"},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
publicKeys := PublicKeys{keys: tc.keys}
response := publicKeys.Get(tc.keyID)
assert.Equal(t, tc.response, response)
})
}
}
func Test_getPublicKeys(t *testing.T) {
testCases := []struct {
path string
responseLength int
err error
}{
{path: "/empty-body", err: errEmptyResponseBody},
{path: "/dns-error", err: &net.DNSError{}},
{path: "/wrong-path", err: errInvalidURL},
{path: "/.well-known/unparseable-json", err: &json.SyntaxError{}},
{path: "/.well-known/format-error", err: &json.UnmarshalTypeError{}},
{path: "/empty-list"},
{path: "", responseLength: 2},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
response, err := getPublicKeys(t.Context(), MockJWKSProvider{}, tc.path)
assert.Len(t, response.Keys, tc.responseLength)
assert.IsType(t, tc.err, err)
})
}
}
func TestPublicKeys_updateKeys(t *testing.T) {
testCases := []struct {
keys map[string]*rsa.PublicKey
path string
updatedLength int
}{
{keys: nil, path: "/empty-response"},
{keys: nil, path: "/empty-list", updatedLength: 0},
{keys: nil, path: "", updatedLength: 2},
{keys: map[string]*rsa.PublicKey{"11": {}}, path: "/empty-response", updatedLength: 1},
{keys: map[string]*rsa.PublicKey{"11": {}}, path: "/empty-list", updatedLength: 1},
{keys: map[string]*rsa.PublicKey{"11": {}}, path: "", updatedLength: 2},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
config := OauthConfigs{Provider: MockJWKSProvider{}, Path: tc.path}
publicKeys := PublicKeys{keys: tc.keys}
publicKeys.updateKeys(config)
assert.Len(t, publicKeys.keys, tc.updatedLength)
})
}
}
func Test_getPublicKeyFunc(t *testing.T) {
testCases := []struct {
provider PublicKeyProvider
response any
err error
funcErr error
}{
{err: errEmptyProvider},
{provider: validPublicKeyProvider{}, response: &rsa.PublicKey{N: big.NewInt(65537), E: 65537}},
{provider: emptyPublicKeyProvider{}, response: nil, funcErr: JWKNotFound{}},
}
for i, tc := range testCases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
function, err := getPublicKeyFunc(tc.provider)
assert.Equal(t, tc.err, err)
if function == nil {
return
}
// Create a token with a kid header for the emptyPublicKeyProvider test
token := &jwt.Token{}
if i == 2 { // Test case 2 is emptyPublicKeyProvider
token.Header = map[string]any{"kid": "test-key-id"}
}
response, err := function(token)
assert.Equal(t, tc.response, response)
if tc.funcErr != nil {
assert.Equal(t, tc.funcErr, err)
} else {
assert.Equal(t, tc.err, err)
}
})
}
}
func notFoundPublicKeyFunc() func(token *jwt.Token) (any, error) {
return func(_ *jwt.Token) (any, error) {
return nil, JWKNotFound{}
}
}
func validPublicKeyFunc() func(token *jwt.Token) (any, error) {
return func(_ *jwt.Token) (any, error) {
return []byte("a-string-secret-at-least-256-bits-long"), nil
}
}
func emptyProviderPublicKeyFunc() func(token *jwt.Token) (any, error) {
return func(_ *jwt.Token) (any, error) {
return nil, errEmptyProvider
}
}
type validPublicKeyProvider struct {
}
func (validPublicKeyProvider) Get(_ string) *rsa.PublicKey {
return &rsa.PublicKey{N: big.NewInt(65537), E: 65537}
}
type emptyPublicKeyProvider struct{}
func (emptyPublicKeyProvider) Get(_ string) *rsa.PublicKey {
return nil
}
type MockJWKSProvider struct {
}
func (MockJWKSProvider) GetWithHeaders(_ context.Context, path string, _ map[string]any,
_ map[string]string) (*http.Response, error) {
keys := []JSONWebKey{{ID: "111", Type: "RSA", Modulus: "someBase64UrlEncodedModulus", PublicExponent: "AQAB"},
{ID: "212", Type: "RSA", Modulus: "AnotherModulus", PublicExponent: "AQAB"}}
switch path {
case "":
jwks := JWKS{
Keys: keys,
}
jwksJSON, _ := json.Marshal(jwks)
return &http.Response{
Body: io.NopCloser(bytes.NewBuffer(jwksJSON)),
StatusCode: http.StatusOK,
}, nil
case "/empty-list":
jwks := JWKS{}
jwksJSON, _ := json.Marshal(jwks)
return &http.Response{
Body: io.NopCloser(bytes.NewBuffer(jwksJSON)),
StatusCode: http.StatusOK,
}, nil
case "/.well-known/format-error":
jwksJSON, _ := json.Marshal(keys)
return &http.Response{
Body: io.NopCloser(bytes.NewBuffer(jwksJSON)),
StatusCode: http.StatusOK,
}, nil
case "/.well-known/unparseable-json":
return &http.Response{
Body: io.NopCloser(bytes.NewBufferString(`{ "key": "value", invalid }`)),
StatusCode: http.StatusOK,
}, nil
case "/wrong-path":
return &http.Response{StatusCode: http.StatusNotFound}, nil
case "/dns-error":
return nil, &net.DNSError{}
default:
return &http.Response{StatusCode: http.StatusOK}, nil
}
}
================================================
FILE: pkg/gofr/http/middleware/rate_limiter.go
================================================
package middleware
import (
"context"
"errors"
"fmt"
"math"
"net"
"net/http"
"strings"
gofrHttp "gofr.dev/pkg/gofr/http"
)
var (
// errInvalidRequestsPerSecond is returned when RequestsPerSecond is not positive.
errInvalidRequestsPerSecond = errors.New("requestsPerSecond must be positive")
// errInvalidBurst is returned when Burst is not positive.
errInvalidBurst = errors.New("burst must be positive")
)
// RateLimiterConfig holds configuration for rate limiting.
//
// Note: The default implementation uses in-memory token buckets and is suitable
// for single-pod deployments. In multi-pod deployments, each pod will enforce
// limits independently. For distributed rate limiting across multiple pods,
// a Redis-backed store can be implemented in a future update.
//
// Security: When using PerIP=true, only enable TrustedProxies if your application
// is behind a trusted reverse proxy (nginx, ALB, etc.) that sets X-Forwarded-For.
// Without trusted proxies, clients can spoof IP addresses to bypass rate limits.
//
// Cleanup: The rate limiter starts a background goroutine that runs for the
// application lifetime. This is acceptable for long-running servers but consider
// calling Store.StopCleanup() in shutdown handlers if needed.
type RateLimiterConfig struct {
RequestsPerSecond float64
Burst int
PerIP bool
Store RateLimiterStore // Optional: defaults to in-memory store
TrustedProxies bool // If true, trust X-Forwarded-For and X-Real-IP headers
MaxKeys int64 // Maximum unique rate limit keys (0 = default 100000)
}
// Validate checks if the configuration values are valid.
func (c RateLimiterConfig) Validate() error {
if c.RequestsPerSecond <= 0 {
return errInvalidRequestsPerSecond
}
if c.Burst <= 0 {
return errInvalidBurst
}
return nil
}
// getIP extracts the client IP address from the request.
// If trustProxies is false, only RemoteAddr is used to prevent IP spoofing.
func getIP(r *http.Request, trustProxies bool) string {
if !trustProxies {
return getRemoteAddr(r)
}
// Try X-Forwarded-For header first
if ip := getForwardedIP(r); ip != "" {
return ip
}
// Try X-Real-IP header
if ip := getRealIP(r); ip != "" {
return ip
}
// Fall back to RemoteAddr
return getRemoteAddr(r)
}
// getForwardedIP extracts IP from X-Forwarded-For header.
func getForwardedIP(r *http.Request) string {
forwarded := r.Header.Get("X-Forwarded-For")
if forwarded == "" {
return ""
}
// X-Forwarded-For can contain multiple IPs, take the first one
ips := strings.Split(forwarded, ",")
if len(ips) == 0 {
return ""
}
return strings.TrimSpace(ips[0])
}
// getRealIP extracts IP from X-Real-IP header.
func getRealIP(r *http.Request) string {
realIP := r.Header.Get("X-Real-IP")
return strings.TrimSpace(realIP)
}
// getRemoteAddr extracts IP from RemoteAddr.
func getRemoteAddr(r *http.Request) string {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return ip
}
// RateLimiter creates a middleware that limits requests based on the configuration.
func RateLimiter(config RateLimiterConfig, m metrics) func(http.Handler) http.Handler {
// Validate configuration
if err := config.Validate(); err != nil {
panic(fmt.Sprintf("invalid rate limiter config: %v", err))
}
// Use in-memory store if none provided
if config.Store == nil {
config.Store = NewMemoryRateLimiterStore(config)
}
// Start cleanup routine with context.Background().
// The cleanup goroutine runs for the application lifetime.
// For graceful shutdown, call config.Store.StopCleanup() in your shutdown handler.
ctx := context.Background()
config.Store.StartCleanup(ctx)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Skip rate limiting for health check endpoints
if isWellKnown(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
// Determine the rate limit key (IP or global)
key := "global"
if config.PerIP {
key = getIP(r, config.TrustedProxies)
// Fallback to "unknown" if getIP returns empty string
// This prevents all requests from sharing the same bucket
if key == "" {
key = "unknown"
}
}
// Check rate limit
allowed, retryAfter, err := config.Store.Allow(r.Context(), key, config)
if err != nil {
// Fail open on errors
next.ServeHTTP(w, r)
return
}
if !allowed {
// Set Retry-After header (RFC 6585)
// Use math.Ceil to ensure at least 1 second for sub-second delays
w.Header().Set("Retry-After", fmt.Sprintf("%.0f", math.Ceil(retryAfter.Seconds())))
// Increment rate limit exceeded metric
if m != nil {
m.IncrementCounter(r.Context(), "app_http_rate_limit_exceeded_total",
"path", r.URL.Path, "method", r.Method)
}
// Return 429 Too Many Requests
responder := gofrHttp.NewResponder(w, r.Method)
responder.Respond(nil, gofrHttp.ErrorTooManyRequests{})
return
}
next.ServeHTTP(w, r)
})
}
}
================================================
FILE: pkg/gofr/http/middleware/rate_limiter_store.go
================================================
package middleware
import (
"context"
"sync"
"sync/atomic"
"time"
"golang.org/x/time/rate"
)
// RateLimiterStore abstracts the storage and cleanup for rate limiter buckets.
// This interface matches the one defined in pkg/gofr/service for consistency.
//
// Note: The config parameter in Allow() is provided for interface compatibility.
// Implementations may use a stored configuration and ignore this parameter.
type RateLimiterStore interface {
Allow(ctx context.Context, key string, config RateLimiterConfig) (allowed bool, retryAfter time.Duration, err error)
StartCleanup(ctx context.Context)
StopCleanup()
}
// memoryRateLimiterStore implements RateLimiterStore using in-memory token buckets.
type memoryRateLimiterStore struct {
limiters sync.Map // map[string]*limiterEntry
keyCount int64 // atomic counter for tracking number of keys
maxKeys int64 // maximum allowed keys (0 = unlimited)
stopCh chan struct{}
cleanupOnce sync.Once
stopOnce sync.Once
config RateLimiterConfig // Store config for consistency
}
type limiterEntry struct {
limiter *rate.Limiter
lastAccess int64 // Unix timestamp for cleanup
}
// Default limits for delay calculation bounds checking.
const (
minDelay = time.Millisecond
maxDelay = time.Minute
defaultMaxKeys = 100000
)
// NewMemoryRateLimiterStore creates a new in-memory rate limiter store.
// The config is stored to ensure consistent rate limiting for all keys.
func NewMemoryRateLimiterStore(config RateLimiterConfig) RateLimiterStore {
maxKeys := config.MaxKeys
if maxKeys == 0 {
maxKeys = defaultMaxKeys // Default max keys to prevent memory exhaustion
}
return &memoryRateLimiterStore{
config: config,
maxKeys: maxKeys,
}
}
// Allow checks if a request should be allowed based on the rate limit.
func (m *memoryRateLimiterStore) Allow(_ context.Context, key string, _ RateLimiterConfig) (bool, time.Duration, error) {
now := time.Now().Unix()
// Use stored config for consistency across all keys
cfg := m.config
// Get or create limiter for this key
// Check loaded flag to avoid unnecessary object creation when entry already exists
val, loaded := m.limiters.LoadOrStore(key, &limiterEntry{
limiter: rate.NewLimiter(rate.Limit(cfg.RequestsPerSecond), cfg.Burst),
lastAccess: now,
})
entry := val.(*limiterEntry)
// If entry was loaded (already existed), update lastAccess atomically
// If it was stored (new entry), lastAccess is already set correctly
if loaded {
atomic.StoreInt64(&entry.lastAccess, now)
} else {
// Track number of keys to prevent memory exhaustion
newCount := atomic.AddInt64(&m.keyCount, 1)
if m.maxKeys > 0 && newCount > m.maxKeys {
// Exceeded max keys - remove the entry we just added and fail open
m.limiters.Delete(key)
atomic.AddInt64(&m.keyCount, -1)
// Fail open to prevent service denial
return true, 0, nil
}
}
// Use only Reserve() instead of Allow() + Reserve() to avoid race conditions
// Reserve() atomically checks and reserves a token, giving accurate delay information
reservation := entry.limiter.Reserve()
if !reservation.OK() {
// Should not happen with valid config, but handle gracefully
// Use bounds-checked delay calculation
return false, m.calculateSafeDelay(cfg.RequestsPerSecond), nil
}
delay := reservation.Delay()
if delay > 0 {
// Request would need to wait - cancel reservation and return the delay
reservation.Cancel()
return false, delay, nil
}
// Request is allowed immediately (delay == 0)
return true, 0, nil
}
// calculateSafeDelay calculates delay with bounds checking to prevent overflow or zero values.
// Ensures delay is always within reasonable bounds.
func (*memoryRateLimiterStore) calculateSafeDelay(requestsPerSecond float64) time.Duration {
if requestsPerSecond <= 0 {
return maxDelay
}
delay := time.Duration(float64(time.Second) / requestsPerSecond)
if delay < minDelay {
return minDelay
}
if delay > maxDelay {
return maxDelay
}
return delay
}
// StartCleanup starts a background goroutine to clean up stale limiters.
// This method is safe to call multiple times - only one cleanup goroutine will be started.
func (m *memoryRateLimiterStore) StartCleanup(ctx context.Context) {
m.cleanupOnce.Do(func() {
m.stopCh = make(chan struct{})
go func() {
const cleanupInterval = 5 * time.Minute
const staleThreshold = 10 * time.Minute
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.cleanup(staleThreshold)
case <-m.stopCh:
return
case <-ctx.Done():
return
}
}
}()
})
}
// StopCleanup stops the cleanup goroutine.
// This method is safe to call multiple times.
func (m *memoryRateLimiterStore) StopCleanup() {
m.stopOnce.Do(func() {
if m.stopCh != nil {
close(m.stopCh)
}
})
}
// cleanup removes stale limiters that haven't been accessed recently.
func (m *memoryRateLimiterStore) cleanup(staleThreshold time.Duration) {
cutoff := time.Now().Unix() - int64(staleThreshold.Seconds())
m.limiters.Range(func(key, value any) bool {
entry := value.(*limiterEntry)
if atomic.LoadInt64(&entry.lastAccess) < cutoff {
m.limiters.Delete(key)
// Decrement key count when removing stale entries
atomic.AddInt64(&m.keyCount, -1)
}
return true
})
}
================================================
FILE: pkg/gofr/http/middleware/rate_limiter_test.go
================================================
package middleware
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type rateLimiterMockMetrics struct {
mu sync.Mutex
counters map[string]int
}
func newRateLimiterMockMetrics() *rateLimiterMockMetrics {
return &rateLimiterMockMetrics{
counters: make(map[string]int),
}
}
func (m *rateLimiterMockMetrics) IncrementCounter(_ context.Context, name string, _ ...string) {
m.mu.Lock()
defer m.mu.Unlock()
m.counters[name]++
}
func (*rateLimiterMockMetrics) DeltaUpDownCounter(_ context.Context, _ string, _ float64, _ ...string) {
// Not used in rate limiter tests
}
func (*rateLimiterMockMetrics) RecordHistogram(_ context.Context, _ string, _ float64, _ ...string) {
// Not used in rate limiter tests
}
func (*rateLimiterMockMetrics) SetGauge(_ string, _ float64, _ ...string) {
// Not used in rate limiter tests
}
func (m *rateLimiterMockMetrics) GetCounter(name string) int {
m.mu.Lock()
defer m.mu.Unlock()
return m.counters[name]
}
func TestRateLimiter_GlobalLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: false,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
}))
// First 2 requests should succeed (burst)
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1)
}
// 3rd request should be rate limited
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Request should be rate limited")
// Verify metric was incremented
assert.Equal(t, 1, metrics.GetCounter("app_http_rate_limit_exceeded_total"))
}
func TestRateLimiter_PerIPLimit(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// IP1: First 2 requests should succeed
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "192.168.1.1:12345"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
// IP1: 3rd request should be rate limited
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "192.168.1.1:12345"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code)
// IP2: Should still be able to make requests (different limiter)
req = httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "192.168.1.2:54321"
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestRateLimiter_SkipHealthEndpoints(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 1,
Burst: 1,
PerIP: false,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Health endpoints should not be rate limited
healthPaths := []string{"/.well-known/health", "/.well-known/alive"}
for _, path := range healthPaths {
for i := 0; i < 5; i++ {
req := httptest.NewRequest(http.MethodGet, path, http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "Health endpoint %s should not be rate limited", path)
}
}
}
func TestRateLimiter_ConcurrentRequests(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 10,
PerIP: true,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
var wg sync.WaitGroup
successCount := 0
rateLimitedCount := 0
var mu sync.Mutex
// Send 20 concurrent requests from same IP
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "192.168.1.1:12345"
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
mu.Lock()
if rr.Code == http.StatusOK {
successCount++
} else if rr.Code == http.StatusTooManyRequests {
rateLimitedCount++
}
mu.Unlock()
}()
}
wg.Wait()
// Due to timing/race conditions in concurrent tests, we allow a small tolerance
// The important thing is that rate limiting occurred
assert.GreaterOrEqual(t, successCount, 9, "Should allow approximately burst size requests")
assert.LessOrEqual(t, successCount, 11, "Should not allow significantly more than burst size")
assert.Positive(t, rateLimitedCount, "Should have some rate limited requests")
assert.Equal(t, 20, successCount+rateLimitedCount, "Total requests should be 20")
}
func TestRateLimiter_TokenRefill(t *testing.T) {
if testing.Short() {
t.Skip("Skipping time-based test in short mode")
}
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 5, // 5 requests per second
Burst: 2,
PerIP: false,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Use up burst
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
}
// Next request should be rate limited
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code)
// Wait for token refill (200ms = 1 token at 5 req/sec)
time.Sleep(220 * time.Millisecond)
// Should succeed now
req = httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestGetIP_XForwardedFor(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.Header.Set("X-Forwarded-For", "203.0.113.1, 198.51.100.1")
req.RemoteAddr = "192.168.1.1:12345"
ip := getIP(req, true)
assert.Equal(t, "203.0.113.1", ip, "Should extract first IP from X-Forwarded-For when trusting proxies")
// Without trusting proxies, should use RemoteAddr
ip = getIP(req, false)
assert.Equal(t, "192.168.1.1", ip, "Should use RemoteAddr when not trusting proxies")
}
func TestGetIP_XRealIP(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.Header.Set("X-Real-IP", "203.0.113.5")
req.RemoteAddr = "192.168.1.1:12345"
ip := getIP(req, true)
assert.Equal(t, "203.0.113.5", ip, "Should extract IP from X-Real-IP when trusting proxies")
// Without trusting proxies, should use RemoteAddr
ip = getIP(req, false)
assert.Equal(t, "192.168.1.1", ip, "Should use RemoteAddr when not trusting proxies")
}
func TestGetIP_RemoteAddr(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "192.168.1.1:12345"
ip := getIP(req, false)
assert.Equal(t, "192.168.1.1", ip, "Should extract IP from RemoteAddr")
}
func TestGetIP_Priority(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.Header.Set("X-Forwarded-For", "203.0.113.1")
req.Header.Set("X-Real-IP", "203.0.113.2")
req.RemoteAddr = "192.168.1.1:12345"
ip := getIP(req, true)
assert.Equal(t, "203.0.113.1", ip, "X-Forwarded-For should have highest priority when trusting proxies")
}
func TestRateLimiter_RetryAfterHeader(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 1,
PerIP: false,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// First request should succeed
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
// Second request should be rate limited and include Retry-After header
req = httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code)
assert.NotEmpty(t, rr.Header().Get("Retry-After"), "Retry-After header should be set")
}
func TestRateLimiterConfig_Validate(t *testing.T) {
tests := []struct {
name string
config RateLimiterConfig
wantErr bool
}{
{
name: "valid config",
config: RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 20,
PerIP: true,
},
wantErr: false,
},
{
name: "zero RequestsPerSecond",
config: RateLimiterConfig{
RequestsPerSecond: 0,
Burst: 20,
PerIP: true,
},
wantErr: true,
},
{
name: "negative RequestsPerSecond",
config: RateLimiterConfig{
RequestsPerSecond: -5,
Burst: 20,
PerIP: true,
},
wantErr: true,
},
{
name: "zero Burst",
config: RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 0,
PerIP: true,
},
wantErr: true,
},
{
name: "negative Burst",
config: RateLimiterConfig{
RequestsPerSecond: 10,
Burst: -5,
PerIP: true,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestMemoryRateLimiterStore_StopCleanupMultipleCalls(t *testing.T) {
t.Helper()
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 20,
PerIP: true,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
ctx := context.Background()
// Start cleanup
store.StartCleanup(ctx)
// Stop multiple times - should not panic
store.StopCleanup()
store.StopCleanup()
store.StopCleanup()
// Test passes if no panic occurs
}
func TestMemoryRateLimiterStore_Cleanup(t *testing.T) {
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 20,
PerIP: true,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
ctx := context.Background()
// Add some entries
allowed1, _, _ := store.Allow(ctx, "ip1", config)
allowed2, _, _ := store.Allow(ctx, "ip2", config)
allowed3, _, _ := store.Allow(ctx, "ip3", config)
assert.True(t, allowed1 && allowed2 && allowed3, "All initial requests should be allowed")
// Verify entries exist
count := 0
store.limiters.Range(func(_, _ any) bool {
count++
return true
})
assert.Equal(t, 3, count, "Should have 3 entries")
// Manually trigger cleanup with a threshold that marks all as stale
// Set lastAccess to past time
store.limiters.Range(func(_ any, value any) bool {
entry := value.(*limiterEntry)
atomic.StoreInt64(&entry.lastAccess, time.Now().Unix()-3600) // 1 hour ago
return true
})
// Run cleanup with 10 minute threshold
store.cleanup(10 * time.Minute)
// Verify stale entries were removed
count = 0
store.limiters.Range(func(_, _ any) bool {
count++
return true
})
assert.Equal(t, 0, count, "Stale entries should be removed")
}
func TestRateLimiter_TrustedProxiesEnabled(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: true, // Trust proxy headers
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Send 2 requests from same X-Forwarded-For IP
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "127.0.0.1:12345" // Proxy IP
req.Header.Set("X-Forwarded-For", "203.0.113.1") // Client IP
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
// 3rd request from same X-Forwarded-For IP should be rate limited
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "127.0.0.1:12345"
req.Header.Set("X-Forwarded-For", "203.0.113.1")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Should rate limit based on X-Forwarded-For IP")
// Different X-Forwarded-For IP should have separate limit
req = httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "127.0.0.1:12345" // Same proxy
req.Header.Set("X-Forwarded-For", "203.0.113.2") // Different client IP
rr = httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "Different client IP should have separate rate limit")
}
func TestRateLimiter_TrustedProxiesDisabled(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
TrustedProxies: false, // Do not trust proxy headers
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Send 2 requests with same RemoteAddr but different X-Forwarded-For
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "127.0.0.1:12345"
req.Header.Set("X-Forwarded-For", fmt.Sprintf("203.0.113.%d", i+1)) // Different spoofed IPs
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
}
// 3rd request should be rate limited based on RemoteAddr, ignoring X-Forwarded-For
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = "127.0.0.1:12345"
req.Header.Set("X-Forwarded-For", "203.0.113.99") // Different spoofed IP
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Should rate limit based on RemoteAddr, ignoring spoofed headers")
}
// TestGetIP_EmptyFallback tests that empty IP should fallback to "unknown".
func TestGetIP_EmptyFallback(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
// Set RemoteAddr to empty (malformed)
req.RemoteAddr = ""
ip := getIP(req, false)
// Even with empty RemoteAddr, getRemoteAddr returns it as-is
// The fix is in the middleware layer, not in getIP itself
// This test documents the current behavior
assert.Empty(t, ip, "getIP returns empty string for empty RemoteAddr")
}
// TestRateLimiter_EmptyIPFallback tests that rate limiter uses "unknown" key for empty IP.
func TestRateLimiter_EmptyIPFallback(t *testing.T) {
metrics := newRateLimiterMockMetrics()
config := RateLimiterConfig{
RequestsPerSecond: 2,
Burst: 2,
PerIP: true,
}
handler := RateLimiter(config, metrics)(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Send requests with empty RemoteAddr - should be grouped under "unknown"
for i := 0; i < 2; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = ""
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code, "Request %d should succeed", i+1)
}
// 3rd request with empty RemoteAddr should be rate limited under "unknown" key
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
req.RemoteAddr = ""
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusTooManyRequests, rr.Code, "Empty RemoteAddr should use 'unknown' key and be rate limited")
}
// TestMemoryRateLimiterStore_CalculateSafeDelay tests delay bounds checking.
func TestMemoryRateLimiterStore_CalculateSafeDelay(t *testing.T) {
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
tests := []struct {
name string
requestsPerSecond float64
expectedMinDelay time.Duration
expectedMaxDelay time.Duration
}{
{
name: "normal rate",
requestsPerSecond: 10,
expectedMinDelay: time.Millisecond,
expectedMaxDelay: time.Second,
},
{
name: "zero rate returns max delay",
requestsPerSecond: 0,
expectedMinDelay: time.Minute,
expectedMaxDelay: time.Minute,
},
{
name: "negative rate returns max delay",
requestsPerSecond: -5,
expectedMinDelay: time.Minute,
expectedMaxDelay: time.Minute,
},
{
name: "very high rate returns min delay",
requestsPerSecond: 1e15,
expectedMinDelay: time.Millisecond,
expectedMaxDelay: time.Millisecond,
},
{
name: "very low rate is capped at max delay",
requestsPerSecond: 0.0001,
expectedMinDelay: time.Minute,
expectedMaxDelay: time.Minute,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
delay := store.calculateSafeDelay(tt.requestsPerSecond)
assert.GreaterOrEqual(t, delay, tt.expectedMinDelay, "Delay should be >= expected min")
assert.LessOrEqual(t, delay, tt.expectedMaxDelay, "Delay should be <= expected max")
})
}
}
// TestMemoryRateLimiterStore_MaxKeysLimit tests maximum keys limit.
func TestMemoryRateLimiterStore_MaxKeysLimit(t *testing.T) {
maxKeys := int64(5)
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
MaxKeys: maxKeys,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
ctx := context.Background()
// Add entries up to the limit
for i := int64(0); i < maxKeys; i++ {
key := fmt.Sprintf("ip-%d", i)
allowed, _, err := store.Allow(ctx, key, config)
require.NoError(t, err)
assert.True(t, allowed, "Request for key %s should be allowed", key)
}
// Verify we're at capacity
assert.Equal(t, maxKeys, atomic.LoadInt64(&store.keyCount), "Should have max keys")
// Additional new key should fail open (allow request, don't create limiter)
allowed, _, err := store.Allow(ctx, "overflow-key", config)
require.NoError(t, err)
assert.True(t, allowed, "Overflow request should fail open (be allowed)")
// Verify key count hasn't increased
assert.Equal(t, maxKeys, atomic.LoadInt64(&store.keyCount), "Key count should not exceed max")
}
// TestMemoryRateLimiterStore_ConcurrentLoadOrStore tests concurrent key creation.
func TestMemoryRateLimiterStore_ConcurrentLoadOrStore(t *testing.T) {
config := RateLimiterConfig{
RequestsPerSecond: 100,
Burst: 10,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
ctx := context.Background()
var wg sync.WaitGroup
const (
numGoroutines = 100
key = "concurrent-test-key"
)
successCount := int64(0)
// Launch many goroutines trying to access the same key concurrently
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
allowed, _, err := store.Allow(ctx, key, config)
if err == nil && allowed {
atomic.AddInt64(&successCount, 1)
}
}()
}
wg.Wait()
// Verify only one entry exists for this key
entryCount := 0
store.limiters.Range(func(k, _ any) bool {
if k.(string) == key {
entryCount++
}
return true
})
assert.Equal(t, 1, entryCount, "Should have exactly one entry for the key")
assert.Equal(t, int64(1), atomic.LoadInt64(&store.keyCount), "Key count should be 1")
// Some requests should succeed (burst allows up to 10), others should be rate limited
assert.GreaterOrEqual(t, successCount, int64(1), "At least some requests should succeed")
assert.LessOrEqual(t, successCount, int64(10)+1, "Not more than burst+1 should succeed due to timing")
}
// TestMemoryRateLimiterStore_CleanupDecrementsKeyCount tests that cleanup decrements key count.
func TestMemoryRateLimiterStore_CleanupDecrementsKeyCount(t *testing.T) {
config := RateLimiterConfig{
RequestsPerSecond: 10,
Burst: 5,
}
store := NewMemoryRateLimiterStore(config).(*memoryRateLimiterStore)
ctx := context.Background()
// Add some entries
for i := 0; i < 3; i++ {
_, _, _ = store.Allow(ctx, fmt.Sprintf("key-%d", i), config)
}
assert.Equal(t, int64(3), atomic.LoadInt64(&store.keyCount), "Should have 3 keys")
// Make all entries stale
store.limiters.Range(func(_ any, value any) bool {
entry := value.(*limiterEntry)
atomic.StoreInt64(&entry.lastAccess, time.Now().Unix()-3600) // 1 hour ago
return true
})
// Run cleanup
store.cleanup(10 * time.Minute)
// Verify key count is decremented
assert.Equal(t, int64(0), atomic.LoadInt64(&store.keyCount), "Key count should be 0 after cleanup")
}
================================================
FILE: pkg/gofr/http/middleware/tracer.go
================================================
package middleware
import (
"fmt"
"net/http"
"strings"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"gofr.dev/pkg/gofr/version"
)
// Tracer is a middleware that starts a new OpenTelemetry trace span for each request.
func Tracer(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Start context and Tracing
ctx := r.Context()
// extract the traceID and spanID from the headers and create a new context for the same
// this context will make a new span using the traceID and link the incoming SpanID as
// its parentID, thus connecting two spans
ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))
tr := otel.GetTracerProvider().Tracer("gofr-" + version.Framework)
ctx, span := tr.Start(ctx, fmt.Sprintf("%s %s", strings.ToUpper(r.Method), r.URL.Path))
defer span.End()
inner.ServeHTTP(w, r.WithContext(ctx))
})
}
================================================
FILE: pkg/gofr/http/middleware/tracer_test.go
================================================
package middleware
import (
"net/http"
"net/http/httptest"
"testing"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/sdk/trace"
otelTrace "go.opentelemetry.io/otel/trace"
)
type MockHandlerForTracing struct{}
// ServeHTTP is used for testing if the request context has traceId.
func (*MockHandlerForTracing) ServeHTTP(w http.ResponseWriter, req *http.Request) {
traceID := otelTrace.SpanFromContext(req.Context()).SpanContext().TraceID().String()
_, _ = w.Write([]byte(traceID))
}
func TestTrace(_ *testing.T) {
tp := trace.NewTracerProvider()
otel.SetTracerProvider(tp)
handler := Tracer(&MockHandlerForTracing{})
req := httptest.NewRequest(http.MethodGet, "/dummy", http.NoBody)
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
}
================================================
FILE: pkg/gofr/http/middleware/validate.go
================================================
package middleware
import "strings"
func isWellKnown(path string) bool {
return strings.HasPrefix(path, "/.well-known")
}
================================================
FILE: pkg/gofr/http/middleware/validate_test.go
================================================
package middleware
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_isWellKnown(t *testing.T) {
tests := []struct {
desc string
endpoint string
resp bool
}{
{"empty endpoint", "", false},
{"sample endpoint", "/sample", false},
{"health-check endpoint", "/.well-known/health-check", true},
{"alive endpoint", "/.well-known/alive", true},
}
for i, tc := range tests {
resp := isWellKnown(tc.endpoint)
assert.Equal(t, tc.resp, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/http/middleware/web_socket.go
================================================
package middleware
import (
"context"
"net/http"
gorillaWebsocket "github.com/gorilla/websocket"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/websocket"
)
// WSHandlerUpgrade middleware upgrades the incoming http request to a websocket connection using websocket upgrader.
func WSHandlerUpgrade(c *container.Container, wsManager *websocket.Manager) func(inner http.Handler) http.Handler {
return func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if gorillaWebsocket.IsWebSocketUpgrade(r) {
conn, err := wsManager.WebSocketUpgrader.Upgrade(w, r, nil)
if err != nil {
c.Errorf("Failed to upgrade to WebSocket: %v", err)
http.Error(w, "Could not open WebSocket connection", http.StatusBadRequest)
return
}
// Add the connection to the hub
wsManager.AddWebsocketConnection(r.Header.Get("Sec-WebSocket-Key"), &websocket.Connection{Conn: conn})
// Store the websocket connection key in the context
ctx := context.WithValue(r.Context(), websocket.WSConnectionKey, r.Header.Get("Sec-WebSocket-Key"))
r = r.WithContext(ctx)
}
inner.ServeHTTP(w, r)
})
}
}
================================================
FILE: pkg/gofr/http/middleware/web_socket_test.go
================================================
package middleware
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
gofrWebSocket "gofr.dev/pkg/gofr/websocket"
)
var errConnection = errors.New("can't create connection")
func initializeWebSocketMocks(t *testing.T) (gofrWebSocket.MockUpgrader, *gofrWebSocket.Manager) {
t.Helper()
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
mockUpgrader := gofrWebSocket.NewMockUpgrader(mockCtrl)
wsManager := gofrWebSocket.New()
wsManager.WebSocketUpgrader = &gofrWebSocket.WSUpgrader{Upgrader: mockUpgrader}
return *mockUpgrader, wsManager
}
func TestWSConnectionCreate_Error(t *testing.T) {
mockUpgrader, wsManager := initializeWebSocketMocks(t)
mockContainer, _ := container.NewMockContainer(t)
mockUpgrader.EXPECT().Upgrade(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil,
errConnection).Times(1)
handler := WSHandlerUpgrade(mockContainer, wsManager)(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
}))
// Create a test request with incomplete upgrade header
req := httptest.NewRequest(http.MethodGet, "/ws", http.NoBody)
req.Header.Set("Connection", "upgrade")
req.Header.Set("Upgrade", "websocket")
// Serve the request through the middleware
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, req)
// No response expected, status code should be 400 (Bad Request)
if status := recorder.Code; status != http.StatusBadRequest {
t.Errorf("Unexpected status code: %d", status)
}
}
func Test_WSConnectionCreate_Success(t *testing.T) {
mockUpgrader, wsManager := initializeWebSocketMocks(t)
mockContainer, _ := container.NewMockContainer(t)
mockConn := &gofrWebSocket.Connection{
Conn: &websocket.Conn{},
}
mockUpgrader.EXPECT().Upgrade(gomock.Any(), gomock.Any(), gomock.Any()).Return(mockConn.Conn, nil).Times(1)
middleware := WSHandlerUpgrade(mockContainer, wsManager)
innerHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest(http.MethodGet, "/ws", http.NoBody)
req.Header.Set("Connection", "Upgrade")
req.Header.Set("Upgrade", "websocket")
rec := httptest.NewRecorder()
handler := middleware(innerHandler)
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
}
================================================
FILE: pkg/gofr/http/multipart_file_bind.go
================================================
package http
import (
"errors"
"io"
"mime/multipart"
"reflect"
"strconv"
"gofr.dev/pkg/gofr/file"
)
var (
errUnsupportedInterfaceType = errors.New("unsupported interface value type")
errDataLengthExceeded = errors.New("data length exceeds array capacity")
errUnsupportedKind = errors.New("unsupported kind")
errSettingValueFailure = errors.New("error setting value at index")
errNotAStruct = errors.New("provided value is not a struct")
errUnexportedField = errors.New("cannot set field; it might be unexported")
errUnsupportedFieldType = errors.New("unsupported type for field")
errFieldsNotSet = errors.New("no fields were set")
)
type formData struct {
fields map[string][]string
files map[string][]*multipart.FileHeader
}
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)
}
if vKind != reflect.Struct || !field.Anonymous {
set, err := uf.trySet(val, field)
if err != nil {
return false, err
}
if set {
return true, nil
}
}
if vKind == reflect.Struct {
return uf.checkStruct(val)
}
return false, nil
}
func (uf *formData) checkPointer(val reflect.Value, field *reflect.StructField) (bool, error) {
var (
// isNew is a flag if the value of pointer is nil
isNew bool
vPtr = val
)
if val.IsNil() {
isNew = true
// if the pointer is nil, assign an empty value
vPtr = reflect.New(val.Type().Elem())
}
// try to set value with the underlying data type for the pointer
ok, err := uf.mapStruct(vPtr.Elem(), field)
if err != nil {
return false, err
}
if isNew && ok {
val.Set(vPtr)
}
return ok, nil
}
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()
for i := 0; i < val.NumField(); i++ {
sf := tVal.Field(i)
if sf.PkgPath != "" && !sf.Anonymous {
continue
}
ok, err := uf.mapStruct(val.Field(i), &sf)
if err != nil {
return false, err
}
set = set || ok
}
return set, nil
}
func (uf *formData) trySet(value reflect.Value, field *reflect.StructField) (bool, error) {
tag, ok := getFieldName(field)
if !ok {
return false, nil
}
if header, ok := uf.files[tag]; ok {
return uf.setFile(value, header)
}
if values, ok := uf.fields[tag]; ok {
return uf.setFieldValue(value, values[0])
}
return false, nil
}
func (*formData) setFile(value reflect.Value, header []*multipart.FileHeader) (bool, error) {
f, err := header[0].Open()
if err != nil {
return false, err
}
content, err := io.ReadAll(f)
if err != nil {
return false, err
}
switch value.Interface().(type) {
case file.Zip:
zip, err := file.NewZip(content)
if err != nil {
return false, err
}
value.Set(reflect.ValueOf(*zip))
case multipart.FileHeader:
value.Set(reflect.ValueOf(*header[0]))
default:
return false, nil
}
return true, nil
}
func (uf *formData) setFieldValue(value reflect.Value, data string) (bool, error) {
value = dereferencePointerType(value)
kind := value.Kind()
switch kind {
case reflect.String:
return uf.setStringValue(value, data)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return uf.setIntValue(value, data)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uf.setUintValue(value, data)
case reflect.Float32, reflect.Float64:
return uf.setFloatValue(value, data)
case reflect.Bool:
return uf.setBoolValue(value, data)
case reflect.Slice, reflect.Array:
return uf.setSliceOrArrayValue(value, data)
case reflect.Interface:
return uf.setInterfaceValue(value, data)
case reflect.Struct:
return uf.setStructValue(value, data)
case reflect.Invalid, reflect.Complex64, reflect.Complex128, reflect.Chan, reflect.Func,
reflect.Map, reflect.Pointer, reflect.UnsafePointer:
return false, nil
}
return false, nil
}
func dereferencePointerType(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
if value.IsNil() {
// Initialize the pointer to a new value if it's nil
value.Set(reflect.New(value.Type().Elem()))
}
value = value.Elem() // Dereference the pointer
}
return value
}
func (*formData) setStringValue(value reflect.Value, data string) (bool, error) {
value.SetString(data)
return true, nil
}
func (*formData) setIntValue(value reflect.Value, data string) (bool, error) {
i, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return false, err
}
value.SetInt(i)
return true, nil
}
func (*formData) setUintValue(value reflect.Value, data string) (bool, error) {
ui, err := strconv.ParseUint(data, 10, 64)
if err != nil {
return false, err
}
value.SetUint(ui)
return true, nil
}
func (*formData) setFloatValue(value reflect.Value, data string) (bool, error) {
f, err := strconv.ParseFloat(data, 64)
if err != nil {
return false, err
}
value.SetFloat(f)
return true, nil
}
func (*formData) setBoolValue(value reflect.Value, data string) (bool, error) {
boolVal, err := strconv.ParseBool(data)
if err != nil {
return false, err
}
value.SetBool(boolVal)
return true, nil
}
func getFieldName(field *reflect.StructField) (string, bool) {
var (
formTag = "form"
fileTag = "file"
key string
)
if field.Tag.Get(formTag) != "" && field.IsExported() {
key = field.Tag.Get(formTag)
} else if field.Tag.Get(fileTag) != "" && field.IsExported() {
key = field.Tag.Get(fileTag)
} else if field.IsExported() {
key = field.Name
} else {
return "", false
}
if key == "" || key == "-" {
return "", false
}
return key, true
}
================================================
FILE: pkg/gofr/http/multipart_file_bind_test.go
================================================
package http
import (
"errors"
"fmt"
"reflect"
"testing"
"unsafe"
"github.com/stretchr/testify/require"
)
var (
errUnsupportedType = errors.New("unsupported type for field: expected float64 but got bool")
errJSON = errors.New("unexpected end of JSON input")
)
func TestGetFieldName(t *testing.T) {
tests := []struct {
desc string
field *reflect.StructField
key string
wantOk bool
}{
{
desc: "Field with form tag",
field: &reflect.StructField{Tag: reflect.StructTag("form:\"name\"")},
key: "name",
wantOk: true,
},
{
desc: "Field with file tag",
field: &reflect.StructField{Tag: reflect.StructTag("file:\"avatar\"")},
key: "avatar",
wantOk: true,
},
{
desc: "Field with exported name",
field: &reflect.StructField{Name: "ID"},
key: "ID",
wantOk: true,
},
{
desc: "Unexported field with tag",
field: &reflect.StructField{Name: "unexported", Tag: reflect.StructTag("form:\"data\""), PkgPath: "unexported"},
key: "",
wantOk: false,
},
{
desc: "Field with omitted tag",
field: &reflect.StructField{},
key: "",
wantOk: false,
},
}
for i, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
result, gotOk := getFieldName(tt.field)
require.Equal(t, tt.key, result, "TestGetFieldName[%d] : %v Failed!", i, tt.desc)
require.Equal(t, tt.wantOk, gotOk, "TestGetFieldName[%d] : %v Failed!", i, tt.desc)
})
}
}
type testValue struct {
kind reflect.Kind
value any
}
func Test_SetFieldValue_Success(t *testing.T) {
testCases := []struct {
desc string
data string
expected bool
valueType testValue
}{
{"String", "test", true, testValue{reflect.String, "string"}},
{"Int", "10", true, testValue{reflect.Int, 0}},
{"Uint", "10", true, testValue{reflect.Uint16, uint16(10)}},
{"Float64", "3.14", true, testValue{reflect.Float64, 0.0}},
{"Bool", "true", true, testValue{reflect.Bool, false}},
{"Slice", "1,2,3,4,5", true, testValue{reflect.Slice, []int{}}},
{"Array", "1,2,3,4,5", true, testValue{reflect.Array, [5]int{}}},
{"Struct", `{"name": "John", "age": 30}`, true, testValue{reflect.Struct, struct {
Name string `json:"name"`
Age int `json:"age"`
}{}}},
{"Interface", "test interface", true, testValue{reflect.Interface, new(any)}},
}
for _, tc := range testCases {
f := &formData{}
val := reflect.New(reflect.TypeOf(tc.valueType.value)).Elem()
set, err := f.setFieldValue(val, tc.data)
require.NoErrorf(t, err, "Unexpected error for value kind %v and data %q", val.Kind(), tc.data)
require.Equalf(t, tc.expected, set, "Expected set to be %v for value kind %v and data %q", tc.expected, val.Kind(), tc.data)
}
}
func TestSetFieldValue_InvalidKinds(t *testing.T) {
uf := &formData{}
tests := []struct {
kind reflect.Kind
data string
destType reflect.Type
}{
{reflect.Complex64, "foo", reflect.TypeOf(complex64(0))},
{reflect.Complex128, "bar", reflect.TypeOf(complex128(0))},
{reflect.Chan, "baz", reflect.TypeOf(make(chan int))},
{reflect.Func, "qux", reflect.TypeOf(func() {})},
{reflect.Map, "quux", reflect.TypeOf(map[string]int{})},
{reflect.UnsafePointer, "grault", reflect.TypeOf(unsafe.Pointer(nil))},
}
for _, tt := range tests {
value := reflect.New(tt.destType).Elem()
ok, err := uf.setFieldValue(value, tt.data)
require.False(t, ok, "expected false, got true for kind %v", tt.kind)
require.NoError(t, err, "expected nil, got %v for kind %v", err, tt.kind)
}
}
func TestSetSliceOrArrayValue(t *testing.T) {
type testStruct struct {
Slice []string
Array [3]string
}
uf := &formData{}
// Test with a slice
value := reflect.ValueOf(&testStruct{Slice: nil}).Elem().FieldByName("Slice")
data := "a,b,c"
ok, err := uf.setSliceOrArrayValue(value, data)
require.True(t, ok, "setSliceOrArrayValue failed")
require.NoError(t, err, "setSliceOrArrayValue failed: %v", err)
require.Len(t, value.Interface().([]string), 3, "slice not set correctly")
// Test with an array
value = reflect.ValueOf(&testStruct{Array: [3]string{}}).Elem().FieldByName("Array")
data = "a,b,c"
ok, err = uf.setSliceOrArrayValue(value, data)
require.True(t, ok, "setSliceOrArrayValue failed")
require.NoError(t, err, "setSliceOrArrayValue failed: %v", err)
}
func TestSetStructValue_Success(t *testing.T) {
type testStruct struct {
Field1 string
Field2 int
}
uf := &formData{}
tests := []struct {
name string
data string
wantField1 string
wantField2 int
}{
{
name: "Valid input with correct case",
data: `{"Field1":"value1","Field2":123}`,
wantField1: "value1",
wantField2: 123,
},
{
name: "Valid input with case insensitive fields",
data: `{"field1":"value2","FIELD2":456}`,
wantField1: "value2",
wantField2: 456,
},
{
name: "Mixed Case and invalid field names",
// spellchecker:off # using field with case sensitive variations would be reported as a typo otherwise
data: `{"FielD1":"value4", "invalidField":"ignored", "FiEld2":789}`,
// spellchecker:on
wantField1: "value4",
wantField2: 789,
},
{
name: "Case-insensitive field name but not in dataMap",
data: `{"fIeLd1":"value5", "not_in_dataMap": 123}`,
wantField1: "value5",
wantField2: 0, // Field2 should remain unset (default 0)
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := reflect.ValueOf(&testStruct{}).Elem()
ok, err := uf.setStructValue(value, tt.data)
require.NoError(t, err, "TestSetStructValue_Success Failed.")
require.True(t, ok, "TestSetStructValue_Success Failed.")
require.Equal(t, tt.wantField1, value.FieldByName("Field1").String(),
"TestSetStructValue_Success Failed : Field1 not set correctly")
require.Equal(t, tt.wantField2, int(value.FieldByName("Field2").Int()),
"TestSetStructValue_Success Failed : Field2 not set correctly")
})
}
}
func TestSetStructValue_Errors(t *testing.T) {
type testStruct struct {
Field1 string
Field2 int
Field4 float64
}
uf := &formData{}
tests := []struct {
name string
data string
err error
}{
{
name: "Unexported field",
data: `{"field3":"value3"}`,
err: errFieldsNotSet,
},
{
name: "Unsupported field type",
data: `{"field2":1,"Field4":true}`,
err: fmt.Errorf("%w; %w", nil, errUnsupportedType),
},
{
name: "Invalid JSON",
data: `{"Field1":"value1", "Field2":123,`,
err: errJSON, // JSON parsing error
},
{
name: "Field not settable",
data: `{"Field1":"value1", "Field2":123, "Field4": "not a float"}`,
err: errUnsupportedFieldType,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
value := reflect.ValueOf(&testStruct{}).Elem()
_, err := uf.setStructValue(value, tt.data)
require.Error(t, err, "TestSetStructValue_Errors Failed.")
require.Contains(t, err.Error(), tt.err.Error(), "TestSetStructValue_Errors Failed.")
})
}
}
================================================
FILE: pkg/gofr/http/request.go
================================================
package http
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"reflect"
"strings"
"github.com/gorilla/mux"
)
const (
defaultMaxMemory = 32 << 20 // 32 MB
)
var (
errNoFileFound = errors.New("no files were bounded")
errNonPointerBind = errors.New("bind error, cannot bind to a non pointer type")
errNonSliceBind = errors.New("bind error: input is not a pointer to a byte slice")
)
// Request is an abstraction over the underlying http.Request. This abstraction is useful because it allows us
// to create applications without being aware of the transport. cmd.Request is another such abstraction.
type Request struct {
req *http.Request
pathParams map[string]string
}
// NewRequest creates a new GoFr Request instance from the given http.Request.
func NewRequest(r *http.Request) *Request {
return &Request{
req: r,
pathParams: mux.Vars(r),
}
}
// Param returns the query parameter with the given key.
func (r *Request) Param(key string) string {
return r.req.URL.Query().Get(key)
}
// Context returns the context of the request.
func (r *Request) Context() context.Context {
return r.req.Context()
}
// PathParam retrieves a path parameter from the request.
func (r *Request) PathParam(key string) string {
return r.pathParams[key]
}
// Bind parses the request body and binds it to the provided interface.
func (r *Request) Bind(i any) error {
v := r.req.Header.Get("Content-Type")
contentType := strings.Split(v, ";")[0]
switch contentType {
case "application/json":
body, err := r.body()
if err != nil {
return err
}
return json.Unmarshal(body, &i)
case "multipart/form-data":
return r.bindMultipart(i)
case "application/x-www-form-urlencoded":
return r.bindFormURLEncoded(i)
case "binary/octet-stream":
return r.bindBinary(i)
}
return nil
}
// HostName retrieves the hostname from the request.
func (r *Request) HostName() string {
proto := r.req.Header.Get("X-Forwarded-Proto")
if proto == "" {
proto = "http"
}
return fmt.Sprintf("%s://%s", proto, r.req.Host)
}
// Params returns a slice of strings containing the values associated with the given query parameter key.
// If the parameter is not present, an empty slice is returned.
func (r *Request) Params(key string) []string {
values := r.req.URL.Query()[key]
var result []string
for _, value := range values {
result = append(result, strings.Split(value, ",")...)
}
return result
}
func (r *Request) body() ([]byte, error) {
bodyBytes, err := io.ReadAll(r.req.Body)
if err != nil {
return nil, err
}
r.req.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return bodyBytes, nil
}
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
}
ptrVal = ptrVal.Elem()
var fd formData
if isMultipart {
if err := r.req.ParseMultipartForm(defaultMaxMemory); err != nil {
return err
}
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 {
if isMultipart {
return errNoFileFound
}
return errFieldsNotSet
}
return nil
}
// bindBinary handles binding for binary/octet-stream content type.
func (r *Request) bindBinary(raw any) error {
// Ensure raw is a pointer to a byte slice
byteSlicePtr, ok := raw.(*[]byte)
if !ok {
return fmt.Errorf("%w: %v", errNonSliceBind, raw)
}
body, err := r.body()
if err != nil {
return fmt.Errorf("failed to read request body: %w", err)
}
// Assign the body to the provided slice
*byteSlicePtr = body
return nil
}
================================================
FILE: pkg/gofr/http/request_test.go
================================================
package http
import (
"bytes"
"errors"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/file"
)
func TestParam(t *testing.T) {
req := NewRequest(httptest.NewRequest(http.MethodGet, "/abc?a=b", http.NoBody))
if req.Param("a") != "b" {
t.Error("Can not parse the request params")
}
}
func TestBind(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "/abc", strings.NewReader(`{"a": "b", "b": 5}`))
r.Header.Set("Content-Type", "application/json")
req := NewRequest(r)
x := struct {
A string `json:"a"`
B int `json:"b"`
}{}
_ = req.Bind(&x)
if x.A != "b" || x.B != 5 {
t.Errorf("Bind error. Got: %v", x)
}
}
func TestBind_FileSuccess(t *testing.T) {
r := NewRequest(generateMultipartRequestZip(t))
x := struct {
// Zip file bind for zip struct
Zip file.Zip `file:"zip"`
// Zip file bind for zip pointer
ZipPtr *file.Zip `file:"zip"`
// FileHeader multipart.FileHeader bind(value)
FileHeader multipart.FileHeader `file:"hello"`
// FileHeaderPtr multipart.FileHeader bind for pointer
FileHeaderPtr *multipart.FileHeader `file:"hello"`
// Skip bind
Skip *file.Zip `file:"-"`
// Incompatible type cannot be bound
Incompatible string `file:"hello"`
// File not in multipart form
FileNotPresent *multipart.FileHeader `file:"text"`
// Additional form fields
StringField string `form:"stringField"`
IntField int `form:"intField"`
FloatField float64 `form:"floatField"`
BoolField bool `form:"boolField"`
}{}
err := r.Bind(&x)
require.NoError(t, err)
// Assert zip file bind
assert.Len(t, x.Zip.Files, 2)
assert.Equal(t, "Hello! This is file A.\n", string(x.Zip.Files["a.txt"].Bytes()))
assert.Equal(t, "Hello! This is file B.\n\n", string(x.Zip.Files["b.txt"].Bytes()))
// Assert zip file bind for pointer
assert.NotNil(t, x.ZipPtr)
assert.Len(t, x.ZipPtr.Files, 2)
assert.Equal(t, "Hello! This is file A.\n", string(x.ZipPtr.Files["a.txt"].Bytes()))
assert.Equal(t, "Hello! This is file B.\n\n", string(x.ZipPtr.Files["b.txt"].Bytes()))
// Assert FileHeader struct type
assert.Equal(t, "hello.txt", x.FileHeader.Filename)
f, err := x.FileHeader.Open()
require.NoError(t, err)
assert.NotNil(t, f)
content, err := io.ReadAll(f)
require.NoError(t, err)
assert.Equal(t, "Test hello!", string(content))
// Assert FileHeader pointer type
assert.NotNil(t, x.FileHeader)
assert.Equal(t, "hello.txt", x.FileHeader.Filename)
f, err = x.FileHeader.Open()
require.NoError(t, err)
assert.NotNil(t, f)
content, err = io.ReadAll(f)
require.NoError(t, err)
assert.Equal(t, "Test hello!", string(content))
// Assert skipped field
assert.Nil(t, x.Skip)
// Assert incompatible
assert.Empty(t, x.Incompatible)
// Assert file not present
assert.Nil(t, x.FileNotPresent)
// Assert additional form fields
assert.Equal(t, "testString", x.StringField)
assert.Equal(t, 123, x.IntField)
assert.InEpsilon(t, 123.456, x.FloatField, 0.01)
assert.True(t, x.BoolField)
}
func TestBind_NoContentType(t *testing.T) {
req := NewRequest(httptest.NewRequest(http.MethodPost, "/abc", strings.NewReader(`{"a": "b", "b": 5}`)))
x := struct {
A string `json:"a"`
B int `json:"b"`
}{}
_ = req.Bind(&x)
// The data won't bind so zero values are expected
if x.A != "" || x.B != 0 {
t.Errorf("Bind error. Got: %v", x)
}
}
func Test_GetContext(t *testing.T) {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "test/hello", http.NoBody)
r := Request{req: req, pathParams: map[string]string{"key": "hello"}}
assert.Equal(t, t.Context(), r.Context())
assert.Equal(t, "http://", r.HostName())
assert.Equal(t, "hello", r.PathParam("key"))
}
func generateMultipartRequestZip(t *testing.T) *http.Request {
t.Helper()
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
f, err := os.Open("../testutil/test.zip")
if err != nil {
t.Fatalf("Failed to open test.zip: %v", err)
}
defer f.Close()
zipPart, err := writer.CreateFormFile("zip", "test.zip")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
_, err = io.Copy(zipPart, f)
if err != nil {
t.Fatalf("Failed to write file to form: %v", err)
}
fileHeader, err := writer.CreateFormFile("hello", "hello.txt")
if err != nil {
t.Fatalf("Failed to create form file: %v", err)
}
_, err = io.Copy(fileHeader, bytes.NewReader([]byte(`Test hello!`)))
if err != nil {
t.Fatalf("Failed to write file to form: %v", err)
}
// Add non-file fields
err = writer.WriteField("stringField", "testString")
require.NoError(t, err)
err = writer.WriteField("intField", "123")
require.NoError(t, err)
err = writer.WriteField("floatField", "123.456")
require.NoError(t, err)
err = writer.WriteField("boolField", "true")
require.NoError(t, err)
// Close the multipart writer
writer.Close()
// Create a new HTTP request with the multipart data
req := httptest.NewRequest(http.MethodPost, "/upload", &buf)
req.Header.Set("Content-Type", writer.FormDataContentType())
return req
}
func Test_bindMultipart_Fails(t *testing.T) {
// Non-pointer bind
r := NewRequest(generateMultipartRequestZip(t))
input := struct {
file *file.Zip
}{}
err := r.bindMultipart(input)
require.Error(t, err)
assert.Equal(t, errNonPointerBind, err)
// unexported field cannot be binded
err = r.bindMultipart(&input)
require.ErrorIs(t, err, errNoFileFound)
}
func Test_bindMultipart_Fail_ParseMultiPart(t *testing.T) {
r := NewRequest(generateMultipartRequestZip(t))
input2 := struct {
File *file.Zip `file:"zip"`
}{}
// Call the multipart reader to handle form from a multipart reader
// This is called to invoke error while parsing Multipart form in bind
_, _ = r.req.MultipartReader()
err := r.bindMultipart(&input2)
require.ErrorContains(t, err, "http: multipart handled by MultipartReader")
}
func Test_Params(t *testing.T) {
req := &http.Request{
URL: &url.URL{
RawQuery: "category=books&category=electronics&tag=tech,science",
},
}
r := NewRequest(req)
expectedCategories := []string{"books", "electronics"}
expectedTags := []string{"tech", "science"}
assert.ElementsMatch(t, expectedCategories, r.Params("category"), "expected all values of 'category' to match")
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)
}
}
func TestBind_BinaryOctetStream(t *testing.T) {
testCases := []struct {
name string
data []byte
}{
{"Raw Binary Data", []byte{0x42, 0x65, 0x6c, 0x6c, 0x61}},
{"Text-Based Binary Data", []byte("This is some binary data")},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
req := NewRequest(httptest.NewRequest(http.MethodPost, "/binary", bytes.NewReader(tc.data)))
req.req.Header.Set("Content-Type", "binary/octet-stream")
var result []byte
err := req.Bind(&result)
if err != nil {
t.Errorf("Bind error: %v", err)
}
if !bytes.Equal(result, tc.data) {
t.Errorf("Bind error. Expected: %v, Got: %v", tc.data, result)
}
})
}
}
func TestBind_BinaryOctetStream_NotPointerToByteSlice(t *testing.T) {
req := &Request{
req: httptest.NewRequest(http.MethodPost, "/binary", http.NoBody),
}
req.req.Header.Set("Content-Type", "binary/octet-stream")
err := req.Bind("invalid input")
if !errors.Is(err, errNonSliceBind) {
t.Fatalf("Expected error: %v, got: %v", errNonSliceBind, err)
}
if !strings.Contains(err.Error(), "input is not a pointer to a byte slice: invalid input") {
t.Errorf("Expected error to contain: input is not a pointer to a byte slice: invalid input, got: %v", err)
}
}
================================================
FILE: pkg/gofr/http/responder.go
================================================
package http
import (
"encoding/json"
"errors"
"net/http"
"reflect"
resTypes "gofr.dev/pkg/gofr/http/response"
)
var (
errEmptyResponse = errors.New("internal server error")
)
// NewResponder creates a new Responder instance from the given http.ResponseWriter..
func NewResponder(w http.ResponseWriter, method string) *Responder {
return &Responder{w: w, method: method}
}
// Responder encapsulates an http.ResponseWriter and is responsible for crafting structured responses.
type Responder struct {
w http.ResponseWriter
method string
}
// Respond sends a response with the given data and handles potential errors, setting appropriate
// status codes and formatting responses as JSON or raw data as needed.
func (r Responder) Respond(data any, err error) {
if r.handleSpecialResponseTypes(data, err) {
return
}
statusCode, errorObj := r.determineResponse(data, err)
var resp any
switch v := data.(type) {
case resTypes.Raw:
resp = v.Data
case resTypes.Response:
resp = response{Data: v.Data, Metadata: v.Metadata, Error: errorObj}
default:
// handling where an interface contains a nullable type with a nil value.
if isNil(data) {
data = nil
}
resp = response{Data: data, Error: errorObj}
}
if r.w.Header().Get("Content-Type") == "" {
r.w.Header().Set("Content-Type", "application/json")
}
jsonData, encodeErr := json.Marshal(resp)
if encodeErr != nil {
r.w.WriteHeader(http.StatusInternalServerError)
_, _ = r.w.Write([]byte(`{"error":{"message": "failed to encode response as JSON"}}` + "\n"))
return
}
r.w.WriteHeader(statusCode)
_, _ = r.w.Write(jsonData)
_, _ = r.w.Write([]byte("\n"))
}
// handleSpecialResponseTypes handles special response types that bypass JSON encoding.
// Returns true if the response was handled, false otherwise.
func (r Responder) handleSpecialResponseTypes(data any, err error) bool {
// For special response types (XML/File/Template), use error status code directly
// instead of partial content (206) when errors occur
statusCode := r.getStatusCodeForSpecialResponse(data, err)
switch v := data.(type) {
case resTypes.File:
r.w.Header().Set("Content-Type", v.ContentType)
r.w.WriteHeader(statusCode)
_, _ = r.w.Write(v.Content)
return true
case resTypes.Template:
r.w.Header().Set("Content-Type", "text/html")
r.w.WriteHeader(statusCode)
v.Render(r.w)
return true
case resTypes.XML:
contentType := v.ContentType
if contentType == "" {
contentType = "application/xml"
}
r.w.Header().Set("Content-Type", contentType)
r.w.WriteHeader(statusCode)
if len(v.Content) > 0 {
_, _ = r.w.Write(v.Content)
}
return true
case resTypes.Redirect:
// Redirect status codes are determined by HTTP method, not error state
redirectStatusCode := http.StatusFound
if r.method == http.MethodPost || r.method == http.MethodPut || r.method == http.MethodPatch {
redirectStatusCode = http.StatusSeeOther
}
r.w.Header().Set("Location", v.URL)
r.w.WriteHeader(redirectStatusCode)
return true
}
return false
}
// getStatusCodeForSpecialResponse returns the appropriate status code for special response types.
// Unlike regular responses, special types (XML/File/Template) should use error status codes
// directly instead of returning 206 (Partial Content) when both data and error are present.
func (r Responder) getStatusCodeForSpecialResponse(data any, err error) int {
if err == nil {
return handleSuccessStatusCode(r.method, data)
}
// For special response types, prioritize error status code over partial content
if e, ok := err.(StatusCodeResponder); ok {
return e.StatusCode()
}
return http.StatusInternalServerError
}
// handleSuccessStatusCode returns the status code for successful responses based on HTTP method.
func handleSuccessStatusCode(method string, data any) int {
switch method {
case http.MethodPost:
if data != nil {
return http.StatusCreated
}
return http.StatusAccepted
case http.MethodDelete:
return http.StatusNoContent
default:
return http.StatusOK
}
}
func (r Responder) determineResponse(data any, err error) (statusCode int, errObj any) {
// Handle empty struct case first
if err != nil && isEmptyStruct(data) {
return http.StatusInternalServerError, createErrorResponse(errEmptyResponse)
}
statusCode, errorObj := getStatusCode(r.method, data, err)
if statusCode == 0 {
statusCode = http.StatusInternalServerError
}
return statusCode, errorObj
}
// isEmptyStruct checks if a value is a struct with all zero/empty fields.
func isEmptyStruct(data any) bool {
if data == nil {
return false
}
v := reflect.ValueOf(data)
// Handle pointers by dereferencing them
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return false // nil pointer isn't an empty struct
}
v = v.Elem()
}
// Only check actual struct types
if v.Kind() != reflect.Struct {
return false
}
// Compare against a zero value of the same type
zero := reflect.Zero(v.Type()).Interface()
return reflect.DeepEqual(data, zero)
}
// getStatusCode returns corresponding HTTP status codes.
func getStatusCode(method string, data any, err error) (statusCode int, errResp any) {
if err == nil {
return handleSuccess(method, data)
}
if !isNil(data) {
return http.StatusPartialContent, createErrorResponse(err)
}
if e, ok := err.(StatusCodeResponder); ok {
return e.StatusCode(), createErrorResponse(err)
}
return http.StatusInternalServerError, createErrorResponse(err)
}
func handleSuccess(method string, data any) (statusCode int, err any) {
switch method {
case http.MethodPost:
if data != nil {
return http.StatusCreated, nil
}
return http.StatusAccepted, nil
case http.MethodDelete:
return http.StatusNoContent, nil
default:
return http.StatusOK, nil
}
}
// ResponseMarshaller defines an interface for errors that can provide custom fields.
// This enables errors to extend the error response with additional fields.
type ResponseMarshaller interface {
Response() map[string]any
}
// createErrorResponse returns an error response that always contains a "message" field,
// and if the error implements ResponseMarshaller, it merges custom fields into the response.
func createErrorResponse(err error) map[string]any {
resp := map[string]any{"message": err.Error()}
if rm, ok := err.(ResponseMarshaller); ok {
for k, v := range rm.Response() {
if k == "message" {
continue // Skip to avoid overriding the Error() message
}
resp[k] = v
}
}
return resp
}
// response represents an HTTP response.
type response struct {
Error any `json:"error,omitempty"`
Metadata map[string]any `json:"metadata,omitempty"`
Data any `json:"data,omitempty"`
}
type StatusCodeResponder interface {
StatusCode() int
}
// isNil checks if the given any value is nil.
// It returns true if the value is nil or if it is a pointer that points to nil.
// This function is useful for determining whether a value, including interface or pointer types, is effectively nil.
func isNil(i any) bool {
if i == nil {
return true
}
v := reflect.ValueOf(i)
return v.Kind() == reflect.Ptr && v.IsNil()
}
================================================
FILE: pkg/gofr/http/responder_test.go
================================================
package http
import (
"bytes"
"fmt"
"io"
"math"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
resTypes "gofr.dev/pkg/gofr/http/response"
)
var errTest = fmt.Errorf("internal server error")
func TestResponder(t *testing.T) {
tests := []struct {
desc string
data any
contentType string
expectedBody []byte
}{
{
desc: "xml response type default content type",
data: resTypes.XML{
Content: []byte(`Hello `),
},
contentType: "application/xml",
expectedBody: []byte(`Hello `),
},
{
desc: "xml response type custom content type",
data: resTypes.XML{
Content: []byte(` `),
ContentType: "application/soap+xml",
},
contentType: "application/soap+xml",
expectedBody: []byte(` `),
},
{
desc: "raw response type",
data: resTypes.Raw{Data: []byte("raw data")},
contentType: "application/json",
expectedBody: []byte(`"cmF3IGRhdGE="`),
},
{
desc: "file response type",
data: resTypes.File{
ContentType: "image/png",
},
contentType: "image/png",
expectedBody: nil,
},
{
desc: "map response type",
data: map[string]string{"key": "value"},
contentType: "application/json",
expectedBody: []byte(`{"data":{"key":"value"}}`),
},
{
desc: "gofr response type with metadata",
data: resTypes.Response{
Data: "Hello World from new Server",
Metadata: map[string]any{
"environment": "stage",
},
},
contentType: "application/json",
expectedBody: []byte(`{"metadata":{"environment":"stage"},"data":"Hello World from new Server"}`),
},
{
desc: "gofr response type without metadata",
data: resTypes.Response{
Data: "Hello World from new Server",
},
contentType: "application/json",
expectedBody: []byte(`{"data":"Hello World from new Server"}`),
},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
recorder.Body.Reset()
r := NewResponder(recorder, http.MethodGet)
r.Respond(tc.data, nil)
contentType := recorder.Header().Get("Content-Type")
assert.Equal(t, tc.contentType, contentType, "TEST[%d] Failed: %s", i, tc.desc)
responseBody := recorder.Body.Bytes()
expected := bytes.TrimSpace(tc.expectedBody)
actual := bytes.TrimSpace(responseBody)
assert.Equal(t, expected, actual, "TEST[%d] Failed: %s", i, tc.desc)
}
}
func TestResponder_getStatusCode(t *testing.T) {
tests := []struct {
desc string
method string
data any
err error
statusCode int
errObj any
}{
{"success case", http.MethodGet, "success response", nil, http.StatusOK, nil},
{"post with response body", http.MethodPost, "entity created", nil, http.StatusCreated, nil},
{"post with nil response", http.MethodPost, nil, nil, http.StatusAccepted, nil},
{"success delete", http.MethodDelete, nil, nil, http.StatusNoContent, nil},
{"invalid route error", http.MethodGet, nil, ErrorInvalidRoute{}, http.StatusNotFound,
map[string]any{"message": ErrorInvalidRoute{}.Error()}},
{"internal server error", http.MethodGet, nil, http.ErrHandlerTimeout, http.StatusInternalServerError,
map[string]any{"message": http.ErrHandlerTimeout.Error()}},
{"partial content with error", http.MethodGet, "partial response", ErrorInvalidRoute{},
http.StatusPartialContent, map[string]any{"message": ErrorInvalidRoute{}.Error()}},
{"request timeout error", http.MethodGet, nil, ErrorRequestTimeout{},
http.StatusRequestTimeout,
map[string]any{"message": ErrorRequestTimeout{}.Error()}},
{"client closed request error", http.MethodGet, nil, ErrorClientClosedRequest{}, 499,
map[string]any{"message": ErrorClientClosedRequest{}.Error()}},
{"server timeout error", http.MethodGet, nil, ErrorRequestTimeout{}, http.StatusRequestTimeout,
map[string]any{"message": ErrorRequestTimeout{}.Error()}},
}
for i, tc := range tests {
statusCode, errObj := getStatusCode(tc.method, tc.data, tc.err)
assert.Equal(t, tc.statusCode, statusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.errObj, errObj, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
type temp struct {
ID string `json:"id,omitempty"`
}
// newNilTemp returns a nil pointer of type *temp for testing purposes.
func newNilTemp() *temp {
return nil
}
func TestRespondWithApplicationJSON(t *testing.T) {
sampleData := map[string]string{"message": "Hello World"}
sampleError := ErrorInvalidRoute{}
tests := []struct {
desc string
data any
err error
expectedCode int
expectedBody string
}{
{"sample data response", sampleData, nil,
http.StatusOK, `{"data":{"message":"Hello World"}}`},
{"error response", nil, sampleError,
http.StatusNotFound, `{"error":{"message":"route not registered"}}`},
{"error response contains a nullable type with a nil value", newNilTemp(), sampleError,
http.StatusNotFound, `{"error":{"message":"route not registered"}}`},
{"error response with partial response", sampleData, sampleError,
http.StatusPartialContent,
`{"error":{"message":"route not registered"},"data":{"message":"Hello World"}}`},
{"client closed request - no response", nil, ErrorClientClosedRequest{},
StatusClientClosedRequest, `{"error":{"message":"client closed request"}}`},
{"server timeout error", nil, ErrorRequestTimeout{},
http.StatusRequestTimeout, `{"error":{"message":"request timed out"}}`},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
responder := Responder{w: recorder, method: http.MethodGet}
responder.Respond(tc.data, tc.err)
result := recorder.Result()
assert.Equal(t, tc.expectedCode, result.StatusCode, "TEST[%d], Failed.\n%s", i, tc.desc)
body := new(bytes.Buffer)
_, err := body.ReadFrom(result.Body)
result.Body.Close()
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
// json Encoder by default terminate each value with a newline
tc.expectedBody += "\n"
assert.Equal(t, tc.expectedBody, body.String(), "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestIsNil(t *testing.T) {
tests := []struct {
desc string
value any
expected bool
}{
{"nil value", nil, true},
{"nullable type with a nil value", newNilTemp(), true},
{"not nil value", temp{ID: "test"}, false},
{"chan type", make(chan int), false},
}
for i, tc := range tests {
resp := isNil(tc.value)
assert.Equal(t, tc.expected, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func TestResponder_TemplateResponse(t *testing.T) {
templatePath := "./templates/example.html"
templateContent := `{{.Title}} {{.Body}}`
createTemplateFile(t, templatePath, templateContent)
defer removeTemplateDir(t)
recorder := httptest.NewRecorder()
r := NewResponder(recorder, http.MethodGet)
templateData := map[string]string{"Title": "Test Title", "Body": "Test Body"}
expectedBody := "Test Title Test Body"
r.Respond(resTypes.Template{Name: "example.html", Data: templateData}, nil)
contentType := recorder.Header().Get("Content-Type")
responseBody := recorder.Body.String()
assert.Equal(t, "text/html", contentType)
assert.Equal(t, expectedBody, responseBody)
}
func TestResponder_CustomErrorWithResponse(t *testing.T) {
w := httptest.NewRecorder()
responder := NewResponder(w, http.MethodGet)
customErr := &CustomError{
Code: http.StatusNotFound,
Message: "resource not found",
Title: "Custom Error",
}
responder.Respond(nil, customErr)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
expectedJSON := `{
"error": {
"code": 404,
"title": "Custom Error",
"message": "resource not found"
}
}`
assert.JSONEq(t, expectedJSON, string(bodyBytes))
}
type CustomError struct {
Code int
Message string
Title string
}
func (e *CustomError) Error() string { return e.Message }
func (e *CustomError) StatusCode() int { return e.Code }
func (e *CustomError) Response() map[string]any {
return map[string]any{"title": e.Title, "code": e.Code}
}
func TestResponder_ReservedMessageField(t *testing.T) {
w := httptest.NewRecorder()
responder := NewResponder(w, http.MethodGet)
msgErr := &MessageOverrideError{
Msg: "original message",
}
responder.Respond(nil, msgErr)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err)
expectedJSON := `{
"error": {
"message": "original message",
"info": "additional info"
}
}`
assert.JSONEq(t, expectedJSON, string(bodyBytes))
}
// EmptyError represents an error as an empty struct.
// It implements the error interface.
type emptyError struct{}
// Error implements the error interface.
func (emptyError) Error() string {
return "error occurred"
}
func TestResponder_EmptyErrorStruct(t *testing.T) {
recorder := httptest.NewRecorder()
responder := Responder{w: recorder, method: http.MethodGet}
statusCode, errObj := responder.determineResponse(nil, emptyError{})
assert.Equal(t, http.StatusInternalServerError, statusCode)
assert.Equal(t, map[string]any{"message": "error occurred"}, errObj)
}
func TestIsEmptyStruct(t *testing.T) {
tests := []struct {
desc string
data any
expected bool
}{
{"nil value", nil, false},
{"empty struct", struct{}{}, true},
{"non-empty struct", struct{ ID int }{ID: 1}, false},
{"nil pointer to struct", (*struct{})(nil), false},
{"pointer to non-empty struct", &struct{ ID int }{ID: 1}, false},
{"non-struct type", 42, false},
}
for i, tc := range tests {
result := isEmptyStruct(tc.data)
assert.Equal(t, tc.expected, result, "TEST[%d] Failed: %s", i, tc.desc)
}
}
type MessageOverrideError struct {
Msg string
}
func (e *MessageOverrideError) Error() string { return e.Msg }
func (*MessageOverrideError) Response() map[string]any {
return map[string]any{
"message": "trying to override",
"info": "additional info",
}
}
func createTemplateFile(t *testing.T, path, content string) {
t.Helper()
err := os.MkdirAll("./templates", os.ModePerm)
require.NoError(t, err)
err = os.WriteFile(path, []byte(content), 0600)
require.NoError(t, err)
}
func removeTemplateDir(t *testing.T) {
t.Helper()
err := os.RemoveAll("./templates")
require.NoError(t, err)
}
func TestResponder_RedirectResponse_Post(t *testing.T) {
recorder := httptest.NewRecorder()
r := NewResponder(recorder, http.MethodPost)
// Set up redirect with specific URL and status code
redirectURL := "/new-location?from=start"
statusCode := http.StatusSeeOther // 303
redirect := resTypes.Redirect{URL: redirectURL}
r.Respond(redirect, nil)
assert.Equal(t, statusCode, recorder.Code, "Redirect should set the correct status code")
assert.Equal(t, redirectURL, recorder.Header().Get("Location"),
"Redirect should set the Location header")
assert.Empty(t, recorder.Body.String(), "Redirect response should not have a body")
}
func TestResponder_RedirectResponse_Head(t *testing.T) {
recorder := httptest.NewRecorder()
r := NewResponder(recorder, http.MethodHead)
// Set up redirect with specific URL and status code
redirectURL := "/new-location?from=start"
statusCode := http.StatusFound // 302
redirect := resTypes.Redirect{URL: redirectURL}
r.Respond(redirect, nil)
assert.Equal(t, statusCode, recorder.Code, "Redirect should set the correct status code")
assert.Equal(t, redirectURL, recorder.Header().Get("Location"),
"Redirect should set the Location header")
assert.Empty(t, recorder.Body.String(), "Redirect response should not have a body")
}
func TestResponder_ClientClosedRequestHandling(t *testing.T) {
recorder := httptest.NewRecorder()
responder := NewResponder(recorder, http.MethodGet)
// ErrorClientClosedRequest should not send any response
responder.Respond(nil, ErrorClientClosedRequest{})
assert.Equal(t, 499, recorder.Code)
assert.JSONEq(t, `{"error":{"message":"client closed request"}}`, recorder.Body.String())
}
func TestResponder_ContentTypePreservation(t *testing.T) {
tests := []struct {
desc string
presetContentType string
expectedType string
}{
{
desc: "preset content type should be preserved",
presetContentType: "text/event-stream",
expectedType: "text/event-stream",
},
{
desc: "no preset content type - defaults to application/json",
presetContentType: "",
expectedType: "application/json",
},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
// Simulate SetCustomHeaders by manually setting Content-Type header before calling Respond
if tc.presetContentType != "" {
recorder.Header().Set("Content-Type", tc.presetContentType)
}
responder := NewResponder(recorder, http.MethodGet)
responder.Respond("Test data", nil)
contentType := recorder.Header().Get("Content-Type")
assert.Equal(t, tc.expectedType, contentType, "TEST[%d] Failed: %s", i, tc.desc)
}
}
// TestResponder_XMLFileTemplate_ErrorStatusCodes verifies that XML, File, and Template responses
// return appropriate error status codes when errors occur, not always 200 OK.
func TestResponder_XMLFileTemplate_ErrorStatusCodes(t *testing.T) {
tests := []struct {
desc string
data any
err error
expectedCode int
}{
{
desc: "XML response with 404 error should return 404",
data: resTypes.XML{
Content: []byte(`Not Found `),
},
err: ErrorEntityNotFound{Name: "id", Value: "123"},
expectedCode: http.StatusNotFound,
},
{
desc: "XML response with 500 error should return 500",
data: resTypes.XML{
Content: []byte(`Internal Error `),
},
err: errTest,
expectedCode: http.StatusInternalServerError,
},
{
desc: "File response with 404 error should return 404",
data: resTypes.File{
ContentType: "image/png",
Content: []byte("fake image data"),
},
err: ErrorEntityNotFound{Name: "file", Value: "test.png"},
expectedCode: http.StatusNotFound,
},
{
desc: "File response with 500 error should return 500",
data: resTypes.File{
ContentType: "application/pdf",
Content: []byte("fake pdf data"),
},
err: errTest,
expectedCode: http.StatusInternalServerError,
},
{
desc: "XML response with no error should return 200",
data: resTypes.XML{
Content: []byte(`OK `),
},
err: nil,
expectedCode: http.StatusOK,
},
{
desc: "File response with no error should return 200",
data: resTypes.File{
ContentType: "text/plain",
Content: []byte("file content"),
},
err: nil,
expectedCode: http.StatusOK,
},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
r := NewResponder(recorder, http.MethodGet)
r.Respond(tc.data, tc.err)
assert.Equal(t, tc.expectedCode, recorder.Code, "TEST[%d] Failed: %s", i, tc.desc)
}
}
func TestResponder_JSONEncodingFailure(t *testing.T) {
tests := []struct {
desc string
data any
}{
{"NaN value", math.NaN()},
{"positive infinity", math.Inf(1)},
{"negative infinity", math.Inf(-1)},
{"channel type", make(chan int)},
{"function type", func() {}},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
responder := NewResponder(recorder, http.MethodGet)
responder.Respond(tc.data, nil)
result := recorder.Result()
assert.Equal(t, http.StatusInternalServerError, result.StatusCode, "TEST[%d] Failed: %s", i, tc.desc)
assert.Equal(t, "application/json", result.Header.Get("Content-Type"), "TEST[%d] Failed: %s", i, tc.desc)
body := new(bytes.Buffer)
_, err := body.ReadFrom(result.Body)
require.NoError(t, err, "TEST[%d] Failed: %s", i, tc.desc)
expectedBody := `{"error":{"message": "failed to encode response as JSON"}}` + "\n"
assert.Equal(t, expectedBody, body.String(), "TEST[%d] Failed: %s", i, tc.desc)
require.NoError(t, result.Body.Close())
}
}
func TestResponder_ValidEncodableData(t *testing.T) {
tests := []struct {
desc string
data any
expectedCode int
}{
{"normal float", 42.5, http.StatusOK},
{"zero float", 0.0, http.StatusOK},
{"struct with floats", struct{ Temp float64 }{Temp: 98.6}, http.StatusOK},
{"map with numbers", map[string]float64{"value": 123.45}, http.StatusOK},
}
for i, tc := range tests {
recorder := httptest.NewRecorder()
responder := NewResponder(recorder, http.MethodGet)
responder.Respond(tc.data, nil)
result := recorder.Result()
t.Cleanup(func() {
require.NoError(t, result.Body.Close())
})
assert.Equal(t, tc.expectedCode, result.StatusCode, "TEST[%d] Failed: %s", i, tc.desc)
body := new(bytes.Buffer)
_, err := body.ReadFrom(result.Body)
require.NoError(t, err, "TEST[%d] Failed: %s", i, tc.desc)
assert.NotEmpty(t, body.String(), "TEST[%d] Failed: %s", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/http/response/file.go
================================================
package response
type File struct {
Content []byte
ContentType string
}
================================================
FILE: pkg/gofr/http/response/raw.go
================================================
package response
type Raw struct {
Data any
}
================================================
FILE: pkg/gofr/http/response/redirect.go
================================================
package response
type Redirect struct {
URL string
}
================================================
FILE: pkg/gofr/http/response/response.go
================================================
package response
import (
"net/http"
)
type Response struct {
Data any `json:"data"`
Metadata map[string]any `json:"metadata,omitempty"`
Headers map[string]string `json:"-"`
}
func (resp Response) SetCustomHeaders(w http.ResponseWriter) {
for key, value := range resp.Headers {
w.Header().Set(key, value)
}
}
================================================
FILE: pkg/gofr/http/response/template.go
================================================
package response
import (
"html/template"
"io"
)
type Template struct { // Named as such to avoid conflict with imported template
Data any
Name string
}
func (t *Template) Render(w io.Writer) {
tmpl := template.Must(template.ParseFiles("./templates/" + t.Name))
_ = tmpl.Execute(w, t.Data)
}
================================================
FILE: pkg/gofr/http/response/xml.go
================================================
package response
// XML represents a response that should be sent as XML without JSON encoding.
// If ContentType is empty, Responder defaults it to application/xml.
type XML struct {
Content []byte
ContentType string
}
================================================
FILE: pkg/gofr/http/router.go
================================================
package http
import (
"fmt"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"gofr.dev/pkg/gofr/logging"
)
const (
DefaultSwaggerFileName = "openapi.json"
staticServerNotFoundFileName = "404.html"
)
var errReadPermissionDenied = fmt.Errorf("file does not have read permission")
// Router is responsible for routing HTTP request.
type Router struct {
mux.Router
RegisteredRoutes *[]string
}
type Middleware func(handler http.Handler) http.Handler
// NewRouter creates a new Router instance.
func NewRouter() *Router {
muxRouter := mux.NewRouter().StrictSlash(false).SkipClean(true)
routes := make([]string, 0)
r := &Router{
Router: *muxRouter,
RegisteredRoutes: &routes,
}
r.Router = *muxRouter
return r
}
// ServeHTTP implements [http.Handler] interface with path normalization.
func (rou *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Normalize the path before routing to handle double slashes
originalPath := r.URL.Path
normalizedPath := path.Clean(originalPath)
// path.Clean returns "." for empty paths, convert to "/" for HTTP routing
if normalizedPath == "." {
normalizedPath = "/"
}
// Ensure path starts with "/" for HTTP routing
normalizedPath = "/" + strings.TrimLeft(normalizedPath, "/")
// Only modify if path changed
if originalPath != normalizedPath {
r.URL.Path = normalizedPath
if r.URL.RawPath != "" {
r.URL.RawPath = normalizedPath
}
}
// Delegate to the underlying Gorilla Mux router
rou.Router.ServeHTTP(w, r)
}
// Add adds a new route with the given HTTP method, pattern, and handler, wrapping the handler with OpenTelemetry instrumentation.
func (rou *Router) Add(method, pattern string, handler http.Handler) {
h := otelhttp.NewHandler(handler, "gofr-router")
rou.Router.NewRoute().Methods(method).Path(pattern).Handler(h)
}
// UseMiddleware registers middlewares to the router.
func (rou *Router) UseMiddleware(mws ...Middleware) {
middlewares := make([]mux.MiddlewareFunc, 0, len(mws))
for _, m := range mws {
middlewares = append(middlewares, mux.MiddlewareFunc(m))
}
rou.Use(middlewares...)
}
type staticFileConfig struct {
directoryName string
logger logging.Logger
}
func (rou *Router) AddStaticFiles(logger logging.Logger, endpoint, dirName string) {
cfg := staticFileConfig{directoryName: dirName, logger: logger}
fileServer := http.FileServer(http.Dir(cfg.directoryName))
if endpoint != "/" {
endpoint += "/"
}
rou.Router.NewRoute().PathPrefix(endpoint).Handler(http.StripPrefix(endpoint, cfg.staticHandler(fileServer)))
logger.Logf("registered static files at endpoint %v from directory %v", endpoint, dirName)
}
func (staticConfig staticFileConfig) staticHandler(fileServer http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
url := r.URL.Path
absPath, err := filepath.Abs(filepath.Join(staticConfig.directoryName, url))
if err != nil {
staticConfig.respondWithError(w, "failed to resolve absolute path", url, err, http.StatusInternalServerError)
return
}
// Restrict direct access to openapi.json via static routes.
// Allow access only through /.well-known/swagger or /.well-known/openapi.json.
if staticConfig.isRestrictedFile(url, absPath) {
staticConfig.respondWithError(w, "unauthorized attempt to access restricted file", url, nil, http.StatusForbidden)
return
}
if err := staticConfig.validateFile(absPath); err != nil {
staticConfig.respondWithFileError(w, r, absPath, err)
return
}
staticConfig.logger.Debugf("serving file: %s", absPath)
fileServer.ServeHTTP(w, r)
})
}
// Checks if the file is restricted.
func (staticConfig staticFileConfig) isRestrictedFile(url, absPath string) bool {
fileName := filepath.Base(url)
return !strings.HasPrefix(absPath, staticConfig.directoryName+string(os.PathSeparator)) || fileName == DefaultSwaggerFileName
}
// Validates file existence and permissions.
func (staticFileConfig) validateFile(absPath string) error {
fileInfo, err := os.Stat(absPath)
if err != nil {
return err
}
// Ensure file has at least read (`r--`) permission
if fileInfo.Mode().Perm()&0444 == 0 {
return errReadPermissionDenied
}
return nil
}
// Handles different file-related errors.
func (staticConfig staticFileConfig) respondWithFileError(w http.ResponseWriter, r *http.Request, absPath string, err error) {
if os.IsNotExist(err) {
staticConfig.logger.Debugf("requested file not found: %s", absPath)
w.WriteHeader(http.StatusNotFound)
// Serve custom 404.html if available
notFoundPath, _ := filepath.Abs(filepath.Join(staticConfig.directoryName, staticServerNotFoundFileName))
if _, err = os.Stat(notFoundPath); err == nil {
staticConfig.logger.Debugf("serving custom 404 page: %s", notFoundPath)
http.ServeFile(w, r, notFoundPath)
return
}
_, _ = w.Write([]byte("404 Not Found"))
return
}
staticConfig.respondWithError(w, "error accessing file", absPath, err, http.StatusInternalServerError)
}
// Generic error response handler.
func (staticConfig staticFileConfig) respondWithError(w http.ResponseWriter, message, url string, err error, status int) {
if err != nil {
staticConfig.logger.Errorf("%s: %s, error: %v", message, url, err)
} else {
staticConfig.logger.Debugf("%s: %s", message, url)
}
w.WriteHeader(status)
fmt.Fprintf(w, "%d %s", status, http.StatusText(status))
}
================================================
FILE: pkg/gofr/http/router_test.go
================================================
package http
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestRouter(t *testing.T) {
port := testutil.GetFreePort(t)
cfg := map[string]string{"HTTP_PORT": fmt.Sprint(port), "LOG_LEVEL": "INFO"}
c := container.NewContainer(config.NewMockConfig(cfg))
c.Metrics().NewCounter("test-counter", "test")
// Create a new router instance using the mock container
router := NewRouter()
// Add a test handler to the router
router.Add(http.MethodGet, "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Send a request to the test handler
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
// Verify the response
assert.Equal(t, http.StatusOK, rec.Code)
}
func TestRouterWithMiddleware(t *testing.T) {
port := testutil.GetFreePort(t)
cfg := map[string]string{"HTTP_PORT": fmt.Sprint(port), "LOG_LEVEL": "INFO"}
c := container.NewContainer(config.NewMockConfig(cfg))
c.Metrics().NewCounter("test-counter", "test")
// Create a new router instance using the mock container
router := NewRouter()
router.UseMiddleware(func(inner http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Test-Middleware", "applied")
inner.ServeHTTP(w, r)
})
})
// Add a test handler to the router
router.Add(http.MethodGet, "/test", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Send a request to the test handler
req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
// Verify the response
assert.Equal(t, http.StatusOK, rec.Code)
// checking if the testMiddleware has added the required header in the response properly.
testHeaderValue := rec.Header().Get("X-Test-Middleware")
assert.Equal(t, "applied", testHeaderValue, "Test_UseMiddleware Failed! header value mismatch.")
}
// TestRouter_DoubleSlashPath_GET verifies that GET requests with double slashes
// are normalized and routed correctly to the GET handler.
func TestRouter_DoubleSlashPath_GET(t *testing.T) {
router := NewRouter()
getHandlerCalled := false
postHandlerCalled := false
// Register both GET and POST handlers for /hello
router.Add(http.MethodGet, "/hello", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
getHandlerCalled = true
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("GET handler"))
}))
router.Add(http.MethodPost, "/hello", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
postHandlerCalled = true
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("POST handler"))
}))
tests := []struct {
desc string
path string
}{
{desc: "GET request to /hello", path: "/hello"},
{desc: "GET request to //hello", path: "//hello"},
{desc: "GET request to ///hello", path: "///hello"},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
getHandlerCalled = false
postHandlerCalled = false
req := httptest.NewRequest(http.MethodGet, tc.path, http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code, "Status code mismatch")
assert.True(t, getHandlerCalled, "GET handler should be called")
assert.False(t, postHandlerCalled, "POST handler should NOT be called")
assert.Equal(t, "GET handler", rec.Body.String(), "Response body mismatch")
assert.Empty(t, rec.Header().Get("Location"), "No redirect should be issued")
})
}
}
// TestRouter_PathNormalization tests the path normalization function directly.
func TestRouter_PathNormalization(t *testing.T) {
router := NewRouter()
// Register handlers for testing
router.Add(http.MethodGet, "/hello", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hello"))
}))
router.Add(http.MethodGet, "/api/v1/users", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("users"))
}))
router.Add(http.MethodGet, "/bar", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("bar"))
}))
tests := []struct {
name string
input string
expectedCode int
expectedBody string
}{
{name: "simple path", input: "/hello", expectedCode: http.StatusOK, expectedBody: "hello"},
{name: "double slash", input: "//hello", expectedCode: http.StatusOK, expectedBody: "hello"},
{name: "triple slash", input: "///hello", expectedCode: http.StatusOK, expectedBody: "hello"},
{name: "multiple slashes in middle", input: "/api//v1///users", expectedCode: http.StatusOK, expectedBody: "users"},
{name: "current directory dot", input: "/.", expectedCode: http.StatusNotFound, expectedBody: "404 page not found\n"},
{name: "parent directory", input: "/..", expectedCode: http.StatusNotFound, expectedBody: "404 page not found\n"},
{name: "relative path no leading slash", input: "/hello", expectedCode: http.StatusOK, expectedBody: "hello"},
{name: "parent directory traversal", input: "/foo/../bar", expectedCode: http.StatusOK, expectedBody: "bar"},
{name: "parent directory with relative path", input: "/../hello", expectedCode: http.StatusOK, expectedBody: "hello"},
{name: "root path", input: "/", expectedCode: http.StatusNotFound, expectedBody: "404 page not found\n"},
{name: "empty path", input: "/", expectedCode: http.StatusNotFound, expectedBody: "404 page not found\n"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, tc.input, http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, tc.expectedCode, rec.Code, "Status code mismatch")
assert.Equal(t, tc.expectedBody, rec.Body.String(), "Response body mismatch")
})
}
}
// TestRouter_DoubleSlashPath_POST verifies that POST requests with double slashes
// are normalized and routed correctly to the POST handler.
func TestRouter_DoubleSlashPath_POST(t *testing.T) {
router := NewRouter()
getHandlerCalled := false
postHandlerCalled := false
// Register both GET and POST handlers for /hello
router.Add(http.MethodGet, "/hello", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
getHandlerCalled = true
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("GET handler"))
}))
router.Add(http.MethodPost, "/hello", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
postHandlerCalled = true
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("POST handler"))
}))
tests := []struct {
desc string
path string
}{
{desc: "POST request to /hello", path: "/hello"},
{desc: "POST request to //hello", path: "//hello"},
{desc: "POST request to ////hello", path: "////hello"},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
getHandlerCalled = false
postHandlerCalled = false
req := httptest.NewRequest(http.MethodPost, tc.path, http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code, "Status code mismatch")
assert.True(t, postHandlerCalled, "POST handler should be called")
assert.False(t, getHandlerCalled, "GET handler should NOT be called")
assert.Equal(t, "POST handler", rec.Body.String(), "Response body mismatch")
assert.Empty(t, rec.Header().Get("Location"), "No redirect should be issued")
})
}
}
func Test_StaticFileServing_Static(t *testing.T) {
tempDir := t.TempDir()
testCases := []struct {
name string
setupFiles func() error
path string
staticServerPath string
expectedCode int
expectedBody string
}{
{
name: "Serve existing file from /static",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("Hello, World!"), 0600)
},
path: "/static/test.txt",
staticServerPath: "/static",
expectedCode: http.StatusOK,
expectedBody: "Hello, World!",
},
{
name: "Serve existing file from /",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("Hello, Root!"), 0600)
},
path: "/test.txt",
staticServerPath: "/",
expectedCode: http.StatusOK,
expectedBody: "Hello, Root!",
},
{
name: "Serve existing file from /public",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, "test.txt"), []byte("Hello, Public!"), 0600)
},
path: "/public/test.txt",
staticServerPath: "/public",
expectedCode: http.StatusOK,
expectedBody: "Hello, Public!",
},
{
name: "Serve 404.html for non-existent file",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, "404.html"), []byte("404 Not Found"), 0600)
},
path: "/static/nonexistent.html",
staticServerPath: "/static",
expectedCode: http.StatusNotFound,
expectedBody: "404 Not Found",
},
{
name: "Serve default 404 message when 404.html is missing",
setupFiles: func() error {
return os.Remove(filepath.Join(tempDir, "404.html"))
},
path: "/static/nonexistent.html",
staticServerPath: "/static",
expectedCode: http.StatusNotFound,
expectedBody: "404 Not Found",
},
{
name: "Access forbidden OpenAPI JSON",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, DefaultSwaggerFileName), []byte(`{"openapi": "3.0.0"}`), 0600)
},
path: "/static/openapi.json",
staticServerPath: "/static",
expectedCode: http.StatusForbidden,
expectedBody: "403 Forbidden",
},
{
name: "Serving File with no Read permission",
setupFiles: func() error {
return os.WriteFile(filepath.Join(tempDir, "restricted.txt"), []byte("Restricted content"), 0000)
},
path: "/static/restricted.txt",
staticServerPath: "/static",
expectedCode: http.StatusInternalServerError,
expectedBody: "500 Internal Server Error",
},
}
runStaticFileTests(t, tempDir, testCases)
}
func Test_isRestrictedFile(t *testing.T) {
tests := []struct {
name string
directoryName string
url string
absPath string
expected bool
}{
{
name: "file inside static directory is not restricted",
directoryName: "/app/public",
url: "/index.html",
absPath: "/app/public/index.html",
expected: false,
},
{
name: "openapi.json inside static directory is restricted",
directoryName: "/app/public",
url: "/openapi.json",
absPath: "/app/public/openapi.json",
expected: true,
},
{
name: "file outside static directory is restricted",
directoryName: "/app/public",
url: "/secret.txt",
absPath: "/app/secret.txt",
expected: true,
},
{
name: "sibling directory with shared prefix is restricted",
directoryName: "/app/public",
url: "/secret.txt",
absPath: "/app/publicother/secret.txt",
expected: true,
},
{
name: "nested file inside static directory is not restricted",
directoryName: "/app/public",
url: "/sub/page.html",
absPath: "/app/public/sub/page.html",
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
cfg := staticFileConfig{directoryName: tc.directoryName}
result := cfg.isRestrictedFile(tc.url, tc.absPath)
assert.Equal(t, tc.expected, result)
})
}
}
func runStaticFileTests(t *testing.T, tempDir string, testCases []struct {
name string
setupFiles func() error
path string
staticServerPath string
expectedCode int
expectedBody string
}) {
t.Helper()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if err := tc.setupFiles(); err != nil {
t.Fatalf("Failed to set up files: %v", err)
}
logger := logging.NewMockLogger(logging.DEBUG)
router := NewRouter()
router.AddStaticFiles(logger, tc.staticServerPath, tempDir)
req := httptest.NewRequest(http.MethodGet, tc.path, http.NoBody)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, tc.expectedCode, w.Code)
assert.Equal(t, tc.expectedBody, strings.TrimSpace(w.Body.String()))
})
}
}
================================================
FILE: pkg/gofr/http_server.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"net/http"
"os"
"time"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/http/middleware"
"gofr.dev/pkg/gofr/websocket"
)
type httpServer struct {
router *gofrHTTP.Router
port int
ws *websocket.Manager
srv *http.Server
certFile string
keyFile string
staticFiles map[string]string
}
var (
errInvalidCertificateFile = errors.New("invalid certificate file")
errInvalidKeyFile = errors.New("invalid key file")
)
func newHTTPServer(c *container.Container, port int, middlewareConfigs middleware.Config) *httpServer {
r := gofrHTTP.NewRouter()
wsManager := websocket.New()
r.Use(
middleware.Tracer,
middleware.Logging(middlewareConfigs.LogProbes, c.Logger),
middleware.CORS(middlewareConfigs.CorsHeaders, r.RegisteredRoutes),
middleware.Metrics(c.Metrics()),
)
return &httpServer{
router: r,
port: port,
ws: wsManager,
}
}
func (s *httpServer) run(c *container.Container) {
// Developer Note:
// WebSocket connections do not inherently support authentication mechanisms.
// It is recommended to authenticate users before upgrading to a WebSocket connection.
// Hence, we are registering websocket middleware here, to ensure that authentication or other
// middleware logic is executed during the initial HTTP handshake request, prior to upgrading
// the connection to WebSocket, if any.
s.router.Use(
middleware.WSHandlerUpgrade(c, s.ws),
)
if s.srv != nil {
c.Logf("Server already running on port: %d", s.port)
return
}
c.Logf("Starting server on port: %d", s.port)
s.srv = &http.Server{
Addr: fmt.Sprintf(":%d", s.port),
Handler: s.router,
ReadHeaderTimeout: 5 * time.Second,
}
// If both certFile and keyFile are provided, validate and run HTTPS server
if s.certFile != "" && s.keyFile != "" {
if err := validateCertificateAndKeyFiles(s.certFile, s.keyFile); err != nil {
c.Error(err)
return
}
// Start HTTPS server with TLS
if err := s.srv.ListenAndServeTLS(s.certFile, s.keyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
c.Errorf("error while listening to https server, err: %v", err)
}
return
}
// If no certFile/keyFile is provided, run the HTTP server
if err := s.srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
c.Errorf("error while listening to http server, err: %v", err)
}
}
func (s *httpServer) Shutdown(ctx context.Context) error {
if s.srv == nil {
return nil
}
return ShutdownWithContext(ctx, func(ctx context.Context) error {
return s.srv.Shutdown(ctx)
}, func() error {
if err := s.srv.Close(); err != nil {
return err
}
return nil
})
}
func validateCertificateAndKeyFiles(certificateFile, keyFile string) error {
if _, err := os.Stat(certificateFile); os.IsNotExist(err) {
return fmt.Errorf("%w : %v", errInvalidCertificateFile, certificateFile)
}
if _, err := os.Stat(keyFile); os.IsNotExist(err) {
return fmt.Errorf("%w : %v", errInvalidKeyFile, keyFile)
}
return nil
}
================================================
FILE: pkg/gofr/http_server_test.go
================================================
package gofr
import (
"context"
"fmt"
"net/http"
"os"
"testing"
"time"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
gofrHTTP "gofr.dev/pkg/gofr/http"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestRun_ServerStartsListening(t *testing.T) {
port := testutil.GetFreePort(t)
// Create a mock router and add a new route
router := &gofrHTTP.Router{}
router.Add(http.MethodGet, "/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// adding registered routes for applying middlewares
var registeredMethods []string
_ = router.Walk(func(route *mux.Route, _ *mux.Router, _ []*mux.Route) error {
met, _ := route.GetMethods()
for _, method := range met {
if !contains(registeredMethods, method) { // Check for uniqueness before adding
registeredMethods = append(registeredMethods, method)
}
}
return nil
})
router.RegisteredRoutes = ®isteredMethods
// Create a mock container
c := container.NewContainer(getConfigs(t))
// Create an instance of httpServer
server := &httpServer{
router: router,
port: port,
}
// Start the server
go server.run(c)
// Wait for the server to start listening
time.Sleep(100 * time.Millisecond)
var netClient = &http.Client{
Timeout: 200 * time.Millisecond,
}
// Send a GET request to the server
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet,
fmt.Sprintf("http://localhost:%d", port), http.NoBody)
resp, err := netClient.Do(req)
require.NoError(t, err, "TEST Failed.\n")
assert.Equal(t, http.StatusOK, resp.StatusCode, "TEST Failed.\n")
resp.Body.Close()
}
func getConfigs(t *testing.T) config.Config {
t.Helper()
var configLocation string
if _, err := os.Stat("./configs"); err == nil {
configLocation = "./configs"
}
return config.NewEnvFile(configLocation, logging.NewLogger(logging.INFO))
}
func TestShutdown_ServerStopsListening(t *testing.T) {
// Create a mock router and add a new route
router := &gofrHTTP.Router{}
router.Add(http.MethodGet, "/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Create a mock container
c := &container.Container{
Logger: logging.NewLogger(logging.INFO),
}
// Create an instance of httpServer
server := &httpServer{
router: router,
port: 8080,
}
// Start the server
go server.run(c)
// Create a context with a timeout to test the shutdown
ctx, cancel := context.WithTimeout(t.Context(), 150*time.Millisecond)
defer cancel()
errChan := make(chan error, 1)
go func() {
time.Sleep(100 * time.Millisecond)
errChan <- server.Shutdown(ctx)
}()
err := <-errChan
require.NoError(t, err, "TEST Failed.\n")
}
func TestShutdown_ServerContextDeadline(t *testing.T) {
// Create a mock router and add a new route
router := &gofrHTTP.Router{}
router.Add(http.MethodGet, "/", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
// Create a mock container
c := &container.Container{
Logger: logging.NewLogger(logging.INFO),
}
// Create an instance of httpServer
server := &httpServer{
router: router,
port: 8080,
}
// Start the server
go server.run(c)
// Create a context with a timeout to test the shutdown
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer cancel()
// Simulate a delay in the shutdown process to trigger context timeout
shutdownCh := make(chan error, 1)
go func() {
time.Sleep(100 * time.Millisecond) // Delay longer than the context timeout
shutdownCh <- server.Shutdown(ctx)
}()
err := <-shutdownCh
require.ErrorIs(t, err, context.DeadlineExceeded, "Expected context deadline exceeded error")
}
func TestValidateCertificateAndKeyFiles_Success(t *testing.T) {
certFile := createTempCertFile(t)
defer os.Remove(certFile)
keyFile := createTempKeyFile(t)
defer os.Remove(keyFile)
err := validateCertificateAndKeyFiles(certFile, keyFile)
require.NoError(t, err, "TestValidateCertificateAndKeyFiles_Success Failed!")
}
func TestValidateCertificateAndKeyFiles_Error(t *testing.T) {
tests := []struct {
name string
certFilePath string
keyFilePath string
expectedError error
}{
{
name: "Certificate file does not exist",
certFilePath: "non-existent-cert.pem",
keyFilePath: createTempKeyFile(t),
expectedError: fmt.Errorf("%w : %v", errInvalidCertificateFile, "non-existent-cert.pem"),
},
{
name: "Key file does not exist",
certFilePath: createTempCertFile(t),
keyFilePath: "non-existent-key.pem",
expectedError: fmt.Errorf("%w : %v", errInvalidKeyFile, "non-existent-key.pem"),
},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := validateCertificateAndKeyFiles(tc.certFilePath, tc.keyFilePath)
require.Equal(t, tc.expectedError.Error(), err.Error(),
"TestValidateCertificateAndKeyFiles_Error [%d] : %v Failed!", i, tc.name)
})
}
}
// Helper function to create a temporary key file.
func createTempKeyFile(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "key-*.pem")
if err != nil {
t.Fatalf("could not create temp key file: %v", err)
}
t.Cleanup(func() {
_ = f.Close()
})
return f.Name()
}
// Helper function to create a temporary certificate file.
func createTempCertFile(t *testing.T) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "cert-*.pem")
if err != nil {
t.Fatalf("could not create temp cert file: %v", err)
}
t.Cleanup(func() {
_ = f.Close()
})
return f.Name()
}
================================================
FILE: pkg/gofr/logging/ctx_logger.go
================================================
package logging
import (
"context"
"go.opentelemetry.io/otel/trace"
)
// ContextLogger is a wrapper around a base Logger that injects the current
// trace ID (if present in the context) into log messages automatically.
//
// It is intended for use within request-scoped contexts where OpenTelemetry
// trace information is available.
type ContextLogger struct {
base Logger
traceID string
}
// NewContextLogger creates a new ContextLogger that wraps the provided base logger
// and automatically appends OpenTelemetry trace information (trace ID) to log output
// when available in the context.
func NewContextLogger(ctx context.Context, base Logger) *ContextLogger {
var traceID string
sc := trace.SpanFromContext(ctx).SpanContext()
if sc.IsValid() {
traceID = sc.TraceID().String()
}
return &ContextLogger{base: base, traceID: traceID}
}
// withTraceInfo appends the trace ID from the context (if available).
// This allows trace IDs to be extracted later during formatting or filtering.
func (l *ContextLogger) withTraceInfo(args ...any) []any {
if l.traceID != "" {
return append(args, map[string]any{"__trace_id__": l.traceID})
}
return args
}
func (l *ContextLogger) logWithTraceID(lf func(args ...any), args ...any) {
lf(l.withTraceInfo(args...)...)
}
func (l *ContextLogger) logWithTraceIDf(lf func(f string, args ...any), f string, args ...any) {
lf(f, l.withTraceInfo(args...)...)
}
func (l *ContextLogger) Debug(args ...any) { l.logWithTraceID(l.base.Debug, args...) }
func (l *ContextLogger) Debugf(f string, args ...any) { l.logWithTraceIDf(l.base.Debugf, f, args...) }
func (l *ContextLogger) Log(args ...any) { l.logWithTraceID(l.base.Log, args...) }
func (l *ContextLogger) Logf(f string, args ...any) { l.logWithTraceIDf(l.base.Logf, f, args...) }
func (l *ContextLogger) Info(args ...any) { l.logWithTraceID(l.base.Info, args...) }
func (l *ContextLogger) Infof(f string, args ...any) { l.logWithTraceIDf(l.base.Infof, f, args...) }
func (l *ContextLogger) Notice(args ...any) { l.logWithTraceID(l.base.Notice, args...) }
func (l *ContextLogger) Noticef(f string, args ...any) { l.logWithTraceIDf(l.base.Noticef, f, args...) }
func (l *ContextLogger) Warn(args ...any) { l.logWithTraceID(l.base.Warn, args...) }
func (l *ContextLogger) Warnf(f string, args ...any) { l.logWithTraceIDf(l.base.Warnf, f, args...) }
func (l *ContextLogger) Error(args ...any) { l.logWithTraceID(l.base.Error, args...) }
func (l *ContextLogger) Errorf(f string, args ...any) { l.logWithTraceIDf(l.base.Errorf, f, args...) }
func (l *ContextLogger) Fatal(args ...any) { l.logWithTraceID(l.base.Fatal, args...) }
func (l *ContextLogger) Fatalf(f string, args ...any) { l.logWithTraceIDf(l.base.Fatalf, f, args...) }
func (l *ContextLogger) ChangeLevel(level Level) { l.base.ChangeLevel(level) }
================================================
FILE: pkg/gofr/logging/ctx_logger_test.go
================================================
package logging
import (
"bytes"
"context"
"encoding/json"
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace"
)
// mockLogger is a simple implementation of Logger interface for testing.
type mockLogger struct {
logs []logEntry
}
func (m *mockLogger) Debug(args ...any) {
m.logs = append(m.logs, logEntry{Level: DEBUG, Message: args})
}
func (m *mockLogger) Debugf(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: DEBUG, Message: format})
}
func (m *mockLogger) Log(args ...any) { m.logs = append(m.logs, logEntry{Level: INFO, Message: args}) }
func (m *mockLogger) Logf(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: INFO, Message: format})
}
func (m *mockLogger) Info(args ...any) { m.logs = append(m.logs, logEntry{Level: INFO, Message: args}) }
func (m *mockLogger) Infof(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: INFO, Message: format})
}
func (m *mockLogger) Notice(args ...any) {
m.logs = append(m.logs, logEntry{Level: NOTICE, Message: args})
}
func (m *mockLogger) Noticef(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: NOTICE, Message: format})
}
func (m *mockLogger) Warn(args ...any) { m.logs = append(m.logs, logEntry{Level: WARN, Message: args}) }
func (m *mockLogger) Warnf(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: WARN, Message: format})
}
func (m *mockLogger) Error(args ...any) {
m.logs = append(m.logs, logEntry{Level: ERROR, Message: args})
}
func (m *mockLogger) Errorf(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: ERROR, Message: format})
}
func (m *mockLogger) Fatal(args ...any) {
m.logs = append(m.logs, logEntry{Level: FATAL, Message: args})
}
func (m *mockLogger) Fatalf(format string, _ ...any) {
m.logs = append(m.logs, logEntry{Level: FATAL, Message: format})
}
func (*mockLogger) ChangeLevel(_ Level) {}
// mockTracerProvider creates a context with a valid trace ID for testing.
func mockTracedContext() (ctx context.Context, id string) {
// Create a testing trace ID.
traceID := trace.TraceID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
spanID := trace.SpanID{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
ctx = context.Background()
sc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: traceID,
SpanID: spanID,
TraceFlags: trace.FlagsSampled,
})
// Create a new context with the SpanContext
ctx = trace.ContextWithSpanContext(ctx, sc)
return ctx, traceID.String()
}
func TestNewContextLogger(t *testing.T) {
baseLogger := &mockLogger{}
ctx := t.Context()
ctxLogger := NewContextLogger(ctx, baseLogger)
assert.Equal(t, baseLogger, ctxLogger.base)
}
func TestContextLogger_WithTraceInfo_NoTraceID(t *testing.T) {
baseLogger := &mockLogger{}
ctx := t.Context()
ctxLogger := NewContextLogger(ctx, baseLogger)
args := []any{"test message"}
result := ctxLogger.withTraceInfo(args...)
assert.Equal(t, args, result)
assert.Len(t, result, 1)
}
func TestContextLogger_WithTraceInfo_WithTraceID(t *testing.T) {
baseLogger := &mockLogger{}
ctx, expectedTraceID := mockTracedContext()
ctxLogger := NewContextLogger(ctx, baseLogger)
args := []any{"test message"}
result := ctxLogger.withTraceInfo(args...)
assert.Len(t, result, 2)
traceMap, ok := result[1].(map[string]any)
require.True(t, ok, "Expected a map with trace ID")
traceID, ok := traceMap["__trace_id__"].(string)
require.True(t, ok, "Expected a string trace ID")
assert.Equal(t, expectedTraceID, traceID)
}
func TestContextLogger_LoggingMethods_NoTrace(t *testing.T) {
baseLogger := &mockLogger{}
ctx := t.Context()
ctxLogger := NewContextLogger(ctx, baseLogger)
ctxLogger.Debug("debug message")
ctxLogger.Debugf("debug format %s", "message")
ctxLogger.Info("info message")
ctxLogger.Infof("info format %s", "message")
ctxLogger.Log("log message")
ctxLogger.Logf("log format %s", "message")
ctxLogger.Notice("notice message")
ctxLogger.Noticef("notice format %s", "message")
ctxLogger.Warn("warn message")
ctxLogger.Warnf("warn format %s", "message")
ctxLogger.Error("error message")
ctxLogger.Errorf("error format %s", "message")
assert.Len(t, baseLogger.logs, 12)
}
func TestContextLogger_LoggingMethods_WithTrace(t *testing.T) {
baseLogger := &mockLogger{}
ctx, expectedTraceID := mockTracedContext()
ctxLogger := NewContextLogger(ctx, baseLogger)
ctxLogger.Info("info message")
ctxLogger.Error("error message")
require.Len(t, baseLogger.logs, 2)
infoMsg, ok := baseLogger.logs[0].Message.([]any)
require.True(t, ok, "Expected message to be []any")
require.Len(t, infoMsg, 2)
traceMap, ok := infoMsg[1].(map[string]any)
require.True(t, ok, "Expected a map with trace ID")
traceID, ok := traceMap["__trace_id__"].(string)
require.True(t, ok, "Expected a string trace ID")
assert.Equal(t, expectedTraceID, traceID)
errorMsg, ok := baseLogger.logs[1].Message.([]any)
require.True(t, ok, "Expected message to be []any")
require.Len(t, errorMsg, 2)
traceMap, ok = errorMsg[1].(map[string]any)
require.True(t, ok, "Expected a map with trace ID")
traceID, ok = traceMap["__trace_id__"].(string)
require.True(t, ok, "Expected a string trace ID")
assert.Equal(t, expectedTraceID, traceID)
}
func TestContextLogger_Integration(t *testing.T) {
buf := &bytes.Buffer{}
realLogger := &logger{
level: DEBUG,
normalOut: buf,
errorOut: buf,
isTerminal: false,
lock: make(chan struct{}, 1),
}
ctx, expectedTraceID := mockTracedContext()
ctxLogger := NewContextLogger(ctx, realLogger)
ctxLogger.Info("test message")
var logData map[string]any
err := json.NewDecoder(bytes.NewReader(buf.Bytes())).Decode(&logData)
require.NoError(t, err)
traceID, ok := logData["trace_id"].(string)
require.True(t, ok, "Expected trace_id to be a string in log output")
assert.Equal(t, expectedTraceID, traceID)
message, ok := logData["message"].(string)
assert.True(t, ok, "Expected message to be a string")
assert.Equal(t, "test message", message)
level, ok := logData["level"].(string)
assert.True(t, ok, "Expected level to be a string")
assert.Equal(t, "INFO", level)
}
func TestContextLogger_ChangeLevel(t *testing.T) {
baseLogger := &logger{
level: INFO,
normalOut: io.Discard,
errorOut: io.Discard,
isTerminal: false,
lock: make(chan struct{}, 1),
}
ctx := t.Context()
ctxLogger := NewContextLogger(ctx, baseLogger)
ctxLogger.ChangeLevel(DEBUG)
assert.Equal(t, DEBUG, baseLogger.level)
}
================================================
FILE: pkg/gofr/logging/level.go
================================================
// Package logging provides logging functionalities for GoFr applications.
package logging
import (
"bytes"
"strings"
)
// Level represents different logging levels.
type Level int
const (
DEBUG Level = iota + 1
INFO
NOTICE
WARN
ERROR
FATAL
)
// String constants for logging levels.
const (
levelDEBUG = "DEBUG"
levelINFO = "INFO"
levelNOTICE = "NOTICE"
levelWARN = "WARN"
levelERROR = "ERROR"
levelFATAL = "FATAL"
)
// String returns the string representation of the log level.
func (l Level) String() string {
switch l {
case DEBUG:
return levelDEBUG
case INFO:
return levelINFO
case NOTICE:
return levelNOTICE
case WARN:
return levelWARN
case ERROR:
return levelERROR
case FATAL:
return levelFATAL
default:
return ""
}
}
//nolint:mnd // Color codes are sent as numbers
func (l Level) color() uint {
switch l {
case ERROR, FATAL:
return 160
case WARN, NOTICE:
return 220
case INFO:
return 6
case DEBUG:
return 8
default:
return 37
}
}
func (l Level) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(l.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}
// GetLevelFromString converts a string to a logging level.
func GetLevelFromString(level string) Level {
switch strings.ToUpper(level) {
case levelDEBUG:
return DEBUG
case levelINFO:
return INFO
case levelNOTICE:
return NOTICE
case levelWARN:
return WARN
case levelERROR:
return ERROR
case levelFATAL:
return FATAL
default:
return INFO
}
}
================================================
FILE: pkg/gofr/logging/level_test.go
================================================
package logging
import (
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestLevelString(t *testing.T) {
tests := []struct {
level Level
expectedString string
}{
{DEBUG, levelDEBUG},
{INFO, levelINFO},
{NOTICE, levelNOTICE},
{WARN, levelWARN},
{ERROR, levelERROR},
{FATAL, levelFATAL},
{Level(99), ""}, // Test default case
}
for i, tc := range tests {
assert.Equal(t, tc.expectedString, tc.level.String(), "TEST[%d], Failed.\n", i)
}
}
func TestLevelColor(t *testing.T) {
tests := []struct {
level Level
expectedColor uint
}{
{ERROR, 160},
{FATAL, 160},
{WARN, 220},
{NOTICE, 220},
{INFO, 6},
{DEBUG, 8},
{Level(99), 37}, // Test default case
}
for i, tc := range tests {
assert.Equal(t, tc.expectedColor, tc.level.color(), "TEST[%d], Failed.", i)
}
}
func TestGetLevelFromString(t *testing.T) {
tests := []struct {
desc string
input string
expected Level
}{
{
desc: "DebugLevel",
input: "DEBUG",
expected: DEBUG,
},
{
desc: "InfoLevel",
input: "INFO",
expected: INFO,
},
{
desc: "NoticeLevel",
input: "NOTICE",
expected: NOTICE,
},
{
desc: "WarnLevel",
input: "WARN",
expected: WARN,
},
{
desc: "ErrorLevel",
input: "ERROR",
expected: ERROR,
},
{
desc: "FatalLevel",
input: "FATAL",
expected: FATAL,
},
{
desc: "DefaultLevel",
input: "UNKNOWN",
expected: INFO,
},
}
for i, tc := range tests {
actual := GetLevelFromString(tc.input)
assert.Equal(t, tc.expected, actual, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_changeLevel(t *testing.T) {
l := logger{
level: INFO,
normalOut: os.Stdout,
errorOut: os.Stderr,
isTerminal: false,
}
l.ChangeLevel(ERROR)
assert.Equal(t, ERROR, l.level, "Test_changeLevel failed! expected level to be error ")
}
================================================
FILE: pkg/gofr/logging/logger.go
================================================
package logging
import (
"encoding/json"
"fmt"
"io"
"os"
"time"
"golang.org/x/term"
"gofr.dev/pkg/gofr/version"
)
const fileMode = 0644
// PrettyPrint defines an interface for objects that can render
// themselves in a human-readable format to the provided writer.
type PrettyPrint interface {
PrettyPrint(writer io.Writer)
}
// Logger defines the interface for structured logging across
// different log levels such as Debug, Info, Warn, Error, and Fatal.
// It allows formatted logs and log level control.
type Logger interface {
Debug(args ...any)
Debugf(format string, args ...any)
Log(args ...any)
Logf(format string, args ...any)
Info(args ...any)
Infof(format string, args ...any)
Notice(args ...any)
Noticef(format string, args ...any)
Warn(args ...any)
Warnf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
ChangeLevel(level Level)
}
type logger struct {
level Level
normalOut io.Writer
errorOut io.Writer
isTerminal bool
lock chan struct{}
}
type logEntry struct {
Level Level `json:"level"`
Time time.Time `json:"time"`
Message any `json:"message"`
TraceID string `json:"trace_id,omitempty"`
GofrVersion string `json:"gofrVersion"`
}
func (l *logger) logf(level Level, format string, args ...any) {
if level < l.level {
return
}
out := l.normalOut
if level >= ERROR {
out = l.errorOut
}
entry := logEntry{
Level: level,
Time: time.Now(),
GofrVersion: version.Framework,
}
traceID, filteredArgs := extractTraceIDAndFilterArgs(args)
entry.TraceID = traceID
switch {
case len(filteredArgs) == 1 && format == "":
entry.Message = filteredArgs[0]
case len(filteredArgs) != 1 && format == "":
entry.Message = filteredArgs
case format != "":
entry.Message = fmt.Sprintf(format, filteredArgs...)
}
if l.isTerminal {
l.prettyPrint(&entry, out)
} else {
_ = json.NewEncoder(out).Encode(entry)
}
}
func (l *logger) Debug(args ...any) {
l.logf(DEBUG, "", args...)
}
func (l *logger) Debugf(format string, args ...any) {
l.logf(DEBUG, format, args...)
}
func (l *logger) Info(args ...any) {
l.logf(INFO, "", args...)
}
func (l *logger) Infof(format string, args ...any) {
l.logf(INFO, format, args...)
}
func (l *logger) Notice(args ...any) {
l.logf(NOTICE, "", args...)
}
func (l *logger) Noticef(format string, args ...any) {
l.logf(NOTICE, format, args...)
}
func (l *logger) Warn(args ...any) {
l.logf(WARN, "", args...)
}
func (l *logger) Warnf(format string, args ...any) {
l.logf(WARN, format, args...)
}
func (l *logger) Log(args ...any) {
l.logf(INFO, "", args...)
}
func (l *logger) Logf(format string, args ...any) {
l.logf(INFO, format, args...)
}
func (l *logger) Error(args ...any) {
l.logf(ERROR, "", args...)
}
func (l *logger) Errorf(format string, args ...any) {
l.logf(ERROR, format, args...)
}
func (l *logger) Fatal(args ...any) {
l.logf(FATAL, "", args...)
// Flush output before exiting
if f, ok := l.errorOut.(*os.File); ok {
_ = f.Sync() // Ignore sync error as we're about to exit
}
//nolint:revive // exit status is 1 as it denotes failure as signified by Fatal log
os.Exit(1)
}
func (l *logger) Fatalf(format string, args ...any) {
l.logf(FATAL, format, args...)
// Flush output before exiting
if f, ok := l.errorOut.(*os.File); ok {
_ = f.Sync() // Ignore sync error as we're about to exit
}
//nolint:revive // exit status is 1 as it denotes failure as signified by Fatal log
os.Exit(1)
}
func (l *logger) prettyPrint(e *logEntry, out io.Writer) {
// Note: we need to lock the pretty print as printing to standard output not concurrency safe
// the logs when printed in go routines were getting misaligned since we are achieving
// a single line of log, in 2 separate statements which caused the misalignment.
l.lock <- struct{}{} // Acquire the channel's lock
defer func() {
<-l.lock // Release the channel's token
}()
// Pretty printing if the message interface defines a method PrettyPrint else print the log message
// This decouples the logger implementation from its usage
fmt.Fprintf(out, "\u001B[38;5;%dm%s\u001B[0m [%s]", e.Level.color(), e.Level.String()[0:4], e.Time.Format(time.TimeOnly))
if e.TraceID != "" {
fmt.Fprintf(out, " \u001B[38;5;8m%s\u001B[0m", e.TraceID)
}
fmt.Fprint(out, " ")
// Print the message
if fn, ok := e.Message.(PrettyPrint); ok {
fn.PrettyPrint(out)
} else {
fmt.Fprintf(out, "%v\n", e.Message)
}
}
// NewLogger creates a new Logger instance configured with the given log level.
// Logs will be printed to stdout and stderr depending on the level.
func NewLogger(level Level) Logger {
l := &logger{
normalOut: os.Stdout,
errorOut: os.Stderr,
lock: make(chan struct{}, 1),
}
l.level = level
l.isTerminal = checkIfTerminal(l.normalOut)
return l
}
// NewFileLogger creates a new Logger instance that writes logs to the specified file.
// If the file cannot be opened or created, logs are discarded.
func NewFileLogger(path string) Logger {
l := &logger{
normalOut: io.Discard,
errorOut: io.Discard,
}
if path == "" {
return l
}
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, fileMode)
if err != nil {
return l
}
l.normalOut = f
l.errorOut = f
return l
}
func checkIfTerminal(w io.Writer) bool {
// Force JSON output in test environments
if os.Getenv("GOFR_EXITER") == "1" {
return false
}
switch v := w.(type) {
case *os.File:
return term.IsTerminal(int(v.Fd()))
default:
return false
}
}
// ChangeLevel changes the log level of the logger.
// This allows dynamic adjustment of the logging verbosity.
func (l *logger) ChangeLevel(level Level) {
l.level = level
}
// LogLevelResponder provides a method to get the log level.
type LogLevelResponder interface {
LogLevel() Level
}
// GetLogLevelForError extracts the log level from an error if it implements LogLevelResponder.
// If the error does not implement this interface, it defaults to ERROR level.
// This is useful for determining the appropriate log level when handling errors.
func GetLogLevelForError(err error) Level {
level := ERROR
if e, ok := err.(LogLevelResponder); ok {
level = e.LogLevel()
}
return level
}
// extractTraceIDAndFilterArgs checks if any of the arguments contain a trace ID
// under the key "__trace_id__" and returns the extracted trace ID along with
// the remaining arguments excluding the trace metadata.
func extractTraceIDAndFilterArgs(args []any) (traceID string, filtered []any) {
filtered = make([]any, 0, len(args))
for _, arg := range args {
if m, ok := arg.(map[string]any); ok {
if tid, exists := m["__trace_id__"].(string); exists && traceID == "" {
traceID = tid
continue
}
}
filtered = append(filtered, arg)
}
return traceID, filtered
}
================================================
FILE: pkg/gofr/logging/logger_test.go
================================================
package logging
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/term"
"gofr.dev/pkg/gofr/testutil"
)
func TestLogger_LevelInfo(t *testing.T) {
printLog := func() {
logger := NewLogger(INFO)
logger.Debug("Test Debug Log")
logger.Info("Test Info Log")
logger.Error("Test Error Log")
}
infoLog := testutil.StdoutOutputForFunc(printLog)
errLog := testutil.StderrOutputForFunc(printLog)
assertMessageInJSONLog(t, infoLog, "Test Info Log")
assertMessageInJSONLog(t, errLog, "Test Error Log")
if strings.Contains(infoLog, "DEBUG") {
t.Errorf("TestLogger_LevelInfo Failed. DEBUG log not expected ")
}
}
func TestLogger_LevelError(t *testing.T) {
printLog := func() {
logger := NewLogger(ERROR)
logger.Logf("%s", "Test Log")
logger.Debugf("%s", "Test Debug Log")
logger.Infof("%s", "Test Info Log")
logger.Errorf("%s", "Test Error Log")
}
infoLog := testutil.StdoutOutputForFunc(printLog)
errLog := testutil.StderrOutputForFunc(printLog)
assert.Empty(t, infoLog) // Since log level is ERROR we will not get any INFO logs.
assertMessageInJSONLog(t, errLog, "Test Error Log")
}
func TestLogger_LevelDebug(t *testing.T) {
printLog := func() {
logger := NewLogger(DEBUG)
logger.Logf("Test Log")
logger.Debug("Test Debug Log")
logger.Info("Test Info Log")
logger.Error("Test Error Log")
}
infoLog := testutil.StdoutOutputForFunc(printLog)
errLog := testutil.StderrOutputForFunc(printLog)
if !(strings.Contains(infoLog, "DEBUG") && strings.Contains(infoLog, "INFO")) {
// Debug Log Level will contain all types of logs i.e. DEBUG, INFO and ERROR
t.Errorf("TestLogger_LevelDebug Failed!")
}
assertMessageInJSONLog(t, errLog, "Test Error Log")
}
func TestLogger_LevelNotice(t *testing.T) {
printLog := func() {
logger := NewLogger(NOTICE)
logger.Log("Test Log")
logger.Debug("Test Debug Log")
logger.Info("Test Info Log")
logger.Notice("Test Notice Log")
logger.Error("Test Error Log")
}
infoLog := testutil.StdoutOutputForFunc(printLog)
errLog := testutil.StderrOutputForFunc(printLog)
if strings.Contains(infoLog, "DEBUG") || strings.Contains(infoLog, "INFO") {
// Notice Log Level will not contain DEBUG and INFO logs
t.Errorf("TestLogger_LevelDebug Failed!")
}
assertMessageInJSONLog(t, errLog, "Test Error Log")
}
func TestLogger_LevelWarn(t *testing.T) {
printLog := func() {
logger := NewLogger(WARN)
logger.Debug("Test Debug Log")
logger.Info("Test Info Log")
logger.Notice("Test Notice Log")
logger.Warn("Test Warn Log")
logger.Error("Test Error Log")
}
infoLog := testutil.StdoutOutputForFunc(printLog)
errLog := testutil.StderrOutputForFunc(printLog)
levels := []Level{DEBUG, INFO, NOTICE}
for i, l := range levels {
assert.NotContainsf(t, infoLog, l.String(), "TEST[%d], Failed.\nunexpected %s log", i, l)
}
assertMessageInJSONLog(t, errLog, "Test Error Log")
}
func TestLogger_LevelFatal(t *testing.T) {
// running the failing part only when a specific env variable is set
if os.Getenv("GOFR_EXITER") == "1" {
logger := NewLogger(FATAL)
logger.Debugf("%s", "Test Debug Log")
logger.Infof("%s", "Test Info Log")
logger.Logf("%s", "Test Log")
logger.Noticef("%s", "Test Notice Log")
logger.Warnf("%s", "Test Warn Log")
logger.Errorf("%s", "Test Error Log")
logger.Fatalf("%s", "Test Fatal Log")
return
}
//nolint:gosec // starting the actual test in a different subprocess
cmd := exec.CommandContext(t.Context(), os.Args[0], "-test.run=TestLogger_LevelFatal")
cmd.Env = append(os.Environ(), "GOFR_EXITER=1")
stderr, err := cmd.StderrPipe()
require.NoError(t, err)
require.NoError(t, cmd.Start())
stderrBytes, err := io.ReadAll(stderr)
require.NoError(t, err)
// Use stderr as the log output (the JSON log is written to stderr)
// Extract only the JSON log line from stderr
stderrOutput := string(stderrBytes)
lines := strings.Split(stderrOutput, "\n")
var log string
for _, line := range lines {
if strings.HasPrefix(line, "{") && strings.Contains(line, "level") {
log = line
break
}
}
levels := []Level{DEBUG, INFO, NOTICE, WARN, ERROR} // levels which should not be present in case of FATAL log_level
for i, l := range levels {
assert.NotContainsf(t, log, l.String(), "TEST[%d], Failed.\nunexpected %s log", i, l)
}
assertMessageInJSONLog(t, log, "Test Fatal Log")
err = cmd.Wait()
var e *exec.ExitError
require.ErrorAs(t, err, &e)
assert.False(t, e.Success())
}
func assertMessageInJSONLog(t *testing.T, logLine, expectation string) {
t.Helper()
// Try to unmarshal the entire log line as JSON first
var l logEntry
_ = json.Unmarshal([]byte(logLine), &l)
if l.Message != expectation {
t.Errorf("Log mismatch. Expected: %s Got: %s", expectation, l.Message)
}
}
func TestCheckIfTerminal(t *testing.T) {
tests := []struct {
desc string
writer io.Writer
isTerminal bool
}{
{"Terminal Writer", os.Stdout, term.IsTerminal(int(os.Stdout.Fd()))},
{"Non-Terminal Writer", os.Stderr, term.IsTerminal(int(os.Stderr.Fd()))},
{"Non-Terminal Writer (not *os.File)", &bytes.Buffer{}, false},
}
for i, tc := range tests {
result := checkIfTerminal(tc.writer)
assert.Equal(t, tc.isTerminal, result, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
func Test_NewSilentLoggerSTDOutput(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
l := NewFileLogger("")
l.Info("Info Logs")
l.Debug("Debug Logs")
l.Notice("Notic Logs")
l.Warn("Warn Logs")
l.Infof("%v Logs", "Infof")
l.Debugf("%v Logs", "Debugf")
l.Noticef("%v Logs", "Noticef")
l.Warnf("%v Logs", "warnf")
})
assert.Empty(t, logs)
}
type mockLog struct {
msg string
}
func (m *mockLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "TEST %s", m.msg)
}
func TestPrettyPrint(t *testing.T) {
m := &mockLog{msg: "mock test log"}
out := &bytes.Buffer{}
l := &logger{isTerminal: true, lock: make(chan struct{}, 1)}
// case PrettyPrint is implemented
l.prettyPrint(&logEntry{
Level: INFO,
Message: m,
}, out)
outputLog := out.String()
expOut := []string{"INFO", "[00:00:00]", "TEST mock test log"}
for _, v := range expOut {
assert.Contains(t, outputLog, v)
}
// case pretty print is not implemented
out.Reset()
l.prettyPrint(&logEntry{
Level: DEBUG,
Message: "test log for normal log",
}, out)
outputLog = out.String()
expOut = []string{"DEBU", "[00:00:00]", "test log for normal log"}
for _, v := range expOut {
assert.Contains(t, outputLog, v)
}
}
func TestNewFileLogger_UnwritablePath(t *testing.T) {
l := NewFileLogger("/root/invalid.log")
logger, ok := l.(*logger)
require.True(t, ok)
assert.Equal(t, io.Discard, logger.normalOut)
assert.Equal(t, io.Discard, logger.errorOut)
}
func TestNewFileLogger_NilPath(t *testing.T) {
l := NewFileLogger("")
logger, ok := l.(*logger)
require.True(t, ok)
assert.Equal(t, io.Discard, logger.normalOut)
assert.Equal(t, io.Discard, logger.errorOut)
}
================================================
FILE: pkg/gofr/logging/mock_logger.go
================================================
package logging
import (
"fmt"
"io"
"os"
)
type MockLogger struct {
level Level
out io.Writer
errOut io.Writer
}
func NewMockLogger(level Level) Logger {
return &MockLogger{
level: level,
out: os.Stdout,
errOut: os.Stderr,
}
}
func (m *MockLogger) logf(level Level, format string, args ...any) {
if level < m.level {
return
}
out := m.out
if level == ERROR {
out = m.errOut
}
var message any
switch {
case len(args) == 1 && format == "":
message = args[0]
case len(args) != 1 && format == "":
message = args
case format != "":
message = fmt.Sprintf(format, args...)
}
fmt.Fprintf(out, "%v\n", message)
}
func (m *MockLogger) Debug(args ...any) {
m.logf(DEBUG, "%v", args...) // Add "%v" formatting directive
}
func (m *MockLogger) Debugf(format string, args ...any) {
m.logf(DEBUG, format, args...)
}
func (m *MockLogger) Info(args ...any) {
m.logf(INFO, "%v", args...) // Add "%v" formatting directive
}
func (m *MockLogger) Infof(format string, args ...any) {
m.logf(INFO, format, args...)
}
func (m *MockLogger) Notice(args ...any) {
m.logf(NOTICE, "%v", args...) // Add "%v" formatting directive
}
func (m *MockLogger) Noticef(format string, args ...any) {
m.logf(NOTICE, format, args...)
}
func (m *MockLogger) Warn(args ...any) {
m.logf(WARN, "%v", args...)
}
func (m *MockLogger) Warnf(format string, args ...any) {
m.logf(WARN, format, args...)
}
func (m *MockLogger) Error(args ...any) {
m.logf(ERROR, "%v", args...)
}
func (m *MockLogger) Errorf(format string, args ...any) {
m.logf(ERROR, format, args...)
}
func (m *MockLogger) Fatal(args ...any) {
m.logf(FATAL, "%v", args...)
}
func (m *MockLogger) Fatalf(format string, args ...any) {
m.logf(FATAL, format, args...)
}
func (m *MockLogger) Log(args ...any) {
m.logf(INFO, "%v", args...)
}
func (m *MockLogger) Logf(format string, args ...any) {
m.logf(INFO, format, args...)
}
func (m *MockLogger) ChangeLevel(level Level) {
m.level = level
}
================================================
FILE: pkg/gofr/logging/mock_logger_test.go
================================================
package logging
import (
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/testutil"
)
func Test_NewMockLogger(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
logger := NewMockLogger(DEBUG)
logger.Info("INFO Log")
logger.Infof("Info Log with Format Value: %v", "infof")
logger.Warn("WARN Log")
logger.Warnf("Warn Log with Format Value: %v", "warnf")
logger.Notice("NOTICE Log")
logger.Noticef("Notice Log with Format Value: %v", "noticef")
logger.Log("Direct Log")
logger.Logf("Direct Log with Format Value: %v", "logf")
logger.Debug("DEBUG Log")
logger.Debugf("Debug Log with Format Value: %v", "debugf")
})
assert.Contains(t, logs, "INFO Log")
assert.Contains(t, logs, "Info Log with Format Value: infof")
assert.Contains(t, logs, "WARN Log")
assert.Contains(t, logs, "Warn Log with Format Value: warnf")
assert.Contains(t, logs, "NOTICE Log")
assert.Contains(t, logs, "Notice Log with Format Value: noticef")
assert.Contains(t, logs, "Direct Log")
assert.Contains(t, logs, "Direct Log with Format Value: logf")
assert.Contains(t, logs, "DEBUG Log")
assert.Contains(t, logs, "Debug Log with Format Value: debugf")
}
func Test_NewMockLoggerErrorLogs(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
logger := NewMockLogger(DEBUG)
logger.Error("ERROR Log")
logger.Errorf("error Log with Format Value: %v", "errorf")
})
assert.Contains(t, logs, "ERROR Log")
assert.Contains(t, logs, "error Log with Format Value: errorf")
}
================================================
FILE: pkg/gofr/logging/remotelogger/dynamic_level_logger.go
================================================
package remotelogger
import (
"context"
"encoding/json"
"fmt"
"io"
"slices"
"sync"
"time"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
)
const (
requestTimeout = 5 * time.Second
// ANSI color codes for terminal output.
colorBlue = 34 // For successful responses (2xx)
colorYellow = 220 // For client errors (4xx)
colorRed = 202 // For server errors (5xx)
)
// httpDebugMsg represents a structured HTTP debug log entry.
// It implements PrettyPrint for colored output and json.Marshaler for JSON logs.
type httpDebugMsg struct {
CorrelationID string `json:"correlation_id"`
ResponseCode int `json:"response_code"`
ResponseTime int64 `json:"response_time_us"`
HTTPMethod string `json:"http_method"`
URI string `json:"uri"`
}
func (m httpDebugMsg) PrettyPrint(w io.Writer) {
colorCode := colorForResponseCode(m.ResponseCode)
fmt.Fprintf(w,
"\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8dμs\u001B[0m %s %s\n",
m.CorrelationID,
colorCode,
m.ResponseCode,
m.ResponseTime,
m.HTTPMethod,
m.URI,
)
}
func (m httpDebugMsg) MarshalJSON() ([]byte, error) {
type alias httpDebugMsg
return json.Marshal(alias(m))
}
// httpLogFilter filters HTTP logs from remote logger to reduce noise.
type httpLogFilter struct {
logging.Logger
mu sync.Mutex
firstSuccessfulHit bool
initLogged bool
}
// Log implements a simplified filtering strategy with consistent formatting.
func (f *httpLogFilter) Log(args ...any) {
if len(args) == 0 || args[0] == nil {
f.Logger.Log(args...)
return
}
// Handle HTTP logs.
httpLog, ok := args[0].(*service.Log)
if !ok {
f.Logger.Log(args...)
return
}
f.handleHTTPLog(httpLog, args)
}
func (f *httpLogFilter) handleHTTPLog(httpLog *service.Log, args []any) {
// Log initialization message if not already logged
f.mu.Lock()
notLoggedYet := !f.initLogged
if notLoggedYet {
f.initLogged = true
}
f.mu.Unlock()
if notLoggedYet {
f.Logger.Infof("Initializing remote logger connection to %s", httpLog.URI)
}
isSuccessful := httpLog.ResponseCode >= 200 && httpLog.ResponseCode < 300
f.mu.Lock()
isFirstHit := !f.firstSuccessfulHit
f.mu.Unlock()
switch {
// First successful hit - log at INFO level
case isSuccessful && isFirstHit:
f.mu.Lock()
f.firstSuccessfulHit = true
f.mu.Unlock()
f.Logger.Log(args...)
// Subsequent successful hits - log at DEBUG level with consistent format
case isSuccessful:
msg := httpDebugMsg{
CorrelationID: httpLog.CorrelationID,
ResponseCode: httpLog.ResponseCode,
ResponseTime: httpLog.ResponseTime,
HTTPMethod: httpLog.HTTPMethod,
URI: httpLog.URI,
}
f.Logger.Debug(msg)
// Error responses - pass through to original logger
default:
f.Logger.Log(args...)
}
}
func colorForResponseCode(status int) int {
switch {
case status >= 200 && status < 300:
return colorBlue
case status >= 400 && status < 500:
return colorYellow
case status >= 500 && status < 600:
return colorRed
}
return 0
}
/*
New creates a new RemoteLogger instance with the provided level, remote configuration URL, and level fetch interval.
The remote configuration URL is expected to be a JSON endpoint that returns the desired log level for the service.
The level fetch interval determines how often the logger checks for updates to the remote configuration.
*/
func New(level logging.Level, remoteConfigURL string, loggerFetchInterval time.Duration) logging.Logger {
l := &remoteLogger{
remoteURL: remoteConfigURL,
Logger: logging.NewLogger(level),
levelFetchInterval: loggerFetchInterval,
currentLevel: level,
}
if remoteConfigURL != "" {
go l.UpdateLogLevel()
}
return l
}
type remoteLogger struct {
remoteURL string
levelFetchInterval time.Duration
mu sync.RWMutex
currentLevel logging.Level
logging.Logger
}
// UpdateLogLevel continuously fetches the log level from the remote configuration URL at the specified interval
// and updates the underlying log level if it has changed.
func (r *remoteLogger) UpdateLogLevel() {
// Create filtered logger with proper initialization
filteredLogger := &httpLogFilter{
Logger: r.Logger,
firstSuccessfulHit: false,
initLogged: false,
}
remoteService := service.NewHTTPService(r.remoteURL, filteredLogger, nil)
r.Infof("Remote logger monitoring initialized with URL: %s, interval: %s",
r.remoteURL, r.levelFetchInterval)
checkAndUpdateLevel := func() {
r.mu.RLock()
currentLevel := r.currentLevel
r.mu.RUnlock()
newLevel, err := fetchAndUpdateLogLevel(remoteService, currentLevel)
if err != nil {
r.Warnf("Failed to fetch log level: %v", err)
return
}
r.mu.Lock()
if r.currentLevel != newLevel {
oldLevel := r.currentLevel
r.currentLevel = newLevel
r.mu.Unlock()
logLevelChange(r, oldLevel, newLevel)
r.ChangeLevel(newLevel)
} else {
r.mu.Unlock()
}
}
// Perform initial check immediately
checkAndUpdateLevel()
// Setup ticker for periodic checks
ticker := time.NewTicker(r.levelFetchInterval)
defer ticker.Stop()
for range ticker.C {
checkAndUpdateLevel()
}
}
// Helper function to log level changes at appropriate level.
func logLevelChange(r *remoteLogger, oldLevel, newLevel logging.Level) {
// Use the higher level to ensure visibility
logLevel := oldLevel
if newLevel > oldLevel {
logLevel = newLevel
}
message := fmt.Sprintf("LOG_LEVEL updated from %v to %v", oldLevel, newLevel)
switch logLevel {
case logging.FATAL:
r.Warnf(message)
case logging.ERROR:
r.Errorf(message)
case logging.WARN:
r.Warnf(message)
case logging.NOTICE:
r.Noticef(message)
case logging.INFO:
r.Infof(message)
case logging.DEBUG:
r.Infof(message) // Using Info for DEBUG to ensure visibility
}
}
func fetchAndUpdateLogLevel(remoteService service.HTTP, currentLevel logging.Level) (logging.Level, error) {
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout) // Set timeout for 5 seconds
defer cancel()
resp, err := remoteService.Get(ctx, "", nil)
if err != nil {
return currentLevel, err
}
defer resp.Body.Close()
var response struct {
Data struct {
ServiceName string `json:"serviceName"`
Level string `json:"logLevel"`
} `json:"data"`
}
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
return currentLevel, err
}
err = json.Unmarshal(responseBody, &response)
if err != nil {
return currentLevel, err
}
logLevels := []string{"DEBUG", "INFO", "NOTICE", "WARN", "ERROR", "FATAL"}
if slices.Contains(logLevels, response.Data.Level) && response.Data.ServiceName != "" {
newLevel := logging.GetLevelFromString(response.Data.Level)
return newLevel, nil
}
return currentLevel, nil
}
================================================
FILE: pkg/gofr/logging/remotelogger/dynamic_level_logger_test.go
================================================
package remotelogger
import (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"os"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/service"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestRemoteLogger_UpdateLevel(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
body := `{ "data": { "serviceName": "test-service","logLevel":"DEBUG" } }`
_, _ = w.Write([]byte(body))
}))
rl := remoteLogger{
remoteURL: mockServer.URL,
levelFetchInterval: 100 * time.Millisecond,
currentLevel: 2,
Logger: logging.NewMockLogger(logging.INFO),
}
go rl.UpdateLogLevel()
time.Sleep(200 * time.Millisecond)
assert.Equal(t, logging.DEBUG, rl.currentLevel)
}
func TestRemoteLogger_UpdateLevelError(t *testing.T) {
rl := remoteLogger{
remoteURL: "invalid url",
levelFetchInterval: 1,
currentLevel: 2,
Logger: logging.NewMockLogger(logging.INFO),
}
go rl.UpdateLogLevel()
time.Sleep(100 * time.Millisecond)
assert.Equal(t, logging.INFO, rl.currentLevel)
}
func Test_fetchAndUpdateLogLevel_InvalidResponse(t *testing.T) {
logger := logging.NewMockLogger(logging.INFO)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
body := `{ "data": { "serviceName": "test-service","logLevel":"TEST" } }`
_, _ = w.Write([]byte(body))
}))
t.Cleanup(mockServer.Close)
remoteService := service.NewHTTPService(mockServer.URL, logger, nil)
level, err := fetchAndUpdateLogLevel(remoteService, logging.DEBUG)
assert.Equal(t, logging.DEBUG, level, "Test_fetchAndUpdateLogLevel_InvalidResponse, Failed.\n")
require.NoError(t, err)
}
func Test_fetchAndUpdateLogLevel_InvalidLogLevel(t *testing.T) {
logger := logging.NewMockLogger(logging.INFO)
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
body := `{
"data": [
{
"invalid"
}
}
]
}`
_, _ = w.Write([]byte(body))
}))
t.Cleanup(mockServer.Close)
remoteService2 := service.NewHTTPService(mockServer.URL, logger, nil)
level, err := fetchAndUpdateLogLevel(remoteService2, logging.DEBUG)
assert.Equal(t, logging.DEBUG, level, "Test_fetchAndUpdateLogLevel_InvalidResponse, Failed.\n")
require.Error(t, err)
}
func TestDynamicLoggerSuccess(t *testing.T) {
// Create a mock server that returns a predefined log level
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
body := `{ "data": { "serviceName": "test-service","logLevel":"DEBUG" } }`
_, _ = w.Write([]byte(body))
}))
t.Cleanup(mockServer.Close)
log := testutil.StdoutOutputForFunc(func() {
// Create a new remote logger with the mock server URL
rl := New(logging.INFO, mockServer.URL, 100*time.Millisecond)
// Wait for the remote logger to update the log level
time.Sleep(200 * time.Millisecond)
// Check if the log level has been updated
rl.Debug("Debug log after log level change")
})
if !strings.Contains(log, "LOG_LEVEL updated from INFO to DEBUG") {
t.Errorf("TestDynamicLoggerSuccess failed! Missing log message about level update")
}
if !strings.Contains(log, "Debug log after log level change") {
t.Errorf("TestDynamicLoggerSuccess failed! missing debug log")
}
}
// TestHTTPLogFilter_NonHTTPLogs tests regular non-HTTP logs are passed through.
func TestHTTPLogFilter_NonHTTPLogs(t *testing.T) {
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{
Logger: testLogger,
}
filter.Log("This is a regular message")
assert.Contains(t, buf.String(), "This is a regular message")
}
// TestHTTPLogFilter_EmptyArgs tests handling of empty arguments.
func TestHTTPLogFilter_EmptyArgs(t *testing.T) {
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{
Logger: testLogger,
}
filter.Log()
// Should not write anything meaningful
assert.Equal(t, "\n", buf.String())
}
// TestHTTPLogFilter_InitAndFirstSuccess tests initialization and first successful hit.
func TestHTTPLogFilter_InitAndFirstSuccess(t *testing.T) {
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{
Logger: testLogger,
}
successLog := &service.Log{
URI: "http://example.com/test",
ResponseCode: 200,
ResponseTime: 150,
HTTPMethod: "GET",
CorrelationID: "test-id-1",
}
filter.Log(successLog)
output := buf.String()
assert.Contains(t, output, "Initializing remote logger connection to http://example.com/test")
assert.Contains(t, output, "test-id-1")
}
// TestHTTPLogFilter_SubsequentSuccess tests subsequent successful HTTP hits.
func TestHTTPLogFilter_SubsequentSuccess(t *testing.T) {
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{
Logger: testLogger,
firstSuccessfulHit: true,
initLogged: true,
}
successLog := &service.Log{
URI: "http://example.com/test2",
ResponseCode: 200,
ResponseTime: 200,
HTTPMethod: "POST",
CorrelationID: "test-id-2",
}
filter.Log(successLog)
output := buf.String()
assert.Contains(t, output, "test-id-2")
assert.Contains(t, output, "POST")
assert.Contains(t, output, "http://example.com/test2")
}
// TestHTTPLogFilter_ErrorLogs tests handling of error HTTP logs.
func TestHTTPLogFilter_ErrorLogs(t *testing.T) {
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{
Logger: testLogger,
firstSuccessfulHit: true,
initLogged: true,
}
errorLog := &service.Log{
URI: "http://example.com/error",
ResponseCode: 500,
ResponseTime: 300,
HTTPMethod: "GET",
CorrelationID: "test-id-3",
}
filter.Log(errorLog)
output := buf.String()
assert.Contains(t, output, "http://example.com/error")
assert.Contains(t, output, "500")
}
func TestHTTPLogFilter_ConcurrentAccess(t *testing.T) {
const (
goroutines = 50
logsPerGoroutine = 20
)
var buf strings.Builder
testLogger := &testBufferLogger{buf: &buf}
filter := &httpLogFilter{Logger: testLogger}
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func(id int) {
defer wg.Done()
for j := 0; j < logsPerGoroutine; j++ {
filter.Log(&service.Log{
CorrelationID: fmt.Sprintf("req-%d-%d", id, j),
URI: "/test",
HTTPMethod: "GET",
ResponseCode: 200,
ResponseTime: int64(j),
})
}
}(i)
}
wg.Wait()
filter.mu.Lock()
assert.True(t, filter.initLogged, "expected initLogged to be true")
filter.mu.Unlock()
assert.NotEmpty(t, buf.String(), "expected logs to be written")
}
func TestRemoteLogger_ConcurrentLevelAccess(t *testing.T) {
var count int32
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
lvl := "DEBUG"
if atomic.AddInt32(&count, 1)%2 == 0 {
lvl = "ERROR"
}
fmt.Fprintf(w, `{"data":{"serviceName":"test-service","logLevel":"%s"}}`, lvl)
}))
t.Cleanup(mockServer.Close)
rl := &remoteLogger{
remoteURL: mockServer.URL,
levelFetchInterval: 5 * time.Millisecond,
currentLevel: logging.INFO,
Logger: logging.NewMockLogger(logging.INFO),
}
// Run UpdateLogLevel in multiple goroutines
for i := 0; i < 5; i++ {
go rl.UpdateLogLevel()
}
time.Sleep(10 * time.Millisecond)
rl.mu.RLock()
defer rl.mu.RUnlock()
assert.NotEqual(t, logging.INFO, rl.currentLevel, "expected level to change")
}
func TestLogLevelChangeToFatal_NoExit(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
body := `{ "data": { "serviceName": "test-service", "logLevel": "FATAL" } }`
_, _ = w.Write([]byte(body))
}))
t.Cleanup(mockServer.Close)
// This test succeeds if it completes without the process exiting
log := testutil.StdoutOutputForFunc(func() {
rl := New(logging.INFO, mockServer.URL, 10*time.Millisecond)
time.Sleep(20 * time.Millisecond)
// If we reach this point, the application didn't exit
// Now check that the logger did change to FATAL level
if remoteLogger, ok := rl.(*remoteLogger); ok {
remoteLogger.mu.RLock()
assert.Equal(t, logging.FATAL, remoteLogger.currentLevel, "Log level should be updated to FATAL")
remoteLogger.mu.RUnlock()
}
})
// Verify the log contains a warning about the level change
assert.Contains(t, log, "LOG_LEVEL updated from INFO to FATAL")
}
func TestHTTPDebugMsg_PrettyPrint(t *testing.T) {
cases := []struct {
name string
msg httpDebugMsg
wantColorSeq string
}{
{
name: "2xx uses blue",
msg: httpDebugMsg{
CorrelationID: "corr-200",
ResponseCode: 200,
ResponseTime: 123,
HTTPMethod: "GET",
URI: "/ok",
},
wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorBlue),
},
{
name: "4xx uses yellow",
msg: httpDebugMsg{
CorrelationID: "corr-404",
ResponseCode: 404,
ResponseTime: 456,
HTTPMethod: "POST",
URI: "/not-found",
},
wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorYellow),
},
{
name: "5xx uses red",
msg: httpDebugMsg{
CorrelationID: "corr-500",
ResponseCode: 500,
ResponseTime: 789,
HTTPMethod: "PUT",
URI: "/err",
},
wantColorSeq: fmt.Sprintf("\u001B[38;5;%dm", colorRed),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var buf bytes.Buffer
tc.msg.PrettyPrint(&buf)
out := buf.String()
// basic content checks
assert.Contains(t, out, tc.msg.CorrelationID)
assert.Contains(t, out, tc.msg.HTTPMethod)
assert.Contains(t, out, tc.msg.URI)
assert.Contains(t, out, fmt.Sprintf("%d", tc.msg.ResponseCode))
// response time should include the microsecond suffix
assert.Contains(t, out, fmt.Sprintf("%dμs", tc.msg.ResponseTime))
// color sequence must be present
assert.Contains(t, out, tc.wantColorSeq, "expected color sequence %q in output: %q", tc.wantColorSeq, out)
// ensure reset code present
assert.Contains(t, out, "\u001B[0m")
})
}
}
================================================
FILE: pkg/gofr/logging/remotelogger/mock_buffer_logger.go
================================================
package remotelogger
import (
"fmt"
"strings"
"gofr.dev/pkg/gofr/logging"
)
// testBufferLogger is a simple logger that writes to a buffer.
// It's primarily used in dynamic_level_logger_test.go for testing HTTPLogFilter and other log-related functionality,
// where direct capture of stdout would be insufficient.
// Unlike testutil.StdoutOutputForFunc, this logger provides isolation for component-specific testing and respects the logging levels.
type testBufferLogger struct {
buf *strings.Builder
level logging.Level
}
func (l *testBufferLogger) Debug(args ...any) {
if l.level <= logging.DEBUG {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Logf(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Info(args ...any) {
if l.level <= logging.INFO {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Notice(args ...any) {
if l.level <= logging.NOTICE {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Warn(args ...any) {
if l.level <= logging.WARN {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Error(args ...any) {
if l.level <= logging.ERROR {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Fatal(args ...any) {
if l.level <= logging.FATAL {
fmt.Fprintln(l.buf, args...)
}
}
func (l *testBufferLogger) Log(args ...any) {
fmt.Fprintln(l.buf, args...)
}
func (l *testBufferLogger) Infof(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Debugf(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Warnf(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Errorf(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Fatalf(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) Noticef(format string, args ...any) {
fmt.Fprintf(l.buf, format+"\n", args...)
}
func (l *testBufferLogger) ChangeLevel(level logging.Level) {
l.level = level
}
================================================
FILE: pkg/gofr/metrics/errors.go
================================================
// Package metrics provides functionalities for instrumenting GoFr applications with metrics.
package metrics
import "fmt"
type metricsAlreadyRegistered struct {
metricsName string
}
type metricsNotRegistered struct {
metricsName string
}
func (e metricsAlreadyRegistered) Error() string {
return fmt.Sprintf("Metrics %v already registered", e.metricsName)
}
func (e metricsNotRegistered) Error() string {
return fmt.Sprintf("Metrics %v is not registered", e.metricsName)
}
================================================
FILE: pkg/gofr/metrics/exporters/exporter.go
================================================
package exporters
import (
"github.com/prometheus/otlptranslator"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/metric"
metricSdk "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
"gofr.dev/pkg/gofr/version"
)
func Prometheus(appName, appVersion string) metric.Meter {
exporter, err := prometheus.New(
prometheus.WithoutTargetInfo(),
prometheus.WithTranslationStrategy(otlptranslator.NoTranslation))
if err != nil {
return nil
}
meter := metricSdk.NewMeterProvider(
metricSdk.WithReader(exporter),
metricSdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(appName),
attribute.String("framework_version", version.Framework),
))).Meter(appName, metric.WithInstrumentationVersion(appVersion))
return meter
}
// TODO : OTLPStdOut and OTLPMetricHTTP are not being used but has to be modified such that user can decide the exporter.
// func OTLPStdOut(appName, appVersion string) metric.Meter {
// exporter, err := stdoutmetric.New()
// if err != nil {
// return nil
// }
//
// meter := metricSdk.NewMeterProvider(
// metricSdk.WithResource(resource.NewSchemaless(semconv.ServiceName(appName))),
// metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter,
// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion))
//
// return meter
// }
//
// func OTLPMetricHTTP(appName, appVersion string) metric.Meter {
// exporter, err := otlpmetrichttp.New(nil,
// otlpmetrichttp.WithInsecure(),
// otlpmetrichttp.WithURLPath("/metrics"),
// otlpmetrichttp.WithEndpoint("localhost:8000"))
// if err != nil {
// return nil
// }
//
// meter := metricSdk.NewMeterProvider(metricSdk.WithReader(metricSdk.NewPeriodicReader(exporter,
// metricSdk.WithInterval(3*time.Second)))).Meter(appName, metric.WithInstrumentationVersion(appVersion))
//
// return meter
// }
================================================
FILE: pkg/gofr/metrics/exporters/telemetry.go
================================================
package exporters
import (
"bytes"
"context"
"encoding/json"
"net/http"
"os"
"runtime"
"time"
"github.com/google/uuid"
"gofr.dev/pkg/gofr/version"
)
const (
defaultTelemetryEndpoint = "https://gofr.dev/telemetry/v1/metrics"
defaultAppName = "gofr-app"
requestTimeout = 10 * time.Second
)
// TelemetryData represents the JSON telemetry payload.
type TelemetryData struct {
Timestamp string `json:"timestamp"`
EventID string `json:"event_id"`
Source string `json:"source"`
ServiceName string `json:"service_name,omitempty"`
ServiceVersion string `json:"service_version,omitempty"`
RawDataSize int `json:"raw_data_size"`
FrameworkVersion string `json:"framework_version,omitempty"`
GoVersion string `json:"go_version,omitempty"`
OS string `json:"os,omitempty"`
Architecture string `json:"architecture,omitempty"`
StartupTime string `json:"startup_time,omitempty"`
}
// SendFrameworkStartupTelemetry sends telemetry data.
func SendFrameworkStartupTelemetry(appName, appVersion string) {
if os.Getenv("GOFR_TELEMETRY") == "false" {
return
}
go sendTelemetryData(appName, appVersion)
}
func sendTelemetryData(appName, appVersion string) {
if appName == "" {
appName = defaultAppName
}
if appVersion == "" {
appVersion = "unknown"
}
now := time.Now().UTC()
data := TelemetryData{
Timestamp: now.Format(time.RFC3339),
EventID: uuid.New().String(),
Source: "gofr-framework",
ServiceName: appName,
ServiceVersion: appVersion,
RawDataSize: 0,
FrameworkVersion: version.Framework,
GoVersion: runtime.Version(),
OS: runtime.GOOS,
Architecture: runtime.GOARCH,
StartupTime: now.Format(time.RFC3339),
}
sendToEndpoint(&data, defaultTelemetryEndpoint)
}
func sendToEndpoint(data *TelemetryData, endpoint string) {
jsonData, err := json.Marshal(data)
if err != nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), requestTimeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(jsonData))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return
}
resp.Body.Close()
}
================================================
FILE: pkg/gofr/metrics/exporters/telemetry_test.go
================================================
package exporters
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"runtime"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/version"
)
func TestSendFrameworkStartupTelemetry_Disabled(t *testing.T) {
t.Setenv("GOFR_TELEMETRY", "true")
requestMade := false
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
requestMade = true
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
SendFrameworkStartupTelemetry("test-app", "1.0.0")
time.Sleep(100 * time.Millisecond)
assert.False(t, requestMade, "Expected no telemetry when disabled")
}
func TestSendFrameworkStartupTelemetry_DefaultValues(t *testing.T) {
var receivedData TelemetryData
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
err = json.Unmarshal(body, &receivedData)
assert.NoError(t, err)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
// Test with empty values to verify defaults.
testSendTelemetryData("", "", server.URL)
time.Sleep(100 * time.Millisecond)
assert.Equal(t, defaultAppName, receivedData.ServiceName)
assert.Equal(t, "unknown", receivedData.ServiceVersion)
assert.Equal(t, "gofr-framework", receivedData.Source)
assert.Equal(t, 0, receivedData.RawDataSize)
}
// Helper function that replicates sendTelemetryData but with configurable endpoint.
func testSendTelemetryData(appName, appVersion, endpoint string) {
if appName == "" {
appName = defaultAppName
}
if appVersion == "" {
appVersion = "unknown"
}
now := time.Now().UTC()
data := TelemetryData{
Timestamp: now.Format(time.RFC3339),
EventID: uuid.New().String(),
Source: "gofr-framework",
ServiceName: appName,
ServiceVersion: appVersion,
RawDataSize: 0,
FrameworkVersion: version.Framework,
GoVersion: runtime.Version(),
OS: runtime.GOOS,
Architecture: runtime.GOARCH,
StartupTime: now.Format(time.RFC3339),
}
sendToEndpoint(&data, endpoint)
}
================================================
FILE: pkg/gofr/metrics/handler.go
================================================
package metrics
import (
"net/http"
"net/http/pprof"
"runtime"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// GetHandler creates a new HTTP handler that serves metrics collected by the provided metrics manager to the '/metrics' route.
func GetHandler(m Manager) http.Handler {
var router = mux.NewRouter()
// Prometheus
router.NewRoute().Methods(http.MethodGet).Path("/metrics").Handler(systemMetricsHandler(m, promhttp.Handler()))
// - /debug/pprof/cmdline
// - /debug/pprof/profile
// - /debug/pprof/symbol
// - /debug/pprof/trace
// - /debug/pprof/ (index)
//
// These endpoints provide various profiling information for the application,
// such as command-line arguments, memory profiles, symbol information, and
// execution traces.
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
router.HandleFunc("/debug/pprof/trace", pprof.Trace)
router.NewRoute().Methods(http.MethodGet).PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
return router
}
func systemMetricsHandler(m Manager, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var stats runtime.MemStats
runtime.ReadMemStats(&stats)
m.SetGauge("app_go_routines", float64(runtime.NumGoroutine()))
m.SetGauge("app_sys_memory_alloc", float64(stats.Alloc))
m.SetGauge("app_sys_total_alloc", float64(stats.TotalAlloc))
m.SetGauge("app_go_numGC", float64(stats.NumGC))
m.SetGauge("app_go_sys", float64(stats.Sys))
next.ServeHTTP(w, r)
})
}
================================================
FILE: pkg/gofr/metrics/handler_test.go
================================================
package metrics
import (
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/metrics/exporters"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func Test_MetricsGetHandler_MetricsNotRegistered(t *testing.T) {
var server *httptest.Server
logs := func() {
manager := NewMetricsManager(exporters.Prometheus("test-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
handler := GetHandler(manager)
server = httptest.NewServer(handler)
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL+"/metrics", http.NoBody)
resp, _ := server.Client().Do(req)
if resp != nil {
defer resp.Body.Close()
}
}
assert.Contains(t, testutil.StderrOutputForFunc(logs), "Metrics app_go_routines is not registered\n"+
"Metrics app_sys_memory_alloc is not registered\n"+"Metrics app_sys_total_alloc is not registered\n"+
"Metrics app_go_numGC is not registered\n"+"Metrics app_go_sys is not registered\n")
}
func Test_MetricsGetHandler_SystemMetricsRegistered(t *testing.T) {
manager := NewMetricsManager(exporters.Prometheus("test-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
// Registering the metrics because the values are being set in the GetHandler function.
manager.NewGauge("app_go_routines", "Number of Go routines running.")
manager.NewGauge("app_sys_memory_alloc", "Number of bytes allocated for heap objects.")
manager.NewGauge("app_sys_total_alloc", "Number of cumulative bytes allocated for heap objects.")
manager.NewGauge("app_go_numGC", "Number of completed Garbage Collector cycles.")
manager.NewGauge("app_go_sys", "Number of total bytes of memory.")
handler := GetHandler(manager)
server := httptest.NewServer(handler)
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL+"/metrics", http.NoBody)
resp, err := server.Client().Do(req)
require.NoError(t, err)
body, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
bodyString := string(body)
assert.Contains(t, bodyString, `app_go_sys{otel_scope_name="test-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"}`)
assert.Contains(t, bodyString, `app_sys_memory_alloc{otel_scope_name="test-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"}`)
assert.Contains(t, bodyString, `app_sys_total_alloc{otel_scope_name="test-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"}`)
assert.Contains(t, bodyString, `app_go_numGC{otel_scope_name="test-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"}`)
}
func Test_MetricsGetHandler_RegisteredProfilingRoutes(t *testing.T) {
manager := NewMetricsManager(exporters.Prometheus("test-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
// Registering the metrics because the values are being set in the GetHandler function.
manager.NewGauge("app_go_routines", "Number of Go routines running.")
manager.NewGauge("app_sys_memory_alloc", "Number of bytes allocated for heap objects.")
manager.NewGauge("app_sys_total_alloc", "Number of cumulative bytes allocated for heap objects.")
manager.NewGauge("app_go_numGC", "Number of completed Garbage Collector cycles.")
manager.NewGauge("app_go_sys", "Number of total bytes of memory.")
handler := GetHandler(manager)
server := httptest.NewServer(handler)
// Test if the expected handlers are registered for the pprof endpoints
expectedRoutes := []string{
"/debug/pprof/",
"/debug/pprof/cmdline",
"/debug/pprof/symbol",
}
for _, route := range expectedRoutes {
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL+route, http.NoBody)
resp, err := server.Client().Do(req)
require.NotNil(t, resp)
require.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, err)
resp.Body.Close()
}
}
================================================
FILE: pkg/gofr/metrics/register.go
================================================
package metrics
import (
"context"
"sync"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)
// Error can also be returned from all the methods, but it is decided not to do so such that to keep the usage clean -
// as any errors are already being logged from here. Otherwise, user would need to check the error every time.
// Manager defines the interface for registering and interacting with different types of metrics
// (counters, up-down counters, histograms, and gauges).
type Manager interface {
NewCounter(name, desc string)
NewUpDownCounter(name, desc string)
NewHistogram(name, desc string, buckets ...float64)
NewGauge(name, desc string)
IncrementCounter(ctx context.Context, name string, labels ...string)
DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
// Logger defines a simple interface for logging messages at different log levels.
type Logger interface {
Error(args ...any)
Errorf(format string, args ...any)
Warn(args ...any)
Warnf(format string, args ...any)
}
type metricsManager struct {
meter metric.Meter
store Store
logger Logger
}
// Developer Note: float64Gauge is used instead of metric.Float64ObservableGauge because we need a synchronous gauge metric
// and otel/metric supports only asynchronous gauge (Float64ObservableGauge).
// And if we use the otel/metric, we would not be able to have support for labels, Hence created a custom type to implement it.
type float64Gauge struct {
observations map[attribute.Set]float64
mu sync.RWMutex
}
// NewMetricsManager creates a new metrics manager instance with the provided metric meter and logger.
func NewMetricsManager(meter metric.Meter, logger Logger) Manager {
return &metricsManager{
meter: meter,
store: newOtelStore(),
logger: logger,
}
}
// Developer Note : we are not checking the name or desc parameter because the OTEL
// package already takes care of the mandatory params and returns the error.
// NewCounter registers a new counter metrics whose values are monotonically increasing
// and cannot decrement.
//
// Usage: m.NewCounter("requests_total", "Total number of requests")
func (m *metricsManager) NewCounter(name, desc string) {
counter, err := m.meter.Int64Counter(name, metric.WithDescription(desc))
if err != nil {
m.logger.Error(err)
return
}
err = m.store.setCounter(name, counter)
if err != nil {
m.logger.Error(err)
}
}
// NewUpDownCounter registers a new UpDown Counter metrics.
//
// Usage:
// m.NewUpDownCounter("active_users", "Number of active users")
func (m *metricsManager) NewUpDownCounter(name, desc string) {
upDownCounter, err := m.meter.Float64UpDownCounter(name, metric.WithDescription(desc))
if err != nil {
m.logger.Error(err)
return
}
err = m.store.setUpDownCounter(name, upDownCounter)
if err != nil {
m.logger.Error(err)
}
}
// NewHistogram registers a new histogram metrics with different buckets.
//
// Usage:
// m.NewHistogram("another_histogram", "Another histogram metric", 0, 10, 100, 1000)
//
// When creating a histogram metric, we can specify custom bucket boundaries to group data points
// into ranges. Buckets represent specific ranges of values. Each value recorded in the histogram
// is placed into the appropriate bucket based on its value compared to the bucket boundaries.
//
// For example, when tracking response times in milliseconds, we might define buckets like [0, 10),
// [10, 100), [100, 1000), [1000, +Inf), where each range represents response times
// within a certain range, and the last bucket includes all values above 1000ms (represented by +Inf,
// which stands for positive infinity).
func (m *metricsManager) NewHistogram(name, desc string, buckets ...float64) {
histogram, err := m.meter.Float64Histogram(name, metric.WithDescription(desc),
metric.WithExplicitBucketBoundaries(buckets...))
if err != nil {
m.logger.Error(err)
return
}
err = m.store.setHistogram(name, histogram)
if err != nil {
m.logger.Error(err)
}
}
// NewGauge registers a new gauge metrics. This metric can set
// the value of metric to a particular value, but it doesn't store the last recorded value for the metrics.
//
// Usage:
// m.NewGauge("memory_usage", "Current memory usage in bytes")
func (m *metricsManager) NewGauge(name, desc string) {
gauge := &float64Gauge{observations: make(map[attribute.Set]float64)}
_, err := m.meter.Float64ObservableGauge(name, metric.WithDescription(desc), metric.WithFloat64Callback(gauge.callbackFunc))
if err != nil {
m.logger.Error(err)
return
}
err = m.store.setGauge(name, gauge)
if err != nil {
m.logger.Error(err)
}
}
// callbackFunc implements the callback function for the underlying asynchronous gauge
// it observes the current state of all previous set() calls.
func (f *float64Gauge) callbackFunc(_ context.Context, o metric.Float64Observer) error {
f.mu.RLock()
defer f.mu.RUnlock()
for attrs, val := range f.observations {
o.Observe(val, metric.WithAttributeSet(attrs))
}
return nil
}
// IncrementCounter increases the specified registered counter metric by 1.
//
// Usage:
//
// // Increment a counter metric without labels
// 1. m.IncrementCounter(ctx, "example_counter")
//
// // Increment a counter metric with labels
// 2. m.IncrementCounter(ctx, "example_counter_with_labels", "label1", "value1", "label2", "value2")
//
// The IncrementCounter method is used to increase the specified counter metric by 1. If the counter metric
// does not exist, an error is logged. Optionally, we can provide labels to associate additional information
// with the counter metric. Labels are provided as key-value pairs where the label name and value alternate.
// For example, "label1", "value1", "label2", "value2". Labels allow us to segment and filter your metrics
// based on different dimensions.
func (m *metricsManager) IncrementCounter(ctx context.Context, name string, labels ...string) {
counter, err := m.store.getCounter(name)
if err != nil {
m.logger.Error(err)
return
}
counter.Add(ctx, 1, metric.WithAttributes(m.getAttributes(name, labels...)...))
}
// DeltaUpDownCounter increases or decreases the last value with the value specified.
//
// Usage:
//
// // Increase the number of active users by 1.5 without any additional labels
// 1. m.DeltaUpDownCounter(ctx, "active_users", 1.5)
//
// // Increase the number of successful logins by 1.5 with labels.
// 2. m.DeltaUpDownCounter(ctx, "successful_logins", 1.5, "label1", "value1", "label2", "value2")
//
// The DeltaUpDownCounter method is used to increase or decrease the last value of the specified UpDown counter metric
// by the given value. For example, we might use this method to track changes in the number of active users or
// successful login attempts. Labels can provide additional context, such as the method and endpoint of the request,
// allowing us to analyze metrics based on different dimensions.
func (m *metricsManager) DeltaUpDownCounter(ctx context.Context, name string, value float64, labels ...string) {
upDownCounter, err := m.store.getUpDownCounter(name)
if err != nil {
m.logger.Error(err)
return
}
upDownCounter.Add(ctx, value, metric.WithAttributes(m.getAttributes(name, labels...)...))
}
// RecordHistogram records the specified value in the respective buckets of the histogram metric.
//
// Usage:
//
// // Record the latency of an API request without any labels.
// 1. m.RecordHistogram(ctx, "api_request_latency", 25.5)
//
// // Record the latency of an API request with labels.
// 2. m.RecordHistogram(ctx, "api_request_latency", 25.5, "label1", "value1", "label2", "value2")
func (m *metricsManager) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
histogram, err := m.store.getHistogram(name)
if err != nil {
m.logger.Error(err)
return
}
histogram.Record(ctx, value, metric.WithAttributes(m.getAttributes(name, labels...)...))
}
// SetGauge gets the value and sets the metric to the specified value.
// Unlike counters, gauges do not track the last value for the metric. This method allows us to
// directly set the value of the gauge to the specified value.
//
// Usage:
// manager.SetGauge("memory_usage", 1024*1024*100)
// // Set memory usage to 100 MB
func (m *metricsManager) SetGauge(name string, value float64, labels ...string) {
gauge, err := m.store.getGauge(name)
if err != nil {
m.logger.Error(err)
return
}
gauge.set(value, attribute.NewSet(m.getAttributes(name, labels...)...))
}
func (f *float64Gauge) set(val float64, attrs attribute.Set) {
f.mu.Lock()
defer f.mu.Unlock()
f.observations[attrs] = val
}
// getAttributes validates the given labels and convert them to corresponding otel attributes.
func (m *metricsManager) getAttributes(name string, labels ...string) []attribute.KeyValue {
labelsCount := len(labels)
if labelsCount%2 != 0 {
m.logger.Warnf("metrics %v label has invalid key-value pairs", name)
}
cardinalityLimit := 20
if labelsCount > cardinalityLimit {
m.logger.Warnf("metrics %v has high cardinality: %v", name, labelsCount)
}
var attributes []attribute.KeyValue
if labels != nil {
for i := 0; i < len(labels)-1; i += 2 {
attributes = append(attributes, attribute.String(labels[i], labels[i+1]))
}
}
return attributes
}
================================================
FILE: pkg/gofr/metrics/register_test.go
================================================
package metrics
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/metrics/exporters"
"gofr.dev/pkg/gofr/testutil"
)
func Test_NewMetricsManagerSuccess(t *testing.T) {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.NewGauge("gauge-test", "this is metric to test gauge")
metrics.NewCounter("counter-test", "this is metric to test counter")
metrics.NewUpDownCounter("up-down-counter", "this is metric to test up-down-counter")
metrics.NewHistogram("histogram-test", "this is metric to test histogram")
metrics.SetGauge("gauge-test", 50)
metrics.IncrementCounter(t.Context(), "counter-test")
metrics.DeltaUpDownCounter(t.Context(), "up-down-counter", 10)
metrics.RecordHistogram(t.Context(), "histogram-test", 1)
server := httptest.NewServer(GetHandler(metrics))
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, server.URL+"/metrics", http.NoBody)
resp, _ := server.Client().Do(req)
body, _ := io.ReadAll(resp.Body)
defer resp.Body.Close()
stringBody := string(body)
assert.Contains(t, stringBody, `otel_scope_name="testing-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"`,
"TEST Failed. service name and version not coming in metrics")
assert.Contains(t, stringBody, `counter_test this is metric to test counter`,
"TEST Failed. counter-test metrics registration failed")
assert.Contains(t, stringBody, `counter_test{otel_scope_name="testing-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"} 1`,
"TEST Failed. counter-test metrics registration failed")
assert.Contains(t, stringBody, `gauge_test this is metric to test gauge`,
"TEST Failed. gauge-test metrics registration failed")
assert.Contains(t, stringBody, `gauge_test{otel_scope_name="testing-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"} 50`,
"TEST Failed. gauge_test metrics value not set")
assert.Contains(t, stringBody, `up_down_counter{otel_scope_name="testing-app",otel_scope_schema_url="",otel_scope_version="v1.0.0"} 10`,
"TEST Failed. up-down-counter metrics value did not reflect")
assert.Contains(t, stringBody, `up_down_counter this is metric to test up-down-counter`,
"TEST Failed. up-down-counter metrics registration failed")
assert.Contains(t, stringBody, `histogram_test this is metric to test histogram`,
"TEST Failed. histogram metrics registration failed")
assert.Contains(t, stringBody,
`histogram_test_bucket{otel_scope_name="testing-app",otel_scope_schema_url="",otel_scope_version="v1.0.0",le="0"} 0`,
"TEST Failed. histogram metrics value did not reflect")
}
func Test_NewMetricsManagerMetricsNotRegistered(t *testing.T) {
logs := func() {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.SetGauge("gauge-test", 50)
metrics.IncrementCounter(t.Context(), "counter-test")
metrics.DeltaUpDownCounter(t.Context(), "up-down-counter", 10)
metrics.RecordHistogram(t.Context(), "histogram-test", 1)
}
log := testutil.StderrOutputForFunc(logs)
assert.Contains(t, log, `Metrics gauge-test is not registered`, "TEST Failed. gauge-test metrics registered")
assert.Contains(t, log, `Metrics counter-test is not registered`, "TEST Failed. counter-test metrics registered")
assert.Contains(t, log, `Metrics up-down-counter is not registered`, "TEST Failed. up-down-counter metrics registered")
assert.Contains(t, log, `Metrics histogram-test is not registered`, "TEST Failed. histogram-test metrics registered")
}
func Test_NewMetricsManagerInvalidMetricsName(t *testing.T) {
logs := func() {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.NewCounter("", "counter metric with empty name")
metrics.NewUpDownCounter("", "up-down-counter metric with empty name")
metrics.NewHistogram("", "histogram metric with empty name")
metrics.NewGauge("", "gauge metric with empty name")
}
log := testutil.StderrOutputForFunc(logs)
assert.Contains(t, log, `invalid instrument name`, "TEST Failed. counter metric with empty name")
assert.Contains(t, log, `invalid instrument name`, "TEST Failed. up-down-counter metric with empty name")
assert.Contains(t, log, `invalid instrument name`, "TEST Failed. histogram metric with empty name")
assert.Contains(t, log, `invalid instrument name`, "TEST Failed. gauge metric with empty name")
}
func Test_NewMetricsManagerDuplicateMetricsRegistration(t *testing.T) {
logs := func() {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.NewGauge("gauge-test", "this is metric to test gauge")
metrics.NewCounter("counter-test", "this is metric to test counter")
metrics.NewUpDownCounter("up-down-counter", "this is metric to test up-down-counter")
metrics.NewHistogram("histogram-test", "this is metric to test histogram")
metrics.NewGauge("gauge-test", "this is metric to test gauge")
metrics.NewCounter("counter-test", "this is metric to test counter")
metrics.NewUpDownCounter("up-down-counter", "this is metric to test up-down-counter")
metrics.NewHistogram("histogram-test", "this is metric to test histogram")
}
log := testutil.StderrOutputForFunc(logs)
assert.Contains(t, log, `Metrics gauge-test already registered`, "TEST Failed. gauge-test metrics not registered")
assert.Contains(t, log, `Metrics counter-test already registered`, "TEST Failed. counter-test metrics not registered")
assert.Contains(t, log, `Metrics up-down-counter already registered`, "TEST Failed. up-down-counter metrics not registered")
assert.Contains(t, log, `Metrics up-down-counter already registered`, "TEST Failed. histogram-test metrics not registered")
}
func Test_NewMetricsManagerInvalidLabelPairErrors(t *testing.T) {
logs := func() {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.NewCounter("counter-test", "this is metric to test counter")
metrics.IncrementCounter(t.Context(), "counter-test",
"label1", "value1", "label2", "value2", "label3")
}
log := testutil.StdoutOutputForFunc(logs)
assert.Contains(t, log, `metrics counter-test label has invalid key-value pairs`, "TEST Failed. Invalid key-value pair for labels")
}
func Test_NewMetricsManagerLabelHighCardinality(t *testing.T) {
logs := func() {
metrics := NewMetricsManager(exporters.Prometheus("testing-app", "v1.0.0"),
logging.NewMockLogger(logging.INFO))
metrics.NewCounter("counter-test", "this is metric to test counter")
metrics.IncrementCounter(t.Context(), "counter-test",
"label1", "value1", "label2", "value2", "label3", "value3", "label4", "value4", "label5", "value5", "label6", "value6",
"label7", "value7", "label8", "value8", "label9", "value9", "label10", "value10", "label11", "value11", "label12", "value12")
}
log := testutil.StdoutOutputForFunc(logs)
assert.Contains(t, log, `metrics counter-test has high cardinality: 24`, "TEST Failed. high cardinality of metrics")
}
================================================
FILE: pkg/gofr/metrics/store.go
================================================
package metrics
import (
"sync"
"go.opentelemetry.io/otel/metric"
)
type store struct {
mu sync.RWMutex
counter map[string]metric.Int64Counter
upDownCounter map[string]metric.Float64UpDownCounter
histogram map[string]metric.Float64Histogram
gauge map[string]*float64Gauge
}
// Store represents a store for registered metrics. It provides methods to retrieve and manage different
// types of metrics (counters, up-down counters, histograms, and gauges).
type Store interface {
getCounter(name string) (metric.Int64Counter, error)
getUpDownCounter(name string) (metric.Float64UpDownCounter, error)
getHistogram(name string) (metric.Float64Histogram, error)
getGauge(name string) (*float64Gauge, error)
setCounter(name string, m metric.Int64Counter) error
setUpDownCounter(name string, m metric.Float64UpDownCounter) error
setHistogram(name string, m metric.Float64Histogram) error
setGauge(name string, m *float64Gauge) error
}
func newOtelStore() Store {
return &store{
counter: make(map[string]metric.Int64Counter),
upDownCounter: make(map[string]metric.Float64UpDownCounter),
histogram: make(map[string]metric.Float64Histogram),
gauge: make(map[string]*float64Gauge),
}
}
func (s *store) getCounter(name string) (metric.Int64Counter, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.counter[name]
if !ok {
return nil, metricsNotRegistered{metricsName: name}
}
return m, nil
}
func (s *store) getUpDownCounter(name string) (metric.Float64UpDownCounter, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.upDownCounter[name]
if !ok {
return nil, metricsNotRegistered{metricsName: name}
}
return m, nil
}
func (s *store) getHistogram(name string) (metric.Float64Histogram, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.histogram[name]
if !ok {
return nil, metricsNotRegistered{metricsName: name}
}
return m, nil
}
func (s *store) getGauge(name string) (*float64Gauge, error) {
s.mu.RLock()
defer s.mu.RUnlock()
m, ok := s.gauge[name]
if !ok {
return m, metricsNotRegistered{metricsName: name}
}
return m, nil
}
func (s *store) setCounter(name string, m metric.Int64Counter) error {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.counter[name]
if !ok {
s.counter[name] = m
return nil
}
return metricsAlreadyRegistered{metricsName: name}
}
func (s *store) setUpDownCounter(name string, m metric.Float64UpDownCounter) error {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.upDownCounter[name]
if !ok {
s.upDownCounter[name] = m
return nil
}
return metricsAlreadyRegistered{metricsName: name}
}
func (s *store) setHistogram(name string, m metric.Float64Histogram) error {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.histogram[name]
if !ok {
s.histogram[name] = m
return nil
}
return metricsAlreadyRegistered{metricsName: name}
}
func (s *store) setGauge(name string, m *float64Gauge) error {
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.gauge[name]
if !ok {
s.gauge[name] = m
return nil
}
return metricsAlreadyRegistered{metricsName: name}
}
================================================
FILE: pkg/gofr/metrics/store_test.go
================================================
package metrics
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric/noop"
)
func TestStore_SetAndGetCounter(t *testing.T) {
store := newOtelStore()
meter := noop.NewMeterProvider().Meter("test")
counter, err := meter.Int64Counter("test-counter")
require.NoError(t, err)
err = store.setCounter("test-counter", counter)
require.NoError(t, err)
got, err := store.getCounter("test-counter")
require.NoError(t, err)
require.Equal(t, counter, got)
}
func TestStore_SetAndGetUpDownCounter(t *testing.T) {
store := newOtelStore()
meter := noop.NewMeterProvider().Meter("test")
udc, err := meter.Float64UpDownCounter("test-updown")
require.NoError(t, err)
err = store.setUpDownCounter("test-updown", udc)
require.NoError(t, err)
got, err := store.getUpDownCounter("test-updown")
require.NoError(t, err)
require.Equal(t, udc, got)
}
func TestStore_SetAndGetHistogram(t *testing.T) {
store := newOtelStore()
meter := noop.NewMeterProvider().Meter("test")
hist, err := meter.Float64Histogram("test-hist")
require.NoError(t, err)
err = store.setHistogram("test-hist", hist)
require.NoError(t, err)
got, err := store.getHistogram("test-hist")
require.NoError(t, err)
require.Equal(t, hist, got)
}
func TestStore_SetAndGetGauge(t *testing.T) {
store := newOtelStore()
g := &float64Gauge{}
err := store.setGauge("test-gauge", g)
require.NoError(t, err)
got, err := store.getGauge("test-gauge")
require.NoError(t, err)
require.Equal(t, g, got)
}
func TestStore_DuplicateMetricRegistration(t *testing.T) {
store := newOtelStore()
meter := noop.NewMeterProvider().Meter("test")
counter, _ := meter.Int64Counter("dup-counter")
_ = store.setCounter("dup-counter", counter)
err := store.setCounter("dup-counter", counter)
require.ErrorContains(t, err, "already registered")
}
func TestStore_GetNonExistentMetric(t *testing.T) {
store := newOtelStore()
_, err := store.getCounter("no-counter")
require.ErrorContains(t, err, "not registered")
_, err = store.getUpDownCounter("no-updown")
require.ErrorContains(t, err, "not registered")
_, err = store.getHistogram("no-hist")
require.ErrorContains(t, err, "not registered")
_, err = store.getGauge("no-gauge")
require.ErrorContains(t, err, "not registered")
}
func TestStore_ConcurrentGaugeSetGet(t *testing.T) {
store := newOtelStore()
g := &float64Gauge{
observations: make(map[attribute.Set]float64),
}
err := store.setGauge("concurrent-gauge", g)
require.NoError(t, err)
var wg sync.WaitGroup
for i := range make([]struct{}, 10) {
wg.Add(1)
go func(val float64) {
defer wg.Done()
g.mu.Lock()
g.observations[attribute.NewSet()] = val
g.mu.Unlock()
}(float64(i))
}
wg.Wait()
got, err := store.getGauge("concurrent-gauge")
require.NoError(t, err)
require.NotNil(t, got)
}
================================================
FILE: pkg/gofr/metrics_server.go
================================================
package gofr
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/metrics"
)
type metricServer struct {
port int
srv *http.Server
}
func newMetricServer(port int) *metricServer {
return &metricServer{port: port}
}
func (m *metricServer) Run(c *container.Container) {
if m != nil {
c.Logf("Starting metrics server on port: %d", m.port)
m.srv = &http.Server{
Addr: fmt.Sprintf(":%d", m.port),
Handler: metrics.GetHandler(c.Metrics()),
ReadHeaderTimeout: 5 * time.Second,
}
err := m.srv.ListenAndServe()
if !errors.Is(err, http.ErrServerClosed) {
c.Errorf("error while listening to metrics server, err: %v", err)
}
}
}
func (m *metricServer) Shutdown(ctx context.Context) error {
if m.srv == nil {
return nil
}
return ShutdownWithContext(ctx, func(ctx context.Context) error {
return m.srv.Shutdown(ctx)
}, nil)
}
================================================
FILE: pkg/gofr/migration/arango.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
// arangoDS is our adapter struct that will implement both interfaces.
type arangoDS struct {
client ArangoDB
}
// arangoMigrator struct remains the same but uses our adapter.
type arangoMigrator struct {
ArangoDB
migrator
}
const (
arangoMigrationDB = "_system"
arangoMigrationCollection = "gofr_migrations"
getLastArangoMigration = `
FOR doc IN gofr_migrations
SORT doc.version DESC
LIMIT 1
RETURN doc.version
`
insertArangoMigrationRecord = `
INSERT {
version: @version,
method: @method,
start_time: @start_time,
duration: @duration
} INTO gofr_migrations
`
)
func (ds arangoDS) CreateDB(ctx context.Context, database string) error {
return ds.client.CreateDB(ctx, database)
}
func (ds arangoDS) DropDB(ctx context.Context, database string) error {
return ds.client.DropDB(ctx, database)
}
func (ds arangoDS) CreateCollection(ctx context.Context, database, collection string, isEdge bool) error {
return ds.client.CreateCollection(ctx, database, collection, isEdge)
}
func (ds arangoDS) DropCollection(ctx context.Context, database, collection string) error {
return ds.client.DropCollection(ctx, database, collection)
}
func (ds arangoDS) CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error {
return ds.client.CreateGraph(ctx, database, graph, edgeDefinitions)
}
func (ds arangoDS) DropGraph(ctx context.Context, database, graph string) error {
return ds.client.DropGraph(ctx, database, graph)
}
func (ds arangoDS) apply(m migrator) migrator {
return arangoMigrator{
ArangoDB: ds,
migrator: m,
}
}
func (am arangoMigrator) checkAndCreateMigrationTable(c *container.Container) error {
err := am.CreateCollection(context.Background(), arangoMigrationDB, arangoMigrationCollection, false)
if err != nil {
c.Debug("Migration collection might already exist:", err)
}
return am.migrator.checkAndCreateMigrationTable(c)
}
func (am arangoMigrator) getLastMigration(c *container.Container) (int64, error) {
var (
lastMigrations []int64
lastMigration int64
)
err := c.ArangoDB.Query(context.Background(), arangoMigrationDB, getLastArangoMigration, nil, &lastMigrations)
if err != nil {
return -1, fmt.Errorf("arangodb: %w", err)
}
if len(lastMigrations) != 0 {
lastMigration = lastMigrations[0]
}
c.Debugf("ArangoDB last migration fetched value is: %v", lastMigration)
lm2, err := am.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (am arangoMigrator) beginTransaction(c *container.Container) transactionData {
data := am.migrator.beginTransaction(c)
c.Debug("ArangoDB migrator begin successfully")
return data
}
func (am arangoMigrator) commitMigration(c *container.Container, data transactionData) error {
bindVars := map[string]any{
"version": data.MigrationNumber,
"method": "UP",
"start_time": data.StartTime,
"duration": time.Since(data.StartTime).Milliseconds(),
}
var result []map[string]any
err := c.ArangoDB.Query(context.Background(), arangoMigrationDB, insertArangoMigrationRecord, bindVars, &result)
if err != nil {
return err
}
c.Debugf("Inserted record for migration %v in ArangoDB gofr_migrations collection", data.MigrationNumber)
return am.migrator.commitMigration(c, data)
}
func (am arangoMigrator) rollback(c *container.Container, data transactionData) {
am.migrator.rollback(c, data)
c.Fatalf("Migration %v failed and rolled back", data.MigrationNumber)
}
func (am arangoMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return am.migrator.lock(ctx, cancel, c, ownerID)
}
func (am arangoMigrator) unlock(c *container.Container, ownerID string) error {
return am.migrator.unlock(c, ownerID)
}
func (arangoMigrator) name() string {
return "ArangoDB"
}
================================================
FILE: pkg/gofr/migration/arango_test.go
================================================
package migration
import (
"context"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func arangoSetup(t *testing.T) (migrator, *container.MockArangoDBProvider, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockArango := mocks.ArangoDB
ds := Datasource{ArangoDB: mockContainer.ArangoDB}
arangoDB := arangoDS{client: mockArango}
migratorWithArango := arangoDB.apply(&ds)
mockContainer.ArangoDB = mockArango
return migratorWithArango, mockArango, mockContainer
}
func Test_ArangoCheckAndCreateMigrationTable(t *testing.T) {
migratorWithArango, mockArango, mockContainer := arangoSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"collection already exists", nil},
}
for i, tc := range testCases {
mockArango.EXPECT().CreateCollection(gomock.Any(), arangoMigrationDB, arangoMigrationCollection, false).Return(tc.err)
err := migratorWithArango.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_ArangoGetLastMigration(t *testing.T) {
migratorWithArango, mockArango, mockContainer := arangoSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 0},
{"query failed", context.DeadlineExceeded, -1},
}
var lastMigrations []int64
for i, tc := range testCases {
mockArango.EXPECT().Query(gomock.Any(), arangoMigrationDB, getLastArangoMigration, nil, &lastMigrations).Return(tc.err)
resp, err := migratorWithArango.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v]\n %v Failed! ", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
}
func Test_ArangoCommitMigration(t *testing.T) {
migratorWithArango, mockArango, mockContainer := arangoSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"insert failed", context.DeadlineExceeded},
}
timeNow := time.Now()
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
for i, tc := range testCases {
bindVars := map[string]any{
"version": td.MigrationNumber,
"method": "UP",
"start_time": td.StartTime,
"duration": time.Since(td.StartTime).Milliseconds(),
}
mockArango.EXPECT().Query(gomock.Any(), arangoMigrationDB, insertArangoMigrationRecord, bindVars, gomock.Any()).Return(tc.err)
err := migratorWithArango.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_ArangoBeginTransaction(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migratorWithArango, _, mockContainer := arangoSetup(t)
migratorWithArango.beginTransaction(mockContainer)
})
assert.Contains(t, logs, "ArangoDB migrator begin successfully")
}
================================================
FILE: pkg/gofr/migration/cassandra.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
type cassandraDS struct {
container.CassandraWithContext
}
type cassandraMigrator struct {
container.CassandraWithContext
migrator
}
func (cs cassandraDS) apply(m migrator) migrator {
return cassandraMigrator{
CassandraWithContext: cs.CassandraWithContext,
migrator: m,
}
}
const (
checkAndCreateCassandraMigrationTable = `CREATE TABLE IF NOT EXISTS gofr_migrations (version bigint,
method text, start_time timestamp, duration bigint, PRIMARY KEY (version, method));`
getLastCassandraGoFrMigration = `SELECT version FROM gofr_migrations`
insertCassandraGoFrMigrationRow = `INSERT INTO gofr_migrations (version, method, start_time, duration) VALUES (?, ?, ?, ?);`
)
func (cs cassandraMigrator) checkAndCreateMigrationTable(c *container.Container) error {
if err := c.Cassandra.ExecWithCtx(context.Background(), checkAndCreateCassandraMigrationTable); err != nil {
return err
}
return cs.migrator.checkAndCreateMigrationTable(c)
}
func (cs cassandraMigrator) getLastMigration(c *container.Container) (int64, error) {
var (
lastMigration int64
lastMigrations []int64
)
err := c.Cassandra.QueryWithCtx(context.Background(), &lastMigrations, getLastCassandraGoFrMigration)
if err != nil {
return -1, fmt.Errorf("cassandra: %w", err)
}
for _, version := range lastMigrations {
if version > lastMigration {
lastMigration = version
}
}
c.Debugf("cassandra last migration fetched value is: %v", lastMigration)
lm2, err := cs.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (cs cassandraMigrator) beginTransaction(c *container.Container) transactionData {
cmt := cs.migrator.beginTransaction(c)
c.Debug("cassandra migrator begin successfully")
return cmt
}
func (cs cassandraMigrator) commitMigration(c *container.Container, data transactionData) error {
err := cs.CassandraWithContext.ExecWithCtx(context.Background(), insertCassandraGoFrMigrationRow, data.MigrationNumber,
"UP", data.StartTime, time.Since(data.StartTime).Milliseconds())
if err != nil {
return err
}
c.Debugf("inserted record for migration %v in cassandra gofr_migrations table", data.MigrationNumber)
return cs.migrator.commitMigration(c, data)
}
func (cs cassandraMigrator) rollback(c *container.Container, data transactionData) {
cs.migrator.rollback(c, data)
c.Fatalf("migration %v failed and rolled back", data.MigrationNumber)
}
func (cs cassandraMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return cs.migrator.lock(ctx, cancel, c, ownerID)
}
func (cs cassandraMigrator) unlock(c *container.Container, ownerID string) error {
return cs.migrator.unlock(c, ownerID)
}
func (cassandraMigrator) name() string {
return "Cassandra"
}
================================================
FILE: pkg/gofr/migration/cassandra_test.go
================================================
package migration
import (
"database/sql"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func cassandraSetup(t *testing.T) (migrator, *container.MockCassandraWithContext, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockCassandra := mocks.Cassandra
ds := Datasource{Cassandra: mockContainer.Cassandra}
cassandraDB := cassandraDS{CassandraWithContext: mockCassandra}
migratorWithCassandra := cassandraDB.apply(&ds)
mockContainer.Cassandra = mockCassandra
return migratorWithCassandra, mockCassandra, mockContainer
}
func Test_CassandraCheckAndCreateMigrationTable(t *testing.T) {
migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", sql.ErrConnDone},
}
for i, tc := range testCases {
mockCassandra.EXPECT().ExecWithCtx(gomock.Any(), checkAndCreateCassandraMigrationTable).Return(tc.err)
err := migratorWithCassandra.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_CassandraGetLastMigration(t *testing.T) {
migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 0},
{"connection failed", sql.ErrConnDone, -1},
}
var lastMigration []int64
for i, tc := range testCases {
mockCassandra.EXPECT().QueryWithCtx(gomock.Any(), &lastMigration, getLastCassandraGoFrMigration).Return(tc.err)
resp, err := migratorWithCassandra.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v]\n %v Failed! ", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
}
func Test_CassandraCommitMigration(t *testing.T) {
migratorWithCassandra, mockCassandra, mockContainer := cassandraSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", sql.ErrConnDone},
}
timeNow := time.Now()
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
for i, tc := range testCases {
mockCassandra.EXPECT().ExecWithCtx(gomock.Any(), insertCassandraGoFrMigrationRow, td.MigrationNumber,
"UP", td.StartTime, gomock.Any()).Return(tc.err)
err := migratorWithCassandra.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_CassandraBeginTransaction(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migratorWithCassandra, _, mockContainer := cassandraSetup(t)
migratorWithCassandra.beginTransaction(mockContainer)
})
assert.Contains(t, logs, "cassandra migrator begin successfully")
}
================================================
FILE: pkg/gofr/migration/clickhouse.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
type clickHouseDS struct {
Clickhouse
}
type clickHouseMigrator struct {
Clickhouse
migrator
}
func (ch clickHouseDS) apply(m migrator) migrator {
return clickHouseMigrator{
Clickhouse: ch.Clickhouse,
migrator: m,
}
}
const (
CheckAndCreateChMigrationTable = `CREATE TABLE IF NOT EXISTS gofr_migrations
(
version Int64 NOT NULL,
method String NOT NULL,
start_time DateTime NOT NULL,
duration Int64 NULL,
PRIMARY KEY (version, method)
) ENGINE = MergeTree()
ORDER BY (version, method);
`
getLastChGoFrMigration = `SELECT COALESCE(MAX(version), 0) as last_migration FROM gofr_migrations;`
insertChGoFrMigrationRow = `INSERT INTO gofr_migrations (version, method, start_time, duration) VALUES (?, ?, ?, ?);`
)
func (ch clickHouseMigrator) checkAndCreateMigrationTable(c *container.Container) error {
if err := c.Clickhouse.Exec(context.Background(), CheckAndCreateChMigrationTable); err != nil {
return err
}
return ch.migrator.checkAndCreateMigrationTable(c)
}
func (ch clickHouseMigrator) getLastMigration(c *container.Container) (int64, error) {
type LastMigration struct {
Timestamp int64 `ch:"last_migration"`
}
var lastMigrations []LastMigration
var lastMigration int64
err := c.Clickhouse.Select(context.Background(), &lastMigrations, getLastChGoFrMigration)
if err != nil {
return -1, fmt.Errorf("clickhouse: %w", err)
}
if len(lastMigrations) != 0 {
lastMigration = lastMigrations[0].Timestamp
}
c.Debugf("Clickhouse last migration fetched value is: %v", lastMigration)
lm2, err := ch.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (ch clickHouseMigrator) beginTransaction(c *container.Container) transactionData {
cmt := ch.migrator.beginTransaction(c)
c.Debug("Clickhouse Migrator begin successfully")
return cmt
}
func (ch clickHouseMigrator) commitMigration(c *container.Container, data transactionData) error {
err := ch.Clickhouse.Exec(context.Background(), insertChGoFrMigrationRow, data.MigrationNumber,
"UP", data.StartTime, time.Since(data.StartTime).Milliseconds())
if err != nil {
return err
}
c.Debugf("inserted record for migration %v in clickhouse gofr_migrations table", data.MigrationNumber)
return ch.migrator.commitMigration(c, data)
}
func (ch clickHouseMigrator) rollback(c *container.Container, data transactionData) {
ch.migrator.rollback(c, data)
c.Fatalf("migration %v failed and rolled back", data.MigrationNumber)
}
func (ch clickHouseMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return ch.migrator.lock(ctx, cancel, c, ownerID)
}
func (ch clickHouseMigrator) unlock(c *container.Container, ownerID string) error {
return ch.migrator.unlock(c, ownerID)
}
func (clickHouseMigrator) name() string {
return "Clickhouse"
}
================================================
FILE: pkg/gofr/migration/clickhouse_test.go
================================================
package migration
import (
"database/sql"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func clickHouseSetup(t *testing.T) (migrator, *MockClickhouse, *container.Container) {
t.Helper()
ctrl := gomock.NewController(t)
mockContainer, _ := container.NewMockContainer(t)
mockClickhouse := NewMockClickhouse(ctrl)
ds := Datasource{Clickhouse: mockClickhouse}
ch := clickHouseDS{Clickhouse: mockClickhouse}
mg := ch.apply(&ds)
mockContainer.Clickhouse = mockClickhouse
return mg, mockClickhouse, mockContainer
}
func Test_ClickHouseCheckAndCreateMigrationTable(t *testing.T) {
mg, mockClickhouse, mockContainer := clickHouseSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", sql.ErrConnDone},
}
for i, tc := range testCases {
mockClickhouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(tc.err)
err := mg.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_ClickHouseGetLastMigration(t *testing.T) {
mg, mockClickhouse, mockContainer := clickHouseSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 0},
{"connection failed", sql.ErrConnDone, -1},
}
for i, tc := range testCases {
mockClickhouse.EXPECT().Select(gomock.Any(), gomock.Any(), getLastChGoFrMigration).Return(tc.err)
resp, err := mg.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v]\n %v Failed! ", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
}
func Test_ClickHouseCommitMigration(t *testing.T) {
mg, mockClickhouse, mockContainer := clickHouseSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", sql.ErrConnDone},
}
timeNow := time.Now()
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
for i, tc := range testCases {
mockClickhouse.EXPECT().Exec(gomock.Any(), insertChGoFrMigrationRow, td.MigrationNumber,
"UP", td.StartTime, gomock.Any()).Return(tc.err)
err := mg.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_ClickHouseBeginTransaction(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
mg, _, mockContainer := clickHouseSetup(t)
mg.beginTransaction(mockContainer)
})
assert.Contains(t, logs, "Clickhouse Migrator begin successfully")
}
================================================
FILE: pkg/gofr/migration/datasource.go
================================================
package migration
import (
"context"
"gofr.dev/pkg/gofr/container"
)
type Datasource struct {
// TODO Logger should not be embedded rather it should be a field.
// Need to think it through as it will bring breaking changes.
Logger
SQL SQL
Redis Redis
PubSub PubSub
Clickhouse Clickhouse
Oracle Oracle
Cassandra Cassandra
Mongo Mongo
ArangoDB ArangoDB
SurrealDB SurrealDB
DGraph DGraph
ScyllaDB ScyllaDB
Elasticsearch Elasticsearch
OpenTSDB OpenTSDB
}
// It is a base implementation for migration manager, on this other database drivers have been wrapped.
func (*Datasource) checkAndCreateMigrationTable(*container.Container) error {
return nil
}
func (*Datasource) getLastMigration(*container.Container) (int64, error) {
return 0, nil
}
func (*Datasource) beginTransaction(*container.Container) transactionData {
return transactionData{}
}
func (*Datasource) commitMigration(c *container.Container, data transactionData) error {
c.Infof("Migration %v ran successfully", data.MigrationNumber)
return nil
}
func (*Datasource) rollback(*container.Container, transactionData) {}
func (Datasource) lock(context.Context, context.CancelFunc, *container.Container, string) error {
return nil
}
func (Datasource) unlock(*container.Container, string) error {
return nil
}
func (Datasource) name() string {
return "Base"
}
================================================
FILE: pkg/gofr/migration/datasource_test.go
================================================
package migration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func Test_getMigratorDatastoreNotInitialized(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
mockContainer, _ := container.NewMockContainer(t)
mockContainer.SQL = nil
mockContainer.Redis = nil
mg := Datasource{}
mg.rollback(mockContainer, transactionData{})
lastMigration, err := mg.getLastMigration(mockContainer)
require.NoError(t, err)
assert.Equal(t, int64(0), lastMigration, "TEST Failed \n Last Migration is not 0")
require.NoError(t, mg.checkAndCreateMigrationTable(mockContainer), "TEST Failed")
assert.Equal(t, transactionData{}, mg.beginTransaction(mockContainer), "TEST Failed")
require.NoError(t, mg.commitMigration(mockContainer, transactionData{}), "TEST Failed")
})
assert.Contains(t, logs, "Migration 0 ran successfully", "TEST Failed")
}
func Test_lock_unlock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockContainer, _ := container.NewMockContainer(t)
ds := Datasource{}
err := ds.lock(t.Context(), nil, mockContainer, "owner")
require.NoError(t, err)
err = ds.unlock(mockContainer, "owner")
require.NoError(t, err)
}
================================================
FILE: pkg/gofr/migration/dgraph.go
================================================
package migration
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/dgraph-io/dgo/v210/protos/api"
"gofr.dev/pkg/gofr/container"
)
// dgraphDS is the adapter struct that implements migration operations.
type dgraphDS struct {
client DGraph
}
// dgraphMigrator struct implements the migrator interface.
type dgraphMigrator struct {
dgraphDS
migrator
}
const (
// dgraphSchema defines the migration schema with fully qualified predicate names.
dgraphSchema = `
migrations.version: int @index(int) .
migrations.method: string .
migrations.start_time: datetime .
migrations.duration: int .
type Migration {
migrations.version
migrations.method
migrations.start_time
migrations.duration
}
`
// getLastMigrationQuery fetches the most recent migration version.
getLastMigrationQuery = `
{
migrations(func: type(Migration), orderdesc: migrations.version, first: 1) {
migrations.version
}
}
`
)
// apply creates a new dgraphMigrator.
func (ds dgraphDS) apply(m migrator) migrator {
return dgraphMigrator{
dgraphDS: ds,
migrator: m,
}
}
// ApplySchema applies the given schema to DGraph. It takes a context and schema string as parameters
// and returns an error if the schema application fails.
func (ds dgraphDS) ApplySchema(ctx context.Context, schema string) error {
return ds.client.ApplySchema(ctx, schema)
}
// AddOrUpdateField adds a new field or updates an existing field in DGraph schema.
// Parameters:
// - ctx: The context for the operation
// - fieldName: Name of the field to add or update
// - fieldType: Data type of the field
// - directives: Additional DGraph directives for the field
//
// Returns an error if the operation fails.
func (ds dgraphDS) AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error {
return ds.client.AddOrUpdateField(ctx, fieldName, fieldType, directives)
}
// DropField removes a field from DGraph schema.
// Parameters:
// - ctx: The context for the operation
// - fieldName: Name of the field to remove
//
// Returns an error if the field deletion fails.
func (ds dgraphDS) DropField(ctx context.Context, fieldName string) error {
return ds.client.DropField(ctx, fieldName)
}
// checkAndCreateMigrationTable ensures migration schema exists.
func (dm dgraphMigrator) checkAndCreateMigrationTable(c *container.Container) error {
err := dm.ApplySchema(context.Background(), dgraphSchema)
if err != nil {
c.Debug("Migration schema might already exist:", err)
}
return dm.migrator.checkAndCreateMigrationTable(c)
}
// getLastMigration retrieves the last applied migration version.
func (dm dgraphMigrator) getLastMigration(c *container.Container) (int64, error) {
var response struct {
Migrations []struct {
Version int64 `json:"version"`
} `json:"migrations"`
}
resp, err := c.DGraph.Query(context.Background(), getLastMigrationQuery)
if err != nil {
return -1, fmt.Errorf("dgraph: %w", err)
}
if resp != nil {
var b []byte
b, err = json.Marshal(resp)
if err != nil {
return 0, fmt.Errorf("dgraph: %w", err)
}
err = json.Unmarshal(b, &response)
if err != nil {
return 0, fmt.Errorf("dgraph: %w", err)
}
}
var lastMigration int64
if len(response.Migrations) > 0 {
lastMigration = response.Migrations[0].Version
}
lm2, err := dm.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
// beginTransaction starts a new migration transaction.
func (dm dgraphMigrator) beginTransaction(c *container.Container) transactionData {
data := dm.migrator.beginTransaction(c)
c.Debug("Dgraph migrator begin successfully")
return data
}
// commitMigration commits the migration and records its metadata.
func (dm dgraphMigrator) commitMigration(c *container.Container, data transactionData) error {
// Build the JSON payload for the migration record.
payload := map[string]any{
"migrations": []map[string]any{
{
"migrations.version": data.MigrationNumber,
"migrations.method": "UP",
"migrations.start_time": data.StartTime.Format(time.RFC3339),
"migrations.duration": time.Since(data.StartTime).Milliseconds(),
},
},
}
jsonPayload, err := json.Marshal(payload)
if err != nil {
return err
}
_, err = c.DGraph.Mutate(context.Background(), &api.Mutation{
SetJson: jsonPayload,
})
if err != nil {
return err
}
c.Debugf("Inserted record for migration %v in Dgraph migrations", data.MigrationNumber)
return dm.migrator.commitMigration(c, data)
}
// rollback handles migration failure and rollback.
func (dm dgraphMigrator) rollback(c *container.Container, data transactionData) {
dm.migrator.rollback(c, data)
c.Fatalf("Migration %v failed and rolled back", data.MigrationNumber)
}
func (dm dgraphMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return dm.migrator.lock(ctx, cancel, c, ownerID)
}
func (dm dgraphMigrator) unlock(c *container.Container, ownerID string) error {
return dm.migrator.unlock(c, ownerID)
}
func (dgraphMigrator) name() string {
return "DGraph"
}
================================================
FILE: pkg/gofr/migration/dgraph_test.go
================================================
package migration
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func dgraphSetup(t *testing.T) (migrator, *container.MockDgraph, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockDGraph := mocks.DGraph
ds := Datasource{DGraph: mockContainer.DGraph}
dgraphDB := dgraphDS{client: mockDGraph}
migratorWithDGraph := dgraphDB.apply(&ds)
mockContainer.DGraph = mockDGraph
return migratorWithDGraph, mockDGraph, mockContainer
}
func Test_DGraphCheckAndCreateMigrationTable(t *testing.T) {
migratorWithDGraph, mockDGraph, mockContainer := dgraphSetup(t)
mockDGraph.EXPECT().ApplySchema(gomock.Any(), dgraphSchema).Return(nil)
err := migratorWithDGraph.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err, "Test_DGraphCheckAndCreateMigrationTable Failed!")
}
func Test_DGraphGetLastMigration(t *testing.T) {
migratorWithDGraph, mockDGraph, mockContainer := dgraphSetup(t)
testCases := []struct {
desc string
err error
mockResp map[string]any
expected int64
}{
{
desc: "success",
err: nil,
mockResp: map[string]any{
"migrations": []map[string]any{
{
"version": float64(10),
},
},
},
expected: 10,
},
{
desc: "query error",
err: context.DeadlineExceeded,
mockResp: nil,
expected: -1,
},
{
desc: "empty response",
err: nil,
mockResp: map[string]any{},
expected: 0,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// Set up mock expectation for the main query
mockDGraph.EXPECT().Query(gomock.Any(), getLastMigrationQuery).
Return(tc.mockResp, tc.err)
resp, err := migratorWithDGraph.getLastMigration(mockContainer)
assert.Equal(t, tc.expected, resp, "TEST[%v] Failed!", i)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v] Failed!", i)
} else {
assert.NoError(t, err, "TEST[%v] Failed!", i)
}
})
}
}
func Test_DGraphCommitMigration(t *testing.T) {
migratorWithDGraph, mockDGraph, mockContainer := dgraphSetup(t)
timeNow := time.Now()
testCases := []struct {
desc string
err error
}{
{"success", nil},
{"mutation failed", context.DeadlineExceeded},
}
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
for i, tc := range testCases {
mockDGraph.EXPECT().Mutate(gomock.Any(), gomock.Any()).Return(nil, tc.err)
err := migratorWithDGraph.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed!", i, tc.desc)
}
}
func Test_DGraphBeginTransaction(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migratorWithDGraph, _, mockContainer := dgraphSetup(t)
migratorWithDGraph.beginTransaction(mockContainer)
})
assert.Contains(t, logs, "Dgraph migrator begin successfully")
}
func Test_DGraphDS_ApplySchema(t *testing.T) {
_, mockDGraph, _ := dgraphSetup(t)
ds := dgraphDS{client: mockDGraph}
ctx := t.Context()
schema := "test schema"
testCases := []struct {
desc string
err error
}{
{"success", nil},
{"schema error", context.DeadlineExceeded},
}
for i, tc := range testCases {
mockDGraph.EXPECT().ApplySchema(ctx, schema).Return(tc.err)
err := ds.ApplySchema(ctx, schema)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed!", i, tc.desc)
}
}
func Test_DGraphDS_AddOrUpdateField(t *testing.T) {
_, mockDGraph, _ := dgraphSetup(t)
ds := dgraphDS{client: mockDGraph}
ctx := t.Context()
fieldName := "test"
fieldType := "string"
directives := "@index(exact)"
testCases := []struct {
desc string
err error
}{
{"success", nil},
{"field error", context.DeadlineExceeded},
}
for i, tc := range testCases {
mockDGraph.EXPECT().AddOrUpdateField(ctx, fieldName, fieldType, directives).Return(tc.err)
err := ds.AddOrUpdateField(ctx, fieldName, fieldType, directives)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed!", i, tc.desc)
}
}
func Test_DGraphDS_DropField(t *testing.T) {
_, mockDGraph, _ := dgraphSetup(t)
ds := dgraphDS{client: mockDGraph}
ctx := t.Context()
fieldName := "test"
testCases := []struct {
desc string
err error
}{
{"success", nil},
{"drop error", context.DeadlineExceeded},
}
for i, tc := range testCases {
mockDGraph.EXPECT().DropField(ctx, fieldName).Return(tc.err)
err := ds.DropField(ctx, fieldName)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed!", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/migration/elasticsearch.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
// elasticsearchDS is the adapter struct that implements migration operations for Elasticsearch.
type elasticsearchDS struct {
client Elasticsearch
}
// elasticsearchMigrator struct implements the migrator interface for Elasticsearch.
type elasticsearchMigrator struct {
elasticsearchDS
migrator
}
const (
// elasticsearchMigrationIndex is the index used to track migrations.
elasticsearchMigrationIndex = "gofr_migrations"
)
// getLastElasticsearchMigrationQuery fetches the most recent migration version.
func getLastElasticsearchMigrationQuery() map[string]any {
return map[string]any{
"size": 1,
"sort": []map[string]any{
{"version": map[string]any{"order": "desc"}},
},
"_source": []string{"version"},
}
}
// apply creates a new elasticsearchMigrator.
func (ds elasticsearchDS) apply(m migrator) migrator {
return elasticsearchMigrator{
elasticsearchDS: ds,
migrator: m,
}
}
// checkAndCreateMigrationTable creates the migration tracking index if it doesn't exist.
func (em elasticsearchMigrator) checkAndCreateMigrationTable(c *container.Container) error {
// Check if the migration index exists
query := map[string]any{
"query": map[string]any{
"match_all": map[string]any{},
},
"size": 0,
}
_, err := c.Elasticsearch.Search(context.Background(), []string{elasticsearchMigrationIndex}, query)
if err != nil {
c.Debug("Migration index might already exist:", err)
// Index doesn't exist, create it
settings := map[string]any{
"settings": map[string]any{
"number_of_shards": 1,
"number_of_replicas": 0,
},
"mappings": map[string]any{
"properties": map[string]any{
"version": map[string]any{
"type": "long",
},
"method": map[string]any{
"type": "keyword",
},
"start_time": map[string]any{
"type": "date",
},
"duration": map[string]any{
"type": "long",
},
},
},
}
err = c.Elasticsearch.CreateIndex(context.Background(), elasticsearchMigrationIndex, settings)
if err != nil {
return fmt.Errorf("failed to create migration index: %w", err)
}
c.Debugf("Created Elasticsearch migration index: %s", elasticsearchMigrationIndex)
}
return em.migrator.checkAndCreateMigrationTable(c)
}
// getLastMigration retrieves the latest migration version from Elasticsearch.
func (em elasticsearchMigrator) getLastMigration(c *container.Container) (int64, error) {
var lastMigration int64
result, err := c.Elasticsearch.Search(context.Background(), []string{elasticsearchMigrationIndex}, getLastElasticsearchMigrationQuery())
if err != nil {
return -1, fmt.Errorf("elasticsearch: %w", err)
}
lastMigration = extractLastMigrationVersion(result)
c.Debugf("Elasticsearch last migration fetched value is: %v", lastMigration)
lm2, err := em.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
// extractLastMigrationVersion extracts the latest migration version from the Elasticsearch search result.
func extractLastMigrationVersion(result map[string]any) int64 {
hits, ok := result["hits"].(map[string]any)
if !ok {
return 0
}
hitsList, ok := hits["hits"].([]any)
if !ok || len(hitsList) == 0 {
return 0
}
firstHit, ok := hitsList[0].(map[string]any)
if !ok {
return 0
}
source, ok := firstHit["_source"].(map[string]any)
if !ok {
return 0
}
version, ok := source["version"].(float64)
if !ok {
return 0
}
return int64(version)
}
// beginTransaction starts a new transaction (Elasticsearch doesn't support traditional transactions).
func (em elasticsearchMigrator) beginTransaction(c *container.Container) transactionData {
return em.migrator.beginTransaction(c)
}
// commitMigration records the migration in the tracking index.
func (em elasticsearchMigrator) commitMigration(c *container.Container, data transactionData) error {
migrationDoc := map[string]any{
"version": data.MigrationNumber,
"method": "UP",
"start_time": data.StartTime.Format(time.RFC3339),
"duration": time.Since(data.StartTime).Milliseconds(),
}
// Use the migration number as the document ID for idempotency
docID := fmt.Sprintf("%d", data.MigrationNumber)
err := c.Elasticsearch.IndexDocument(context.Background(), elasticsearchMigrationIndex, docID, migrationDoc)
if err != nil {
return fmt.Errorf("failed to record migration: %w", err)
}
c.Debugf("Inserted record for migration %v in Elasticsearch gofr_migrations index", data.MigrationNumber)
return em.migrator.commitMigration(c, data)
}
// rollback is a no-op for Elasticsearch migrations.
func (em elasticsearchMigrator) rollback(c *container.Container, data transactionData) {
em.migrator.rollback(c, data)
c.Fatalf("Migration %v failed.", data.MigrationNumber)
}
func (em elasticsearchMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return em.migrator.lock(ctx, cancel, c, ownerID)
}
func (em elasticsearchMigrator) unlock(c *container.Container, ownerID string) error {
return em.migrator.unlock(c, ownerID)
}
func (elasticsearchMigrator) name() string {
return "Elasticsearch"
}
================================================
FILE: pkg/gofr/migration/elasticsearch_test.go
================================================
package migration
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func initializeElasticsearchRunMocks(t *testing.T) (*MockElasticsearch, *container.Container) {
t.Helper()
mockElasticsearch := NewMockElasticsearch(gomock.NewController(t))
mockContainer := container.NewContainer(nil)
mockContainer.SQL = nil
mockContainer.Redis = nil
mockContainer.Mongo = nil
mockContainer.Cassandra = nil
mockContainer.PubSub = nil
mockContainer.ArangoDB = nil
mockContainer.SurrealDB = nil
mockContainer.DGraph = nil
mockContainer.Clickhouse = nil
mockContainer.Oracle = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
mockContainer.Logger = logging.NewMockLogger(logging.DEBUG)
mockContainer.Elasticsearch = mockElasticsearch
return mockElasticsearch, mockContainer
}
func TestMigrationRunElasticsearchSuccess(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
// Create an index
settings := map[string]any{
"settings": map[string]any{
"number_of_shards": 1,
},
}
err := d.Elasticsearch.CreateIndex(t.Context(), "test-index", settings)
if err != nil {
return err
}
// Index a document
document := map[string]any{
"title": "Test Document",
"content": "This is a test document",
}
err = d.Elasticsearch.IndexDocument(t.Context(), "test-index", "1", document)
if err != nil {
return err
}
d.Logger.Infof("Elasticsearch Migration Ran Successfully")
return nil
}},
}
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock the migration index check (index doesn't exist initially)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(nil, assert.AnError)
// Mock the migration index creation
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), elasticsearchMigrationIndex, gomock.Any()).
Return(nil)
// Mock the last migration query (no migrations yet) — called twice: pre-check + under lock
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{},
},
}, nil).Times(2)
// Mock the migration operations
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), "test-index", gomock.Any()).
Return(nil)
mockElasticsearch.EXPECT().IndexDocument(gomock.Any(), "test-index", "1", gomock.Any()).
Return(nil)
// Mock the migration record insertion
mockElasticsearch.EXPECT().IndexDocument(gomock.Any(), elasticsearchMigrationIndex, "1", gomock.Any()).
Return(nil)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "Migration 1 ran successfully")
assert.Contains(t, logs, "Elasticsearch Migration Ran Successfully")
}
func TestMigrationRunElasticsearchMigrationFailure(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Elasticsearch.CreateIndex(t.Context(), "test-index", map[string]any{})
if err != nil {
return err
}
return nil
}},
}
// Mock the migration index check (index doesn't exist initially)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(nil, assert.AnError)
// Mock the migration index creation
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), elasticsearchMigrationIndex, gomock.Any()).
Return(nil)
// Mock the last migration query (no migrations yet) — called twice: pre-check + under lock
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{},
},
}, nil).Times(2)
// Mock the migration operation failure
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), "test-index", gomock.Any()).
Return(assert.AnError)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "failed to run migration : [1], err: assert.AnError general error for testing")
}
func TestMigrationRunElasticsearchMigrationFailureWhileCheckingTable(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
testutil.StderrOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error {
return nil
}},
}
// checkAndCreateMigrationTable: Check if index exists (error)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(nil, assert.AnError)
// checkAndCreateMigrationTable: Try to create index (fails)
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), elasticsearchMigrationIndex, gomock.Any()).
Return(assert.AnError)
Run(migrationMap, mockContainer)
})
assert.True(t, mockElasticsearch.ctrl.Satisfied())
}
func TestMigrationRunElasticsearchCurrentMigrationEqualLastMigration(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error {
return nil
}},
}
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock the migration index check (index doesn't exist initially)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(nil, assert.AnError)
// Mock the migration index creation
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), elasticsearchMigrationIndex, gomock.Any()).
Return(nil)
// Mock the last migration query (migration 1 already exists)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{
map[string]any{
"_source": map[string]any{
"version": float64(1),
},
},
},
},
}, nil)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "no new migrations to run")
}
func TestMigrationRunElasticsearchCommitError(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Elasticsearch.CreateIndex(t.Context(), "test-index", map[string]any{})
if err != nil {
return err
}
return nil
}},
}
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock the migration index check (index doesn't exist initially)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(nil, assert.AnError)
// Mock the migration index creation
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), elasticsearchMigrationIndex, gomock.Any()).
Return(nil)
// Mock the last migration query (no migrations yet) — called twice: pre-check + under lock
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{},
},
}, nil).Times(2)
// Mock the migration operation success
mockElasticsearch.EXPECT().CreateIndex(gomock.Any(), "test-index", gomock.Any()).
Return(nil)
// Mock the migration record insertion failure
mockElasticsearch.EXPECT().IndexDocument(gomock.Any(), elasticsearchMigrationIndex, "1", gomock.Any()).
Return(assert.AnError)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "failed to commit migration, err: failed to record migration: assert.AnError general error for testing")
}
func TestElasticsearchMigrator_checkAndCreateMigrationTable_IndexExists(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock successful search (index exists)
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex}, gomock.Any()).
Return(map[string]any{
"hits": map[string]any{
"total": map[string]any{
"value": float64(0),
},
},
}, nil)
ds := elasticsearchDS{client: mockElasticsearch}
mg := elasticsearchMigrator{elasticsearchDS: ds, migrator: &Datasource{}}
err := mg.checkAndCreateMigrationTable(mockContainer)
assert.NoError(t, err)
}
func TestElasticsearchMigrator_getLastMigration_WithMigrations(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock successful search with existing migrations
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{
map[string]any{
"_source": map[string]any{
"version": float64(5),
},
},
},
},
}, nil)
ds := elasticsearchDS{client: mockElasticsearch}
mg := elasticsearchMigrator{elasticsearchDS: ds, migrator: &Datasource{}}
lastMigration, err := mg.getLastMigration(mockContainer)
require.NoError(t, err)
assert.Equal(t, int64(5), lastMigration)
}
func TestElasticsearchMigrator_getLastMigration_NoMigrations(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock successful search with no migrations
mockElasticsearch.EXPECT().Search(gomock.Any(), []string{elasticsearchMigrationIndex},
getLastElasticsearchMigrationQuery()).
Return(map[string]any{
"hits": map[string]any{
"hits": []any{},
},
}, nil)
ds := elasticsearchDS{client: mockElasticsearch}
mg := elasticsearchMigrator{elasticsearchDS: ds, migrator: &Datasource{}}
lastMigration, err := mg.getLastMigration(mockContainer)
require.NoError(t, err)
assert.Equal(t, int64(0), lastMigration)
}
func TestElasticsearchMigrator_commitMigration_Success(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock successful document indexing
mockElasticsearch.EXPECT().IndexDocument(gomock.Any(), elasticsearchMigrationIndex, "1", gomock.Any()).
Return(nil)
ds := elasticsearchDS{client: mockElasticsearch}
mg := elasticsearchMigrator{elasticsearchDS: ds, migrator: &Datasource{}}
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now(),
}
err := mg.commitMigration(mockContainer, data)
assert.NoError(t, err)
}
func TestElasticsearchMigrator_commitMigration_Failure(t *testing.T) {
mockElasticsearch, mockContainer := initializeElasticsearchRunMocks(t)
// Mock failed document indexing
mockElasticsearch.EXPECT().IndexDocument(gomock.Any(), elasticsearchMigrationIndex, "1", gomock.Any()).
Return(assert.AnError)
ds := elasticsearchDS{client: mockElasticsearch}
mg := elasticsearchMigrator{elasticsearchDS: ds, migrator: &Datasource{}}
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now(),
}
err := mg.commitMigration(mockContainer, data)
require.Error(t, err)
assert.Contains(t, err.Error(), "failed to record migration")
}
================================================
FILE: pkg/gofr/migration/interface.go
================================================
package migration
import (
"context"
"database/sql"
"time"
goRedis "github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/container"
)
type Redis interface {
Get(ctx context.Context, key string) *goRedis.StringCmd
Set(ctx context.Context, key string, value any, expiration time.Duration) *goRedis.StatusCmd
Del(ctx context.Context, keys ...string) *goRedis.IntCmd
Rename(ctx context.Context, key, newKey string) *goRedis.StatusCmd
}
type SQL interface {
Query(query string, args ...any) (*sql.Rows, error)
QueryRow(query string, args ...any) *sql.Row
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
Exec(query string, args ...any) (sql.Result, error)
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}
type PubSub interface {
Query(ctx context.Context, query string, args ...any) ([]byte, error)
CreateTopic(context context.Context, name string) error
DeleteTopic(context context.Context, name string) error
}
type Clickhouse interface {
Exec(ctx context.Context, query string, args ...any) error
Select(ctx context.Context, dest any, query string, args ...any) error
AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error
HealthCheck(ctx context.Context) (any, error)
}
type Oracle interface {
Select(ctx context.Context, dest any, query string, args ...any) error
Exec(ctx context.Context, query string, args ...any) error
Begin() (container.OracleTx, error)
}
type Cassandra interface {
Exec(query string, args ...any) error
NewBatch(name string, batchType int) error
BatchQuery(name, stmt string, values ...any) error
ExecuteBatch(name string) error
HealthCheck(ctx context.Context) (any, error)
}
// Mongo is an interface representing a MongoDB database client with common CRUD operations.
type Mongo interface {
Find(ctx context.Context, collection string, filter any, results any) error
FindOne(ctx context.Context, collection string, filter any, result any) error
InsertOne(ctx context.Context, collection string, document any) (any, error)
InsertMany(ctx context.Context, collection string, documents []any) ([]any, error)
DeleteOne(ctx context.Context, collection string, filter any) (int64, error)
DeleteMany(ctx context.Context, collection string, filter any) (int64, error)
UpdateByID(ctx context.Context, collection string, id any, update any) (int64, error)
UpdateOne(ctx context.Context, collection string, filter any, update any) error
UpdateMany(ctx context.Context, collection string, filter any, update any) (int64, error)
Drop(ctx context.Context, collection string) error
CreateCollection(ctx context.Context, name string) error
StartSession() (any, error)
}
// ArangoDB is an interface representing an ArangoDB database client with common CRUD operations.
type ArangoDB interface {
// CreateDB creates a new database in ArangoDB.
CreateDB(ctx context.Context, database string) error
// DropDB deletes an existing database in ArangoDB.
DropDB(ctx context.Context, database string) error
// CreateCollection creates a new collection in a database with specified type.
CreateCollection(ctx context.Context, database, collection string, isEdge bool) error
// DropCollection deletes an existing collection from a database.
DropCollection(ctx context.Context, database, collection string) error
// CreateGraph creates a new graph in a database.
CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error
// DropGraph deletes an existing graph from a database.
DropGraph(ctx context.Context, database, graph string) error
}
type SurrealDB interface {
// Query executes a Surreal query with the provided variables and returns the query results as a slice of interfaces{}.
// It returns an error if the query execution fails.
Query(ctx context.Context, query string, vars map[string]any) ([]any, error)
// CreateNamespace creates a new namespace in the SurrealDB instance.
CreateNamespace(ctx context.Context, namespace string) error
// CreateDatabase creates a new database in the SurrealDB instance.
CreateDatabase(ctx context.Context, database string) error
// DropNamespace deletes a namespace from the SurrealDB instance.
DropNamespace(ctx context.Context, namespace string) error
// DropDatabase deletes a database from the SurrealDB instance.
DropDatabase(ctx context.Context, database string) error
}
type DGraph interface {
// ApplySchema applies or updates the complete database schema.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - schema: Schema definition in Dgraph Schema Definition Language (SDL) format
// Returns:
// - error: An error if the schema application fails
ApplySchema(ctx context.Context, schema string) error
// AddOrUpdateField atomically creates or updates a single field definition.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - fieldName: Name of the field/predicate to create or update
// - fieldType: Dgraph data type (e.g., string, int, datetime)
// - directives: Space-separated Dgraph directives (e.g., "@index(hash) @upsert")
// Returns:
// - error: An error if the field operation fails
AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error
// DropField permanently removes a field/predicate and all its associated data.
// Parameters:
// - ctx: Context for request cancellation and timeouts
// - fieldName: Name of the field/predicate to remove
// Returns:
// - error: An error if the field removal fails
DropField(ctx context.Context, fieldName string) error
}
type ScyllaDB interface {
Query(dest any, stmt string, values ...any) error
QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error
Exec(stmt string, values ...any) error
ExecWithCtx(ctx context.Context, stmt string, values ...any) error
ExecCAS(dest any, stmt string, values ...any) (bool, error)
NewBatch(name string, batchType int) error
NewBatchWithCtx(ctx context.Context, name string, batchType int) error
BatchQuery(name, stmt string, values ...any) error
BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error
ExecuteBatchWithCtx(ctx context.Context, name string) error
}
// Elasticsearch is an interface representing an Elasticsearch client for migration operations.
// It includes only the essential methods needed for schema changes and migrations.
type Elasticsearch interface {
// CreateIndex creates a new index with optional mapping/settings.
CreateIndex(ctx context.Context, index string, settings map[string]any) error
// DeleteIndex deletes an existing index.
DeleteIndex(ctx context.Context, index string) error
// IndexDocument indexes (creates or replaces) a single document.
// Useful for seeding data or adding configuration documents during migrations.
IndexDocument(ctx context.Context, index, id string, document any) error
// GetDocument retrieves a single document by ID.
GetDocument(ctx context.Context, index, id string) (map[string]any, error)
// UpdateDocument applies a partial update to an existing document.
UpdateDocument(ctx context.Context, index, id string, update map[string]any) error
// DeleteDocument removes a document by ID.
// Useful for removing specific documents during migrations.
DeleteDocument(ctx context.Context, index, id string) error
// Bulk executes multiple indexing/updating/deleting operations in one request.
// Each entry in `operations` should be a JSON‑serializable object
// following the Elasticsearch bulk API format.
// Useful for bulk operations during migrations.
Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error)
// Search performs a search request.
Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error)
HealthCheck(ctx context.Context) (any, error)
}
// keeping the migrator interface unexported as, right now it is not being implemented directly, by the externalDB drivers.
// keeping the implementations for externalDB at one place such that if any change in migration logic, we would change directly here.
type migrator interface {
checkAndCreateMigrationTable(c *container.Container) error
getLastMigration(c *container.Container) (int64, error)
beginTransaction(c *container.Container) transactionData
commitMigration(c *container.Container, data transactionData) error
rollback(c *container.Container, data transactionData)
locker
}
type OpenTSDB interface {
// PutDataPoints can be used for seeding initial metrics during migration
PutDataPoints(ctx context.Context, data any, queryParam string, res any) error
// PostAnnotation creates or updates an annotation in OpenTSDB using the 'POST /api/annotation' endpoint.
PostAnnotation(ctx context.Context, annotation any, res any) error
// PutAnnotation creates or replaces an annotation in OpenTSDB using the 'PUT /api/annotation' endpoint.
PutAnnotation(ctx context.Context, annotation any, res any) error
// DeleteAnnotation removes an annotation from OpenTSDB using the 'DELETE /api/annotation' endpoint.
DeleteAnnotation(ctx context.Context, annotation any, res any) error
}
type locker interface {
lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error
unlock(c *container.Container, ownerID string) error
name() string
}
================================================
FILE: pkg/gofr/migration/logger.go
================================================
package migration
type Logger interface {
Debug(args ...any)
Debugf(format string, args ...any)
Info(args ...any)
Infof(format string, args ...any)
Notice(args ...any)
Noticef(format string, args ...any)
Warn(args ...any)
Warnf(format string, args ...any)
Error(args ...any)
Errorf(format string, args ...any)
Fatal(args ...any)
Fatalf(format string, args ...any)
}
================================================
FILE: pkg/gofr/migration/migration.go
================================================
package migration
import (
"context"
"errors"
"reflect"
"sort"
"time"
"github.com/gogo/protobuf/sortkeys"
"github.com/google/uuid"
goRedis "github.com/redis/go-redis/v9"
"gofr.dev/pkg/gofr/container"
gofrSql "gofr.dev/pkg/gofr/datasource/sql"
)
var (
errLockAcquisitionFailed = errors.New("failed to acquire migration lock")
errLockReleaseFailed = errors.New("failed to release migration lock")
)
const (
// lockKey is the key used for distributed locking.
lockKey = "gofr_migrations_lock"
// Default values for configuration.
defaultRetry = 500 * time.Millisecond
// defaultLockTTL is the duration for which the migration lock is valid.
// It is kept at 15 seconds to provide a safety margin for network jitters or transient failures.
defaultLockTTL = 15 * time.Second
// defaultRefresh is the interval at which the migration lock is renewed.
// A 5-second interval allows for up to 2 failed refresh attempts before the 15-second TTL expires,
// ensuring the lock stays robust while still allowing fairly quick recovery if a process crashes.
defaultRefresh = 5 * time.Second
)
type MigrateFunc func(d Datasource) error
type Migrate struct {
UP MigrateFunc
}
type transactionData struct {
StartTime time.Time
MigrationNumber int64
SQLTx *gofrSql.Tx
RedisTx goRedis.Pipeliner
OracleTx container.OracleTx
}
func Run(migrationsMap map[int64]Migrate, c *container.Container) {
invalidKeys, keys := getKeys(migrationsMap)
if len(invalidKeys) > 0 {
c.Errorf("migration run failed! UP not defined for the following keys: %v", invalidKeys)
return
}
sortkeys.Int64s(keys)
ds, mg, ok := getMigrator(c)
ds.Logger = c.Logger
// Returning with an error log as migration would eventually fail as No databases are initialized.
// Pub/Sub is considered as initialized if its configurations are given.
if !ok {
c.Errorf("no migrations are running as datasources are not initialized")
return
}
// Create migration tables BEFORE acquiring locks (lock table must exist first)
err := mg.checkAndCreateMigrationTable(c)
if err != nil {
c.Fatalf("failed to create gofr_migration table, err: %v", err)
return
}
// Optimistic pre-check: only acquire locks if there MIGHT be new migrations
// This is a fast path to avoid lock contention when no migrations are needed
lastMigration, err := mg.getLastMigration(c)
if err != nil {
c.Fatalf("migration failed: could not verify migration state from datasources, err: %v", err)
return
}
if !hasNewMigrations(keys, lastMigration) {
c.Info("no new migrations to run")
return
}
acquireLockAndRun(c, mg, &ds, migrationsMap, keys)
}
func acquireLockAndRun(c *container.Container, mg migrator, ds *Datasource, migrationsMap map[int64]Migrate, keys []int64) {
ownerID := uuid.New().String()
ctx, cancel := context.WithCancel(context.Background())
if err := mg.lock(ctx, cancel, c, ownerID); err != nil {
cancel()
if unlockErr := mg.unlock(c, ownerID); unlockErr != nil {
c.Errorf("failed to cleanup lock after acquisition failure: %v", unlockErr)
}
c.Fatalf("migration failed: could not acquire locks, err: %v", err)
return
}
defer func() {
cancel()
if err := mg.unlock(c, ownerID); err != nil {
c.Errorf("failed to unlock during cleanup: %v", err)
}
}()
// Re-fetch lastMigration under the lock to avoid racing with another pod
// that may have completed the same migration between our pre-check and lock acquisition.
lastMigration, err := mg.getLastMigration(c)
if err != nil {
c.Fatalf("migration failed: could not verify migration state under lock, err: %v", err)
return
}
if !hasNewMigrations(keys, lastMigration) {
c.Info("no new migrations to run (verified under lock)")
return
}
runMigrations(ctx, c, mg, ds, migrationsMap, keys, lastMigration)
}
func hasNewMigrations(keys []int64, lastMigration int64) bool {
for _, k := range keys {
if k > lastMigration {
return true
}
}
return false
}
func runMigrations(ctx context.Context, c *container.Container, mg migrator, ds *Datasource, migrationsMap map[int64]Migrate,
keys []int64, lastMigration int64) {
for _, currentMigration := range keys {
if currentMigration <= lastMigration {
c.Infof("skipping migration %v", currentMigration)
continue
}
// Check if lock refresh failed before starting the migration
select {
case <-ctx.Done():
c.Fatalf("migration %v aborted: lock refresh failed", currentMigration)
return
default:
}
c.Infof("running migration %v", currentMigration)
migrationInfo := mg.beginTransaction(c)
// Check if lock refresh failed after starting the transaction but before execution
select {
case <-ctx.Done():
mg.rollback(c, migrationInfo)
c.Fatalf("migration %v aborted: lock refresh failed", currentMigration)
return
default:
}
// Replacing the objects in datasource object only for those Datasources which support transactions.
ds.SQL = migrationInfo.SQLTx
ds.Redis = migrationInfo.RedisTx
if !isNil(migrationInfo.OracleTx) {
ds.Oracle = &oracleTransactionWrapper{tx: migrationInfo.OracleTx}
}
migrationInfo.StartTime = time.Now().UTC()
migrationInfo.MigrationNumber = currentMigration
err := migrationsMap[currentMigration].UP(*ds)
// Check if lock refresh failed during migration execution
select {
case <-ctx.Done():
mg.rollback(c, migrationInfo)
c.Fatalf("migration %v aborted: lock refresh failed during execution", currentMigration)
return
default:
}
if err != nil {
c.Errorf("failed to run migration : [%v], err: %v", currentMigration, err)
mg.rollback(c, migrationInfo)
return
}
err = mg.commitMigration(c, migrationInfo)
if err != nil {
c.Errorf("failed to commit migration, err: %v", err)
mg.rollback(c, migrationInfo)
return
}
}
}
func getKeys(migrationsMap map[int64]Migrate) (invalidKeys, validKeys []int64) {
for k := range migrationsMap {
if migrationsMap[k].UP != nil {
validKeys = append(validKeys, k)
} else {
invalidKeys = append(invalidKeys, k)
}
}
return invalidKeys, validKeys
}
func getMigrator(c *container.Container) (Datasource, migrator, bool) {
var (
ds Datasource
mg migrator = &ds
ok bool
)
mg, ok = initializeDatasources(c, &ds, mg)
return ds, mg, ok
}
type datasourceInitializer struct {
condition func() bool
setDS func()
apply func(m migrator) migrator
logIdentifier string
}
func initializeDatasources(c *container.Container, ds *Datasource, mg migrator) (migrator, bool) {
initializers := getInitializers(c, ds)
var active []datasourceInitializer
for _, init := range initializers {
if init.condition() {
active = append(active, init)
}
}
if len(active) == 0 {
return nil, false
}
sort.Slice(active, func(i, j int) bool {
return active[i].logIdentifier < active[j].logIdentifier
})
// Build the chain starting from the base Datasource
for i := len(active) - 1; i >= 0; i-- {
active[i].setDS()
mg = active[i].apply(mg)
c.Debugf("initialized data source for %s", active[i].logIdentifier)
}
return mg, true
}
func getInitializers(c *container.Container, ds *Datasource) []datasourceInitializer {
return []datasourceInitializer{
{
condition: func() bool { return !isNil(c.SQL) },
setDS: func() { ds.SQL = c.SQL },
apply: func(m migrator) migrator { return (&sqlDS{c.SQL}).apply(m) },
logIdentifier: "SQL",
},
{
condition: func() bool { return !isNil(c.Redis) },
setDS: func() { ds.Redis = c.Redis },
apply: func(m migrator) migrator { return redisDS{c.Redis}.apply(m) },
logIdentifier: "Redis",
},
{
condition: func() bool { return !isNil(c.DGraph) },
setDS: func() { ds.DGraph = dgraphDS{c.DGraph} },
apply: func(m migrator) migrator { return dgraphDS{c.DGraph}.apply(m) },
logIdentifier: "DGraph",
},
{
condition: func() bool { return !isNil(c.Clickhouse) },
setDS: func() { ds.Clickhouse = c.Clickhouse },
apply: func(m migrator) migrator { return clickHouseDS{c.Clickhouse}.apply(m) },
logIdentifier: "Clickhouse",
},
{
condition: func() bool { return !isNil(c.Oracle) },
setDS: func() { ds.Oracle = c.Oracle },
apply: func(m migrator) migrator { return oracleDS{c.Oracle}.apply(m) },
logIdentifier: "Oracle",
},
{
condition: func() bool { return !isNil(c.PubSub) },
setDS: func() { ds.PubSub = c.PubSub },
apply: func(m migrator) migrator { return pubsubDS{client: c.PubSub}.apply(m) },
logIdentifier: "PubSub",
},
{
condition: func() bool { return !isNil(c.Cassandra) },
setDS: func() { ds.Cassandra = cassandraDS{c.Cassandra} },
apply: func(m migrator) migrator { return cassandraDS{c.Cassandra}.apply(m) },
logIdentifier: "Cassandra",
},
{
condition: func() bool { return !isNil(c.Mongo) },
setDS: func() { ds.Mongo = mongoDS{c.Mongo} },
apply: func(m migrator) migrator { return mongoDS{c.Mongo}.apply(m) },
logIdentifier: "Mongo",
},
{
condition: func() bool { return !isNil(c.ArangoDB) },
setDS: func() { ds.ArangoDB = arangoDS{c.ArangoDB} },
apply: func(m migrator) migrator { return arangoDS{c.ArangoDB}.apply(m) },
logIdentifier: "ArangoDB",
},
{
condition: func() bool { return !isNil(c.SurrealDB) },
setDS: func() { ds.SurrealDB = surrealDS{c.SurrealDB} },
apply: func(m migrator) migrator { return surrealDS{c.SurrealDB}.apply(m) },
logIdentifier: "SurrealDB",
},
{
condition: func() bool { return !isNil(c.Elasticsearch) },
setDS: func() { ds.Elasticsearch = c.Elasticsearch },
apply: func(m migrator) migrator { return elasticsearchDS{c.Elasticsearch}.apply(m) },
logIdentifier: "Elasticsearch",
},
{
condition: func() bool { return !isNil(c.OpenTSDB) },
setDS: func() { ds.OpenTSDB = c.OpenTSDB },
apply: func(m migrator) migrator { return (&openTSDBMigrator{filePath: "gofr_migrations.json", migrator: m}) },
logIdentifier: "OpenTSDB",
},
{
condition: func() bool { return !isNil(c.ScyllaDB) },
setDS: func() { ds.ScyllaDB = c.ScyllaDB },
apply: func(m migrator) migrator { return scyllaDS{c.ScyllaDB}.apply(m) },
logIdentifier: "ScyllaDB",
},
}
}
func isNil(i any) bool {
if i == nil {
return true
}
val := reflect.ValueOf(i)
k := val.Kind()
if k == reflect.Ptr || k == reflect.Interface || k == reflect.Slice || k == reflect.Map || k == reflect.Chan || k == reflect.Func {
return val.IsNil()
}
return false
}
================================================
FILE: pkg/gofr/migration/migration_test.go
================================================
package migration
import (
"database/sql"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
var (
errRandomDB = errors.New("random db error")
errGenericCommit = errors.New("commit error")
)
func TestMigration_InvalidKeys(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
c, _ := container.NewMockContainer(t)
Run(map[int64]Migrate{
1: {UP: nil},
}, c)
})
assert.Contains(t, logs, "migration run failed! UP not defined for the following keys: [1]")
}
func TestMigration_NoDatasource(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
c := container.NewContainer(nil)
c.Logger = logging.NewLogger(logging.DEBUG)
Run(map[int64]Migrate{
1: {UP: func(d Datasource) error {
_, err := d.SQL.Exec("CREATE table customer(id int not null);")
if err != nil {
return err
}
return nil
}},
}, c)
})
assert.Contains(t, logs, "no migrations are running")
}
func Test_getMigratorDBInitialisation(t *testing.T) {
cntnr, _ := container.NewMockContainer(t)
datasource, _, isInitialized := getMigrator(cntnr)
assert.NotNil(t, datasource.SQL, "TEST Failed \nSQL not initialized, but should have been initialized")
assert.NotNil(t, datasource.Redis, "TEST Failed \nRedis not initialized, but should have been initialized")
assert.True(t, isInitialized, "TEST Failed \nNo datastores are Initialized")
}
func TestMigrationRunClickhouseSuccess(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Clickhouse.Exec(t.Context(), "SELECT * FROM users")
if err != nil {
return err
}
d.Logger.Infof("Clickhouse Migration Ran Successfully")
return nil
}},
}
mockClickHouse, mockContainer := initializeClickHouseRunMocks(t)
// Pre-check
mockClickHouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(nil)
mockClickHouse.EXPECT().Select(gomock.Any(), gomock.Any(), getLastChGoFrMigration).Return(nil).Times(2)
mockClickHouse.EXPECT().Exec(gomock.Any(), "SELECT * FROM users").Return(nil)
mockClickHouse.EXPECT().Exec(gomock.Any(), insertChGoFrMigrationRow, int64(1),
"UP", gomock.Any(), gomock.Any()).Return(nil)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "Migration 1 ran successfully")
assert.Contains(t, logs, "Clickhouse Migration Ran Successfully")
}
func TestMigrationRunClickhouseMigrationFailure(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
mockClickHouse, mockContainer := initializeClickHouseRunMocks(t)
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Clickhouse.Exec(t.Context(), "SELECT * FROM users")
if err != nil {
return err
}
return nil
}},
}
// Pre-check
mockClickHouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(nil)
mockClickHouse.EXPECT().Select(gomock.Any(), gomock.Any(), getLastChGoFrMigration).Return(nil).Times(2)
mockClickHouse.EXPECT().Exec(gomock.Any(), "SELECT * FROM users").Return(sql.ErrConnDone)
Run(migrationMap, mockContainer)
assert.True(t, mockClickHouse.ctrl.Satisfied())
})
assert.Contains(t, logs, "failed to run migration : [1], err: sql: connection is already closed")
}
func TestMigrationRunClickhouseMigrationFailureWhileCheckingTable(t *testing.T) {
mockClickHouse, mockContainer := initializeClickHouseRunMocks(t)
testutil.StderrOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Clickhouse.Exec(t.Context(), "SELECT * FROM users")
if err != nil {
return err
}
return nil
}},
}
// checkAndCreateMigrationTable is called first
mockClickHouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(sql.ErrConnDone)
Run(migrationMap, mockContainer)
})
assert.True(t, mockClickHouse.ctrl.Satisfied())
}
func TestMigrationRunClickhouseCurrentMigrationEqualLastMigration(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migrationMap := map[int64]Migrate{
0: {UP: func(d Datasource) error {
err := d.Clickhouse.Exec(t.Context(), "SELECT * FROM users")
if err != nil {
return err
}
return nil
}},
}
mockClickHouse, mockContainer := initializeClickHouseRunMocks(t)
mockClickHouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(nil)
mockClickHouse.EXPECT().Select(gomock.Any(), gomock.Any(), getLastChGoFrMigration).Return(nil)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "no new migrations to run")
}
func TestMigrationRunClickhouseCommitError(t *testing.T) {
logs := testutil.StderrOutputForFunc(func() {
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
err := d.Clickhouse.Exec(t.Context(), "SELECT * FROM users")
if err != nil {
return err
}
return nil
}},
}
mockClickHouse, mockContainer := initializeClickHouseRunMocks(t)
// Pre-check
mockClickHouse.EXPECT().Exec(gomock.Any(), CheckAndCreateChMigrationTable).Return(nil)
mockClickHouse.EXPECT().Select(gomock.Any(), gomock.Any(), getLastChGoFrMigration).Return(nil).Times(2)
mockClickHouse.EXPECT().Exec(gomock.Any(), "SELECT * FROM users").Return(nil)
mockClickHouse.EXPECT().Exec(gomock.Any(), insertChGoFrMigrationRow, int64(1),
"UP", gomock.Any(), gomock.Any()).Return(sql.ErrConnDone)
Run(migrationMap, mockContainer)
})
assert.Contains(t, logs, "failed to commit migration, err: sql: connection is already closed")
}
func initializeClickHouseRunMocks(t *testing.T) (*MockClickhouse, *container.Container) {
t.Helper()
mockClickHouse := NewMockClickhouse(gomock.NewController(t))
mockContainer, _ := container.NewMockContainer(t)
mockContainer.SQL = nil
mockContainer.Redis = nil
mockContainer.Mongo = nil
mockContainer.Cassandra = nil
mockContainer.PubSub = nil
mockContainer.ArangoDB = nil
mockContainer.SurrealDB = nil
mockContainer.DGraph = nil
mockContainer.Elasticsearch = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
mockContainer.Oracle = nil
mockContainer.Logger = logging.NewMockLogger(logging.DEBUG)
mockContainer.Clickhouse = mockClickHouse
return mockClickHouse, mockContainer
}
func TestMigration_SQLLockError(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
// Disable other datasources
mockContainer.Redis = nil
mockContainer.Cassandra = nil
mockContainer.Clickhouse = nil
mockContainer.Mongo = nil
mockContainer.ArangoDB = nil
mockContainer.Elasticsearch = nil
mockContainer.Oracle = nil
mockContainer.PubSub = nil
mockContainer.DGraph = nil
mockContainer.SurrealDB = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
ctrl := gomock.NewController(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error { return nil }},
}
createMigrations := `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createLocks := `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
// 1. checkAndCreateMigrationTable
mocks.SQL.ExpectExec(createMigrations).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec(createLocks).WillReturnResult(sqlmock.NewResult(0, 0))
// 2. getLastMigration
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(0))
// 3. lock fails with non-duplicate error
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg(), sqlmock.AnyArg()).
WillReturnError(errRandomDB)
// 4. unlock in defer
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(0, 0))
// Expectations for logger
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Fatalf(gomock.Any(), gomock.Any()).Times(1)
Run(migrationMap, mockContainer)
}
func TestMigration_CommitFailure(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
// Disable other datasources
mockContainer.Redis = nil
mockContainer.Cassandra = nil
mockContainer.Clickhouse = nil
mockContainer.Mongo = nil
mockContainer.ArangoDB = nil
mockContainer.Elasticsearch = nil
mockContainer.Oracle = nil
mockContainer.PubSub = nil
mockContainer.DGraph = nil
mockContainer.SurrealDB = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
ctrl := gomock.NewController(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error { return nil }},
}
createMigrations := `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createLocks := `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
// 1. checkAndCreateMigrationTable
mocks.SQL.ExpectExec(createMigrations).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec(createLocks).WillReturnResult(sqlmock.NewResult(0, 0))
// 2. getLastMigration
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(0))
// 3. lock
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
// 4. re-fetch getLastMigration under lock
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(0))
// 5. beginTransaction
mocks.SQL.ExpectBegin()
// 6. commitMigration fails
testErr := errGenericCommit
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES (?, ?, ?, ?);").
WillReturnResult(sqlmock.NewResult(1, 1))
mocks.SQL.ExpectCommit().WillReturnError(testErr)
// 7. rollback in runMigrations
mocks.SQL.ExpectRollback()
// 8. unlock in defer
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
// Expectations for logger
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Error(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Fatalf(gomock.Any(), gomock.Any()).AnyTimes()
Run(migrationMap, mockContainer)
}
func TestMigration_RaceCondition_SkipUnderLock(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
// Disable other datasources
mockContainer.Redis = nil
mockContainer.Cassandra = nil
mockContainer.Clickhouse = nil
mockContainer.Mongo = nil
mockContainer.ArangoDB = nil
mockContainer.Elasticsearch = nil
mockContainer.Oracle = nil
mockContainer.PubSub = nil
mockContainer.DGraph = nil
mockContainer.SurrealDB = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
ctrl := gomock.NewController(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error { return nil }},
}
createMigrations := `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createLocks := `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
// 1. checkAndCreateMigrationTable
mocks.SQL.ExpectExec(createMigrations).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec(createLocks).WillReturnResult(sqlmock.NewResult(0, 0))
// 2. pre-check getLastMigration returns 0 (migration pending)
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").
WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(0))
// 3. lock succeeds
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
// 4. re-fetch getLastMigration under lock returns 1 (another pod already ran it)
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").
WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(1))
// 5. unlock in defer (no migration was executed)
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?").
WithArgs("gofr_migrations_lock", sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
// Expectations for logger
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof(gomock.Any(), gomock.Any()).AnyTimes()
// This is the key assertion: "no new migrations to run (verified under lock)" should be logged
mockLogger.EXPECT().Info("no new migrations to run (verified under lock)").Times(1)
Run(migrationMap, mockContainer)
}
func Test_RunMigrations_SkipAlreadyRun(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
// Disable other datasources
mockContainer.Redis = nil
mockContainer.Cassandra = nil
mockContainer.Clickhouse = nil
mockContainer.Mongo = nil
mockContainer.ArangoDB = nil
mockContainer.Elasticsearch = nil
mockContainer.Oracle = nil
mockContainer.PubSub = nil
mockContainer.DGraph = nil
mockContainer.SurrealDB = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
ctrl := gomock.NewController(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
migrationMap := map[int64]Migrate{
1: {UP: func(_ Datasource) error { return nil }},
}
createMigrations := `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createLocks := `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
// 1. checkAndCreateMigrationTable
mocks.SQL.ExpectExec(createMigrations).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec(createLocks).WillReturnResult(sqlmock.NewResult(0, 0))
// 2. getLastMigration returns 1
mocks.SQL.ExpectQuery("SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;").WillReturnRows(sqlmock.NewRows([]string{"MAX"}).AddRow(1))
// Expectations for logger
mockLogger.EXPECT().Debug(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Debugf(gomock.Any(), gomock.Any()).AnyTimes()
mockLogger.EXPECT().Info(gomock.Any()).AnyTimes()
mockLogger.EXPECT().Infof(gomock.Any(), gomock.Any()).AnyTimes()
Run(migrationMap, mockContainer)
}
================================================
FILE: pkg/gofr/migration/mock_interface.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: interface.go
//
// Generated by this command:
//
// mockgen -source=interface.go -destination=mock_interface.go -package=migration
//
// Package migration is a generated GoMock package.
package migration
import (
context "context"
sql "database/sql"
reflect "reflect"
time "time"
redis "github.com/redis/go-redis/v9"
gomock "go.uber.org/mock/gomock"
container "gofr.dev/pkg/gofr/container"
)
// MockRedis is a mock of Redis interface.
type MockRedis struct {
ctrl *gomock.Controller
recorder *MockRedisMockRecorder
isgomock struct{}
}
// MockRedisMockRecorder is the mock recorder for MockRedis.
type MockRedisMockRecorder struct {
mock *MockRedis
}
// NewMockRedis creates a new mock instance.
func NewMockRedis(ctrl *gomock.Controller) *MockRedis {
mock := &MockRedis{ctrl: ctrl}
mock.recorder = &MockRedisMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRedis) EXPECT() *MockRedisMockRecorder {
return m.recorder
}
// Del mocks base method.
func (m *MockRedis) Del(ctx context.Context, keys ...string) *redis.IntCmd {
m.ctrl.T.Helper()
varargs := []any{ctx}
for _, a := range keys {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Del", varargs...)
ret0, _ := ret[0].(*redis.IntCmd)
return ret0
}
// Del indicates an expected call of Del.
func (mr *MockRedisMockRecorder) Del(ctx any, keys ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx}, keys...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Del", reflect.TypeOf((*MockRedis)(nil).Del), varargs...)
}
// Get mocks base method.
func (m *MockRedis) Get(ctx context.Context, key string) *redis.StringCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, key)
ret0, _ := ret[0].(*redis.StringCmd)
return ret0
}
// Get indicates an expected call of Get.
func (mr *MockRedisMockRecorder) Get(ctx, key any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRedis)(nil).Get), ctx, key)
}
// Rename mocks base method.
func (m *MockRedis) Rename(ctx context.Context, key, newKey string) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Rename", ctx, key, newKey)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Rename indicates an expected call of Rename.
func (mr *MockRedisMockRecorder) Rename(ctx, key, newKey any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockRedis)(nil).Rename), ctx, key, newKey)
}
// Set mocks base method.
func (m *MockRedis) Set(ctx context.Context, key string, value any, expiration time.Duration) *redis.StatusCmd {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Set", ctx, key, value, expiration)
ret0, _ := ret[0].(*redis.StatusCmd)
return ret0
}
// Set indicates an expected call of Set.
func (mr *MockRedisMockRecorder) Set(ctx, key, value, expiration any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockRedis)(nil).Set), ctx, key, value, expiration)
}
// MockSQL is a mock of SQL interface.
type MockSQL struct {
ctrl *gomock.Controller
recorder *MockSQLMockRecorder
isgomock struct{}
}
// MockSQLMockRecorder is the mock recorder for MockSQL.
type MockSQLMockRecorder struct {
mock *MockSQL
}
// NewMockSQL creates a new mock instance.
func NewMockSQL(ctrl *gomock.Controller) *MockSQL {
mock := &MockSQL{ctrl: ctrl}
mock.recorder = &MockSQLMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSQL) EXPECT() *MockSQLMockRecorder {
return m.recorder
}
// Exec mocks base method.
func (m *MockSQL) Exec(query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Exec indicates an expected call of Exec.
func (mr *MockSQLMockRecorder) Exec(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockSQL)(nil).Exec), varargs...)
}
// ExecContext mocks base method.
func (m *MockSQL) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecContext", varargs...)
ret0, _ := ret[0].(sql.Result)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecContext indicates an expected call of ExecContext.
func (mr *MockSQLMockRecorder) ExecContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecContext", reflect.TypeOf((*MockSQL)(nil).ExecContext), varargs...)
}
// Query mocks base method.
func (m *MockSQL) Query(query string, args ...any) (*sql.Rows, error) {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(*sql.Rows)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockSQLMockRecorder) Query(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockSQL)(nil).Query), varargs...)
}
// QueryRow mocks base method.
func (m *MockSQL) QueryRow(query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRow", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRow indicates an expected call of QueryRow.
func (mr *MockSQLMockRecorder) QueryRow(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRow", reflect.TypeOf((*MockSQL)(nil).QueryRow), varargs...)
}
// QueryRowContext mocks base method.
func (m *MockSQL) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryRowContext", varargs...)
ret0, _ := ret[0].(*sql.Row)
return ret0
}
// QueryRowContext indicates an expected call of QueryRowContext.
func (mr *MockSQLMockRecorder) QueryRowContext(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRowContext", reflect.TypeOf((*MockSQL)(nil).QueryRowContext), varargs...)
}
// MockPubSub is a mock of PubSub interface.
type MockPubSub struct {
ctrl *gomock.Controller
recorder *MockPubSubMockRecorder
isgomock struct{}
}
// MockPubSubMockRecorder is the mock recorder for MockPubSub.
type MockPubSubMockRecorder struct {
mock *MockPubSub
}
// NewMockPubSub creates a new mock instance.
func NewMockPubSub(ctrl *gomock.Controller) *MockPubSub {
mock := &MockPubSub{ctrl: ctrl}
mock.recorder = &MockPubSubMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockPubSub) EXPECT() *MockPubSubMockRecorder {
return m.recorder
}
// CreateTopic mocks base method.
func (m *MockPubSub) CreateTopic(arg0 context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateTopic", arg0, name)
ret0, _ := ret[0].(error)
return ret0
}
// CreateTopic indicates an expected call of CreateTopic.
func (mr *MockPubSubMockRecorder) CreateTopic(arg0, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTopic", reflect.TypeOf((*MockPubSub)(nil).CreateTopic), arg0, name)
}
// DeleteTopic mocks base method.
func (m *MockPubSub) DeleteTopic(arg0 context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteTopic", arg0, name)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteTopic indicates an expected call of DeleteTopic.
func (mr *MockPubSubMockRecorder) DeleteTopic(arg0, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTopic", reflect.TypeOf((*MockPubSub)(nil).DeleteTopic), arg0, name)
}
// Query mocks base method.
func (m *MockPubSub) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockPubSubMockRecorder) Query(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockPubSub)(nil).Query), varargs...)
}
// MockClickhouse is a mock of Clickhouse interface.
type MockClickhouse struct {
ctrl *gomock.Controller
recorder *MockClickhouseMockRecorder
isgomock struct{}
}
// MockClickhouseMockRecorder is the mock recorder for MockClickhouse.
type MockClickhouseMockRecorder struct {
mock *MockClickhouse
}
// NewMockClickhouse creates a new mock instance.
func NewMockClickhouse(ctrl *gomock.Controller) *MockClickhouse {
mock := &MockClickhouse{ctrl: ctrl}
mock.recorder = &MockClickhouseMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClickhouse) EXPECT() *MockClickhouseMockRecorder {
return m.recorder
}
// AsyncInsert mocks base method.
func (m *MockClickhouse) AsyncInsert(ctx context.Context, query string, wait bool, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query, wait}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "AsyncInsert", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// AsyncInsert indicates an expected call of AsyncInsert.
func (mr *MockClickhouseMockRecorder) AsyncInsert(ctx, query, wait any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query, wait}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AsyncInsert", reflect.TypeOf((*MockClickhouse)(nil).AsyncInsert), varargs...)
}
// Exec mocks base method.
func (m *MockClickhouse) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockClickhouseMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockClickhouse)(nil).Exec), varargs...)
}
// HealthCheck mocks base method.
func (m *MockClickhouse) HealthCheck(ctx context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", ctx)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockClickhouseMockRecorder) HealthCheck(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockClickhouse)(nil).HealthCheck), ctx)
}
// Select mocks base method.
func (m *MockClickhouse) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockClickhouseMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockClickhouse)(nil).Select), varargs...)
}
// MockOracle is a mock of Oracle interface.
type MockOracle struct {
ctrl *gomock.Controller
recorder *MockOracleMockRecorder
isgomock struct{}
}
// MockOracleMockRecorder is the mock recorder for MockOracle.
type MockOracleMockRecorder struct {
mock *MockOracle
}
// NewMockOracle creates a new mock instance.
func NewMockOracle(ctrl *gomock.Controller) *MockOracle {
mock := &MockOracle{ctrl: ctrl}
mock.recorder = &MockOracleMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOracle) EXPECT() *MockOracleMockRecorder {
return m.recorder
}
// Begin mocks base method.
func (m *MockOracle) Begin() (container.OracleTx, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Begin")
ret0, _ := ret[0].(container.OracleTx)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Begin indicates an expected call of Begin.
func (mr *MockOracleMockRecorder) Begin() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Begin", reflect.TypeOf((*MockOracle)(nil).Begin))
}
// Exec mocks base method.
func (m *MockOracle) Exec(ctx context.Context, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockOracleMockRecorder) Exec(ctx, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockOracle)(nil).Exec), varargs...)
}
// Select mocks base method.
func (m *MockOracle) Select(ctx context.Context, dest any, query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Select", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Select indicates an expected call of Select.
func (mr *MockOracleMockRecorder) Select(ctx, dest, query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockOracle)(nil).Select), varargs...)
}
// MockCassandra is a mock of Cassandra interface.
type MockCassandra struct {
ctrl *gomock.Controller
recorder *MockCassandraMockRecorder
isgomock struct{}
}
// MockCassandraMockRecorder is the mock recorder for MockCassandra.
type MockCassandraMockRecorder struct {
mock *MockCassandra
}
// NewMockCassandra creates a new mock instance.
func NewMockCassandra(ctrl *gomock.Controller) *MockCassandra {
mock := &MockCassandra{ctrl: ctrl}
mock.recorder = &MockCassandraMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCassandra) EXPECT() *MockCassandraMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockCassandra) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockCassandraMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockCassandra)(nil).BatchQuery), varargs...)
}
// Exec mocks base method.
func (m *MockCassandra) Exec(query string, args ...any) error {
m.ctrl.T.Helper()
varargs := []any{query}
for _, a := range args {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockCassandraMockRecorder) Exec(query any, args ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{query}, args...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockCassandra)(nil).Exec), varargs...)
}
// ExecuteBatch mocks base method.
func (m *MockCassandra) ExecuteBatch(name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatch", name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatch indicates an expected call of ExecuteBatch.
func (mr *MockCassandraMockRecorder) ExecuteBatch(name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatch", reflect.TypeOf((*MockCassandra)(nil).ExecuteBatch), name)
}
// HealthCheck mocks base method.
func (m *MockCassandra) HealthCheck(ctx context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", ctx)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockCassandraMockRecorder) HealthCheck(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockCassandra)(nil).HealthCheck), ctx)
}
// NewBatch mocks base method.
func (m *MockCassandra) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockCassandraMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockCassandra)(nil).NewBatch), name, batchType)
}
// MockMongo is a mock of Mongo interface.
type MockMongo struct {
ctrl *gomock.Controller
recorder *MockMongoMockRecorder
isgomock struct{}
}
// MockMongoMockRecorder is the mock recorder for MockMongo.
type MockMongoMockRecorder struct {
mock *MockMongo
}
// NewMockMongo creates a new mock instance.
func NewMockMongo(ctrl *gomock.Controller) *MockMongo {
mock := &MockMongo{ctrl: ctrl}
mock.recorder = &MockMongoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMongo) EXPECT() *MockMongoMockRecorder {
return m.recorder
}
// CreateCollection mocks base method.
func (m *MockMongo) CreateCollection(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockMongoMockRecorder) CreateCollection(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockMongo)(nil).CreateCollection), ctx, name)
}
// DeleteMany mocks base method.
func (m *MockMongo) DeleteMany(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteMany", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteMany indicates an expected call of DeleteMany.
func (mr *MockMongoMockRecorder) DeleteMany(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockMongo)(nil).DeleteMany), ctx, collection, filter)
}
// DeleteOne mocks base method.
func (m *MockMongo) DeleteOne(ctx context.Context, collection string, filter any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteOne", ctx, collection, filter)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteOne indicates an expected call of DeleteOne.
func (mr *MockMongoMockRecorder) DeleteOne(ctx, collection, filter any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOne", reflect.TypeOf((*MockMongo)(nil).DeleteOne), ctx, collection, filter)
}
// Drop mocks base method.
func (m *MockMongo) Drop(ctx context.Context, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Drop", ctx, collection)
ret0, _ := ret[0].(error)
return ret0
}
// Drop indicates an expected call of Drop.
func (mr *MockMongoMockRecorder) Drop(ctx, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Drop", reflect.TypeOf((*MockMongo)(nil).Drop), ctx, collection)
}
// Find mocks base method.
func (m *MockMongo) Find(ctx context.Context, collection string, filter, results any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Find", ctx, collection, filter, results)
ret0, _ := ret[0].(error)
return ret0
}
// Find indicates an expected call of Find.
func (mr *MockMongoMockRecorder) Find(ctx, collection, filter, results any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Find", reflect.TypeOf((*MockMongo)(nil).Find), ctx, collection, filter, results)
}
// FindOne mocks base method.
func (m *MockMongo) FindOne(ctx context.Context, collection string, filter, result any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "FindOne", ctx, collection, filter, result)
ret0, _ := ret[0].(error)
return ret0
}
// FindOne indicates an expected call of FindOne.
func (mr *MockMongoMockRecorder) FindOne(ctx, collection, filter, result any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOne", reflect.TypeOf((*MockMongo)(nil).FindOne), ctx, collection, filter, result)
}
// InsertMany mocks base method.
func (m *MockMongo) InsertMany(ctx context.Context, collection string, documents []any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertMany", ctx, collection, documents)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertMany indicates an expected call of InsertMany.
func (mr *MockMongoMockRecorder) InsertMany(ctx, collection, documents any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertMany", reflect.TypeOf((*MockMongo)(nil).InsertMany), ctx, collection, documents)
}
// InsertOne mocks base method.
func (m *MockMongo) InsertOne(ctx context.Context, collection string, document any) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "InsertOne", ctx, collection, document)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// InsertOne indicates an expected call of InsertOne.
func (mr *MockMongoMockRecorder) InsertOne(ctx, collection, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertOne", reflect.TypeOf((*MockMongo)(nil).InsertOne), ctx, collection, document)
}
// StartSession mocks base method.
func (m *MockMongo) StartSession() (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StartSession")
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StartSession indicates an expected call of StartSession.
func (mr *MockMongoMockRecorder) StartSession() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartSession", reflect.TypeOf((*MockMongo)(nil).StartSession))
}
// UpdateByID mocks base method.
func (m *MockMongo) UpdateByID(ctx context.Context, collection string, id, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateByID", ctx, collection, id, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateByID indicates an expected call of UpdateByID.
func (mr *MockMongoMockRecorder) UpdateByID(ctx, collection, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateByID", reflect.TypeOf((*MockMongo)(nil).UpdateByID), ctx, collection, id, update)
}
// UpdateMany mocks base method.
func (m *MockMongo) UpdateMany(ctx context.Context, collection string, filter, update any) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateMany", ctx, collection, filter, update)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// UpdateMany indicates an expected call of UpdateMany.
func (mr *MockMongoMockRecorder) UpdateMany(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateMany", reflect.TypeOf((*MockMongo)(nil).UpdateMany), ctx, collection, filter, update)
}
// UpdateOne mocks base method.
func (m *MockMongo) UpdateOne(ctx context.Context, collection string, filter, update any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateOne", ctx, collection, filter, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateOne indicates an expected call of UpdateOne.
func (mr *MockMongoMockRecorder) UpdateOne(ctx, collection, filter, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateOne", reflect.TypeOf((*MockMongo)(nil).UpdateOne), ctx, collection, filter, update)
}
// MockArangoDB is a mock of ArangoDB interface.
type MockArangoDB struct {
ctrl *gomock.Controller
recorder *MockArangoDBMockRecorder
isgomock struct{}
}
// MockArangoDBMockRecorder is the mock recorder for MockArangoDB.
type MockArangoDBMockRecorder struct {
mock *MockArangoDB
}
// NewMockArangoDB creates a new mock instance.
func NewMockArangoDB(ctrl *gomock.Controller) *MockArangoDB {
mock := &MockArangoDB{ctrl: ctrl}
mock.recorder = &MockArangoDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockArangoDB) EXPECT() *MockArangoDBMockRecorder {
return m.recorder
}
// CreateCollection mocks base method.
func (m *MockArangoDB) CreateCollection(ctx context.Context, database, collection string, isEdge bool) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateCollection", ctx, database, collection, isEdge)
ret0, _ := ret[0].(error)
return ret0
}
// CreateCollection indicates an expected call of CreateCollection.
func (mr *MockArangoDBMockRecorder) CreateCollection(ctx, database, collection, isEdge any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateCollection", reflect.TypeOf((*MockArangoDB)(nil).CreateCollection), ctx, database, collection, isEdge)
}
// CreateDB mocks base method.
func (m *MockArangoDB) CreateDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDB indicates an expected call of CreateDB.
func (mr *MockArangoDBMockRecorder) CreateDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDB", reflect.TypeOf((*MockArangoDB)(nil).CreateDB), ctx, database)
}
// CreateGraph mocks base method.
func (m *MockArangoDB) CreateGraph(ctx context.Context, database, graph string, edgeDefinitions any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateGraph", ctx, database, graph, edgeDefinitions)
ret0, _ := ret[0].(error)
return ret0
}
// CreateGraph indicates an expected call of CreateGraph.
func (mr *MockArangoDBMockRecorder) CreateGraph(ctx, database, graph, edgeDefinitions any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGraph", reflect.TypeOf((*MockArangoDB)(nil).CreateGraph), ctx, database, graph, edgeDefinitions)
}
// DropCollection mocks base method.
func (m *MockArangoDB) DropCollection(ctx context.Context, database, collection string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropCollection", ctx, database, collection)
ret0, _ := ret[0].(error)
return ret0
}
// DropCollection indicates an expected call of DropCollection.
func (mr *MockArangoDBMockRecorder) DropCollection(ctx, database, collection any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropCollection", reflect.TypeOf((*MockArangoDB)(nil).DropCollection), ctx, database, collection)
}
// DropDB mocks base method.
func (m *MockArangoDB) DropDB(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDB", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDB indicates an expected call of DropDB.
func (mr *MockArangoDBMockRecorder) DropDB(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDB", reflect.TypeOf((*MockArangoDB)(nil).DropDB), ctx, database)
}
// DropGraph mocks base method.
func (m *MockArangoDB) DropGraph(ctx context.Context, database, graph string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropGraph", ctx, database, graph)
ret0, _ := ret[0].(error)
return ret0
}
// DropGraph indicates an expected call of DropGraph.
func (mr *MockArangoDBMockRecorder) DropGraph(ctx, database, graph any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropGraph", reflect.TypeOf((*MockArangoDB)(nil).DropGraph), ctx, database, graph)
}
// MockSurrealDB is a mock of SurrealDB interface.
type MockSurrealDB struct {
ctrl *gomock.Controller
recorder *MockSurrealDBMockRecorder
isgomock struct{}
}
// MockSurrealDBMockRecorder is the mock recorder for MockSurrealDB.
type MockSurrealDBMockRecorder struct {
mock *MockSurrealDB
}
// NewMockSurrealDB creates a new mock instance.
func NewMockSurrealDB(ctrl *gomock.Controller) *MockSurrealDB {
mock := &MockSurrealDB{ctrl: ctrl}
mock.recorder = &MockSurrealDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockSurrealDB) EXPECT() *MockSurrealDBMockRecorder {
return m.recorder
}
// CreateDatabase mocks base method.
func (m *MockSurrealDB) CreateDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// CreateDatabase indicates an expected call of CreateDatabase.
func (mr *MockSurrealDBMockRecorder) CreateDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateDatabase", reflect.TypeOf((*MockSurrealDB)(nil).CreateDatabase), ctx, database)
}
// CreateNamespace mocks base method.
func (m *MockSurrealDB) CreateNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// CreateNamespace indicates an expected call of CreateNamespace.
func (mr *MockSurrealDBMockRecorder) CreateNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNamespace", reflect.TypeOf((*MockSurrealDB)(nil).CreateNamespace), ctx, namespace)
}
// DropDatabase mocks base method.
func (m *MockSurrealDB) DropDatabase(ctx context.Context, database string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropDatabase", ctx, database)
ret0, _ := ret[0].(error)
return ret0
}
// DropDatabase indicates an expected call of DropDatabase.
func (mr *MockSurrealDBMockRecorder) DropDatabase(ctx, database any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropDatabase", reflect.TypeOf((*MockSurrealDB)(nil).DropDatabase), ctx, database)
}
// DropNamespace mocks base method.
func (m *MockSurrealDB) DropNamespace(ctx context.Context, namespace string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropNamespace", ctx, namespace)
ret0, _ := ret[0].(error)
return ret0
}
// DropNamespace indicates an expected call of DropNamespace.
func (mr *MockSurrealDBMockRecorder) DropNamespace(ctx, namespace any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropNamespace", reflect.TypeOf((*MockSurrealDB)(nil).DropNamespace), ctx, namespace)
}
// Query mocks base method.
func (m *MockSurrealDB) Query(ctx context.Context, query string, vars map[string]any) ([]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Query", ctx, query, vars)
ret0, _ := ret[0].([]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Query indicates an expected call of Query.
func (mr *MockSurrealDBMockRecorder) Query(ctx, query, vars any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockSurrealDB)(nil).Query), ctx, query, vars)
}
// MockDGraph is a mock of DGraph interface.
type MockDGraph struct {
ctrl *gomock.Controller
recorder *MockDGraphMockRecorder
isgomock struct{}
}
// MockDGraphMockRecorder is the mock recorder for MockDGraph.
type MockDGraphMockRecorder struct {
mock *MockDGraph
}
// NewMockDGraph creates a new mock instance.
func NewMockDGraph(ctrl *gomock.Controller) *MockDGraph {
mock := &MockDGraph{ctrl: ctrl}
mock.recorder = &MockDGraphMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockDGraph) EXPECT() *MockDGraphMockRecorder {
return m.recorder
}
// AddOrUpdateField mocks base method.
func (m *MockDGraph) AddOrUpdateField(ctx context.Context, fieldName, fieldType, directives string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AddOrUpdateField", ctx, fieldName, fieldType, directives)
ret0, _ := ret[0].(error)
return ret0
}
// AddOrUpdateField indicates an expected call of AddOrUpdateField.
func (mr *MockDGraphMockRecorder) AddOrUpdateField(ctx, fieldName, fieldType, directives any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddOrUpdateField", reflect.TypeOf((*MockDGraph)(nil).AddOrUpdateField), ctx, fieldName, fieldType, directives)
}
// ApplySchema mocks base method.
func (m *MockDGraph) ApplySchema(ctx context.Context, schema string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ApplySchema", ctx, schema)
ret0, _ := ret[0].(error)
return ret0
}
// ApplySchema indicates an expected call of ApplySchema.
func (mr *MockDGraphMockRecorder) ApplySchema(ctx, schema any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplySchema", reflect.TypeOf((*MockDGraph)(nil).ApplySchema), ctx, schema)
}
// DropField mocks base method.
func (m *MockDGraph) DropField(ctx context.Context, fieldName string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DropField", ctx, fieldName)
ret0, _ := ret[0].(error)
return ret0
}
// DropField indicates an expected call of DropField.
func (mr *MockDGraphMockRecorder) DropField(ctx, fieldName any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DropField", reflect.TypeOf((*MockDGraph)(nil).DropField), ctx, fieldName)
}
// MockScyllaDB is a mock of ScyllaDB interface.
type MockScyllaDB struct {
ctrl *gomock.Controller
recorder *MockScyllaDBMockRecorder
isgomock struct{}
}
// MockScyllaDBMockRecorder is the mock recorder for MockScyllaDB.
type MockScyllaDBMockRecorder struct {
mock *MockScyllaDB
}
// NewMockScyllaDB creates a new mock instance.
func NewMockScyllaDB(ctrl *gomock.Controller) *MockScyllaDB {
mock := &MockScyllaDB{ctrl: ctrl}
mock.recorder = &MockScyllaDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockScyllaDB) EXPECT() *MockScyllaDBMockRecorder {
return m.recorder
}
// BatchQuery mocks base method.
func (m *MockScyllaDB) BatchQuery(name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQuery", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQuery indicates an expected call of BatchQuery.
func (mr *MockScyllaDBMockRecorder) BatchQuery(name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQuery", reflect.TypeOf((*MockScyllaDB)(nil).BatchQuery), varargs...)
}
// BatchQueryWithCtx mocks base method.
func (m *MockScyllaDB) BatchQueryWithCtx(ctx context.Context, name, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, name, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "BatchQueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// BatchQueryWithCtx indicates an expected call of BatchQueryWithCtx.
func (mr *MockScyllaDBMockRecorder) BatchQueryWithCtx(ctx, name, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchQueryWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).BatchQueryWithCtx), varargs...)
}
// Exec mocks base method.
func (m *MockScyllaDB) Exec(stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Exec", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Exec indicates an expected call of Exec.
func (mr *MockScyllaDBMockRecorder) Exec(stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exec", reflect.TypeOf((*MockScyllaDB)(nil).Exec), varargs...)
}
// ExecCAS mocks base method.
func (m *MockScyllaDB) ExecCAS(dest any, stmt string, values ...any) (bool, error) {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecCAS", varargs...)
ret0, _ := ret[0].(bool)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ExecCAS indicates an expected call of ExecCAS.
func (mr *MockScyllaDBMockRecorder) ExecCAS(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecCAS", reflect.TypeOf((*MockScyllaDB)(nil).ExecCAS), varargs...)
}
// ExecWithCtx mocks base method.
func (m *MockScyllaDB) ExecWithCtx(ctx context.Context, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "ExecWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// ExecWithCtx indicates an expected call of ExecWithCtx.
func (mr *MockScyllaDBMockRecorder) ExecWithCtx(ctx, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).ExecWithCtx), varargs...)
}
// ExecuteBatchWithCtx mocks base method.
func (m *MockScyllaDB) ExecuteBatchWithCtx(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ExecuteBatchWithCtx", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// ExecuteBatchWithCtx indicates an expected call of ExecuteBatchWithCtx.
func (mr *MockScyllaDBMockRecorder) ExecuteBatchWithCtx(ctx, name any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteBatchWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).ExecuteBatchWithCtx), ctx, name)
}
// NewBatch mocks base method.
func (m *MockScyllaDB) NewBatch(name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatch", name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatch indicates an expected call of NewBatch.
func (mr *MockScyllaDBMockRecorder) NewBatch(name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatch", reflect.TypeOf((*MockScyllaDB)(nil).NewBatch), name, batchType)
}
// NewBatchWithCtx mocks base method.
func (m *MockScyllaDB) NewBatchWithCtx(ctx context.Context, name string, batchType int) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewBatchWithCtx", ctx, name, batchType)
ret0, _ := ret[0].(error)
return ret0
}
// NewBatchWithCtx indicates an expected call of NewBatchWithCtx.
func (mr *MockScyllaDBMockRecorder) NewBatchWithCtx(ctx, name, batchType any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewBatchWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).NewBatchWithCtx), ctx, name, batchType)
}
// Query mocks base method.
func (m *MockScyllaDB) Query(dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "Query", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// Query indicates an expected call of Query.
func (mr *MockScyllaDBMockRecorder) Query(dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockScyllaDB)(nil).Query), varargs...)
}
// QueryWithCtx mocks base method.
func (m *MockScyllaDB) QueryWithCtx(ctx context.Context, dest any, stmt string, values ...any) error {
m.ctrl.T.Helper()
varargs := []any{ctx, dest, stmt}
for _, a := range values {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "QueryWithCtx", varargs...)
ret0, _ := ret[0].(error)
return ret0
}
// QueryWithCtx indicates an expected call of QueryWithCtx.
func (mr *MockScyllaDBMockRecorder) QueryWithCtx(ctx, dest, stmt any, values ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, dest, stmt}, values...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithCtx", reflect.TypeOf((*MockScyllaDB)(nil).QueryWithCtx), varargs...)
}
// MockElasticsearch is a mock of Elasticsearch interface.
type MockElasticsearch struct {
ctrl *gomock.Controller
recorder *MockElasticsearchMockRecorder
isgomock struct{}
}
// MockElasticsearchMockRecorder is the mock recorder for MockElasticsearch.
type MockElasticsearchMockRecorder struct {
mock *MockElasticsearch
}
// NewMockElasticsearch creates a new mock instance.
func NewMockElasticsearch(ctrl *gomock.Controller) *MockElasticsearch {
mock := &MockElasticsearch{ctrl: ctrl}
mock.recorder = &MockElasticsearchMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockElasticsearch) EXPECT() *MockElasticsearchMockRecorder {
return m.recorder
}
// Bulk mocks base method.
func (m *MockElasticsearch) Bulk(ctx context.Context, operations []map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bulk", ctx, operations)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Bulk indicates an expected call of Bulk.
func (mr *MockElasticsearchMockRecorder) Bulk(ctx, operations any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bulk", reflect.TypeOf((*MockElasticsearch)(nil).Bulk), ctx, operations)
}
// CreateIndex mocks base method.
func (m *MockElasticsearch) CreateIndex(ctx context.Context, index string, settings map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateIndex", ctx, index, settings)
ret0, _ := ret[0].(error)
return ret0
}
// CreateIndex indicates an expected call of CreateIndex.
func (mr *MockElasticsearchMockRecorder) CreateIndex(ctx, index, settings any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateIndex", reflect.TypeOf((*MockElasticsearch)(nil).CreateIndex), ctx, index, settings)
}
// DeleteDocument mocks base method.
func (m *MockElasticsearch) DeleteDocument(ctx context.Context, index, id string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteDocument", ctx, index, id)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteDocument indicates an expected call of DeleteDocument.
func (mr *MockElasticsearchMockRecorder) DeleteDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteDocument", reflect.TypeOf((*MockElasticsearch)(nil).DeleteDocument), ctx, index, id)
}
// DeleteIndex mocks base method.
func (m *MockElasticsearch) DeleteIndex(ctx context.Context, index string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteIndex", ctx, index)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteIndex indicates an expected call of DeleteIndex.
func (mr *MockElasticsearchMockRecorder) DeleteIndex(ctx, index any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIndex", reflect.TypeOf((*MockElasticsearch)(nil).DeleteIndex), ctx, index)
}
// GetDocument mocks base method.
func (m *MockElasticsearch) GetDocument(ctx context.Context, index, id string) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDocument", ctx, index, id)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetDocument indicates an expected call of GetDocument.
func (mr *MockElasticsearchMockRecorder) GetDocument(ctx, index, id any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDocument", reflect.TypeOf((*MockElasticsearch)(nil).GetDocument), ctx, index, id)
}
// HealthCheck mocks base method.
func (m *MockElasticsearch) HealthCheck(ctx context.Context) (any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", ctx)
ret0, _ := ret[0].(any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockElasticsearchMockRecorder) HealthCheck(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockElasticsearch)(nil).HealthCheck), ctx)
}
// IndexDocument mocks base method.
func (m *MockElasticsearch) IndexDocument(ctx context.Context, index, id string, document any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IndexDocument", ctx, index, id, document)
ret0, _ := ret[0].(error)
return ret0
}
// IndexDocument indicates an expected call of IndexDocument.
func (mr *MockElasticsearchMockRecorder) IndexDocument(ctx, index, id, document any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexDocument", reflect.TypeOf((*MockElasticsearch)(nil).IndexDocument), ctx, index, id, document)
}
// Search mocks base method.
func (m *MockElasticsearch) Search(ctx context.Context, indices []string, query map[string]any) (map[string]any, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", ctx, indices, query)
ret0, _ := ret[0].(map[string]any)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search.
func (mr *MockElasticsearchMockRecorder) Search(ctx, indices, query any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockElasticsearch)(nil).Search), ctx, indices, query)
}
// UpdateDocument mocks base method.
func (m *MockElasticsearch) UpdateDocument(ctx context.Context, index, id string, update map[string]any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateDocument", ctx, index, id, update)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateDocument indicates an expected call of UpdateDocument.
func (mr *MockElasticsearchMockRecorder) UpdateDocument(ctx, index, id, update any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDocument", reflect.TypeOf((*MockElasticsearch)(nil).UpdateDocument), ctx, index, id, update)
}
// Mockmigrator is a mock of migrator interface.
type Mockmigrator struct {
ctrl *gomock.Controller
recorder *MockmigratorMockRecorder
isgomock struct{}
}
// MockmigratorMockRecorder is the mock recorder for Mockmigrator.
type MockmigratorMockRecorder struct {
mock *Mockmigrator
}
// NewMockmigrator creates a new mock instance.
func NewMockmigrator(ctrl *gomock.Controller) *Mockmigrator {
mock := &Mockmigrator{ctrl: ctrl}
mock.recorder = &MockmigratorMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockmigrator) EXPECT() *MockmigratorMockRecorder {
return m.recorder
}
// beginTransaction mocks base method.
func (m *Mockmigrator) beginTransaction(c *container.Container) transactionData {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "beginTransaction", c)
ret0, _ := ret[0].(transactionData)
return ret0
}
// beginTransaction indicates an expected call of beginTransaction.
func (mr *MockmigratorMockRecorder) beginTransaction(c any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "beginTransaction", reflect.TypeOf((*Mockmigrator)(nil).beginTransaction), c)
}
// checkAndCreateMigrationTable mocks base method.
func (m *Mockmigrator) checkAndCreateMigrationTable(c *container.Container) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "checkAndCreateMigrationTable", c)
ret0, _ := ret[0].(error)
return ret0
}
// checkAndCreateMigrationTable indicates an expected call of checkAndCreateMigrationTable.
func (mr *MockmigratorMockRecorder) checkAndCreateMigrationTable(c any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "checkAndCreateMigrationTable", reflect.TypeOf((*Mockmigrator)(nil).checkAndCreateMigrationTable), c)
}
// commitMigration mocks base method.
func (m *Mockmigrator) commitMigration(c *container.Container, data transactionData) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "commitMigration", c, data)
ret0, _ := ret[0].(error)
return ret0
}
// commitMigration indicates an expected call of commitMigration.
func (mr *MockmigratorMockRecorder) commitMigration(c, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "commitMigration", reflect.TypeOf((*Mockmigrator)(nil).commitMigration), c, data)
}
// getLastMigration mocks base method.
func (m *Mockmigrator) getLastMigration(c *container.Container) (int64, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "getLastMigration", c)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// getLastMigration indicates an expected call of getLastMigration.
func (mr *MockmigratorMockRecorder) getLastMigration(c any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getLastMigration", reflect.TypeOf((*Mockmigrator)(nil).getLastMigration), c)
}
// lock mocks base method.
func (m *Mockmigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "lock", ctx, cancel, c, ownerID)
ret0, _ := ret[0].(error)
return ret0
}
// lock indicates an expected call of lock.
func (mr *MockmigratorMockRecorder) lock(ctx, cancel, c, ownerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "lock", reflect.TypeOf((*Mockmigrator)(nil).lock), ctx, cancel, c, ownerID)
}
// name mocks base method.
func (m *Mockmigrator) name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "name")
ret0, _ := ret[0].(string)
return ret0
}
// name indicates an expected call of name.
func (mr *MockmigratorMockRecorder) name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "name", reflect.TypeOf((*Mockmigrator)(nil).name))
}
// rollback mocks base method.
func (m *Mockmigrator) rollback(c *container.Container, data transactionData) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "rollback", c, data)
}
// rollback indicates an expected call of rollback.
func (mr *MockmigratorMockRecorder) rollback(c, data any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "rollback", reflect.TypeOf((*Mockmigrator)(nil).rollback), c, data)
}
// unlock mocks base method.
func (m *Mockmigrator) unlock(c *container.Container, ownerID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "unlock", c, ownerID)
ret0, _ := ret[0].(error)
return ret0
}
// unlock indicates an expected call of unlock.
func (mr *MockmigratorMockRecorder) unlock(c, ownerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "unlock", reflect.TypeOf((*Mockmigrator)(nil).unlock), c, ownerID)
}
// MockOpenTSDB is a mock of OpenTSDB interface.
type MockOpenTSDB struct {
ctrl *gomock.Controller
recorder *MockOpenTSDBMockRecorder
isgomock struct{}
}
// MockOpenTSDBMockRecorder is the mock recorder for MockOpenTSDB.
type MockOpenTSDBMockRecorder struct {
mock *MockOpenTSDB
}
// NewMockOpenTSDB creates a new mock instance.
func NewMockOpenTSDB(ctrl *gomock.Controller) *MockOpenTSDB {
mock := &MockOpenTSDB{ctrl: ctrl}
mock.recorder = &MockOpenTSDBMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockOpenTSDB) EXPECT() *MockOpenTSDBMockRecorder {
return m.recorder
}
// DeleteAnnotation mocks base method.
func (m *MockOpenTSDB) DeleteAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAnnotation indicates an expected call of DeleteAnnotation.
func (mr *MockOpenTSDBMockRecorder) DeleteAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).DeleteAnnotation), ctx, annotation, res)
}
// PostAnnotation mocks base method.
func (m *MockOpenTSDB) PostAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PostAnnotation indicates an expected call of PostAnnotation.
func (mr *MockOpenTSDBMockRecorder) PostAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PostAnnotation), ctx, annotation, res)
}
// PutAnnotation mocks base method.
func (m *MockOpenTSDB) PutAnnotation(ctx context.Context, annotation, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutAnnotation", ctx, annotation, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutAnnotation indicates an expected call of PutAnnotation.
func (mr *MockOpenTSDBMockRecorder) PutAnnotation(ctx, annotation, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutAnnotation", reflect.TypeOf((*MockOpenTSDB)(nil).PutAnnotation), ctx, annotation, res)
}
// PutDataPoints mocks base method.
func (m *MockOpenTSDB) PutDataPoints(ctx context.Context, data any, queryParam string, res any) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutDataPoints", ctx, data, queryParam, res)
ret0, _ := ret[0].(error)
return ret0
}
// PutDataPoints indicates an expected call of PutDataPoints.
func (mr *MockOpenTSDBMockRecorder) PutDataPoints(ctx, data, queryParam, res any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutDataPoints", reflect.TypeOf((*MockOpenTSDB)(nil).PutDataPoints), ctx, data, queryParam, res)
}
// Mocklocker is a mock of locker interface.
type Mocklocker struct {
ctrl *gomock.Controller
recorder *MocklockerMockRecorder
isgomock struct{}
}
// MocklockerMockRecorder is the mock recorder for Mocklocker.
type MocklockerMockRecorder struct {
mock *Mocklocker
}
// NewMocklocker creates a new mock instance.
func NewMocklocker(ctrl *gomock.Controller) *Mocklocker {
mock := &Mocklocker{ctrl: ctrl}
mock.recorder = &MocklockerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mocklocker) EXPECT() *MocklockerMockRecorder {
return m.recorder
}
// lock mocks base method.
func (m *Mocklocker) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "lock", ctx, cancel, c, ownerID)
ret0, _ := ret[0].(error)
return ret0
}
// lock indicates an expected call of lock.
func (mr *MocklockerMockRecorder) lock(ctx, cancel, c, ownerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "lock", reflect.TypeOf((*Mocklocker)(nil).lock), ctx, cancel, c, ownerID)
}
// name mocks base method.
func (m *Mocklocker) name() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "name")
ret0, _ := ret[0].(string)
return ret0
}
// name indicates an expected call of name.
func (mr *MocklockerMockRecorder) name() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "name", reflect.TypeOf((*Mocklocker)(nil).name))
}
// unlock mocks base method.
func (m *Mocklocker) unlock(c *container.Container, ownerID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "unlock", c, ownerID)
ret0, _ := ret[0].(error)
return ret0
}
// unlock indicates an expected call of unlock.
func (mr *MocklockerMockRecorder) unlock(c, ownerID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "unlock", reflect.TypeOf((*Mocklocker)(nil).unlock), c, ownerID)
}
================================================
FILE: pkg/gofr/migration/mongo.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
type mongoDS struct {
container.Mongo
}
type mongoMigrator struct {
container.Mongo
migrator
}
// apply initializes mongoMigrator using the Mongo interface.
func (ds mongoDS) apply(m migrator) migrator {
return mongoMigrator{
Mongo: ds.Mongo,
migrator: m,
}
}
const (
mongoMigrationCollection = "gofr_migrations"
)
// checkAndCreateMigrationTable initializes a MongoDB collection if it doesn't exist.
func (mg mongoMigrator) checkAndCreateMigrationTable(c *container.Container) error {
err := mg.Mongo.CreateCollection(context.Background(), mongoMigrationCollection)
if err != nil {
c.Debug("Migration collection might already exist:", err)
return err
}
return mg.migrator.checkAndCreateMigrationTable(c)
}
func (mg mongoMigrator) getLastMigration(c *container.Container) (int64, error) {
var (
lastMigration int64
migrations []struct {
Version int64 `bson:"version"`
}
)
filter := make(map[string]any)
err := mg.Mongo.Find(context.Background(), mongoMigrationCollection, filter, &migrations)
if err != nil {
return -1, fmt.Errorf("mongo: %w", err)
}
// Identify the highest migration version.
for _, migration := range migrations {
lastMigration = max(lastMigration, migration.Version)
}
c.Debugf("MongoDB last migration fetched value is: %v", lastMigration)
lm2, err := mg.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (mg mongoMigrator) beginTransaction(c *container.Container) transactionData {
return mg.migrator.beginTransaction(c)
}
func (mg mongoMigrator) commitMigration(c *container.Container, data transactionData) error {
migrationDoc := map[string]any{
"version": data.MigrationNumber,
"method": "UP",
"start_time": data.StartTime,
"duration": time.Since(data.StartTime).Milliseconds(),
}
_, err := mg.Mongo.InsertOne(context.Background(), mongoMigrationCollection, migrationDoc)
if err != nil {
return err
}
c.Debugf("Inserted record for migration %v in MongoDB gofr_migrations collection", data.MigrationNumber)
return mg.migrator.commitMigration(c, data)
}
func (mg mongoMigrator) rollback(c *container.Container, data transactionData) {
mg.migrator.rollback(c, data)
c.Fatalf("Migration %v failed.", data.MigrationNumber)
}
func (mg mongoMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return mg.migrator.lock(ctx, cancel, c, ownerID)
}
func (mg mongoMigrator) unlock(c *container.Container, ownerID string) error {
return mg.migrator.unlock(c, ownerID)
}
func (mongoMigrator) name() string {
return "Mongo"
}
================================================
FILE: pkg/gofr/migration/mongo_test.go
================================================
package migration
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
)
var errMongoConn = errors.New("error connecting to mongo")
func mongoSetup(t *testing.T) (migrator, *container.MockMongo, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockMongo := mocks.Mongo
ds := Datasource{Mongo: mockContainer.Mongo}
mongoDB := mongoDS{Mongo: mockMongo}
migratorWithMongo := mongoDB.apply(&ds)
mockContainer.Mongo = mockMongo
return migratorWithMongo, mockMongo, mockContainer
}
func Test_MongoCheckAndCreateMigrationTable(t *testing.T) {
migratorWithMongo, mockMongo, mockContainer := mongoSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", errMongoConn},
}
for i, tc := range testCases {
mockMongo.EXPECT().CreateCollection(gomock.Any(), mongoMigrationCollection).Return(tc.err)
err := migratorWithMongo.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_MongoGetLastMigration(t *testing.T) {
migratorWithMongo, mockMongo, mockContainer := mongoSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 0},
{"connection failed", errMongoConn, -1},
}
var migrations []struct {
Version int64 `bson:"version"`
}
filter := make(map[string]any)
for i, tc := range testCases {
mockMongo.EXPECT().Find(gomock.Any(), mongoMigrationCollection, filter, &migrations).Return(tc.err)
resp, err := migratorWithMongo.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v]\n %v Failed! ", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
}
func Test_MongoCommitMigration(t *testing.T) {
migratorWithMongo, mockMongo, mockContainer := mongoSetup(t)
// mockResult is not the same result type as that returned by InsertOne method in mongoDB,
// but has been used only for mocking the test for migrations in mongoDB.
mockResult := struct{}{}
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"connection failed", errMongoConn},
}
timeNow := time.Now()
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
migrationDoc := map[string]any{
"version": td.MigrationNumber,
"method": "UP",
"start_time": td.StartTime,
"duration": time.Since(td.StartTime).Milliseconds(),
}
for i, tc := range testCases {
mockMongo.EXPECT().InsertOne(gomock.Any(), mongoMigrationCollection, migrationDoc).Return(mockResult, tc.err)
err := migratorWithMongo.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
================================================
FILE: pkg/gofr/migration/opentsdb.go
================================================
package migration
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"gofr.dev/pkg/gofr/container"
)
type openTSDBDS struct {
container.OpenTSDB
filePath string
}
type openTSDBMigrator struct {
filePath string
migrator
mu sync.Mutex
}
type tsdbMigrationRecord struct {
Version int64 `json:"version"`
Method string `json:"method"`
StartTime string `json:"start_time"`
Duration int64 `json:"duration"`
}
const dirPerm = 0755
var errNilFileHandle = errors.New("failed to create migration file: received nil file handle")
// apply initializes openTSDBMigrator using the openTsdbDS.
func (ds openTSDBDS) apply(m migrator) migrator {
return &openTSDBMigrator{ // Return pointer to avoid copying the mutex
filePath: ds.filePath,
migrator: m,
}
}
// checkAndCreateMigrationTable ensures the migration directory and file structure exists.
// It only creates an empty file if no migration file exists at all.
func (om *openTSDBMigrator) checkAndCreateMigrationTable(c *container.Container) error {
om.mu.Lock()
defer om.mu.Unlock()
// Ensure directory exists
dir := filepath.Dir(om.filePath)
if dir != "." {
if err := os.MkdirAll(dir, dirPerm); err != nil {
return fmt.Errorf("failed to create migration directory %q: %w", dir, err)
}
}
// Check if file exists and is readable
if _, err := os.Stat(om.filePath); err == nil {
// File exists, validate it's proper JSON
return om.validateExistingFile(c)
} else if !os.IsNotExist(err) {
// Some other error accessing the file
return fmt.Errorf("unexpected error stating migration file: %w", err)
}
// File doesn't exist, create empty migration file
return om.createEmptyMigrationFile(c)
}
// validateExistingFile checks if the existing migration file is valid JSON.
func (om *openTSDBMigrator) validateExistingFile(c *container.Container) error {
file, err := os.Open(om.filePath)
if err != nil {
return fmt.Errorf("failed to open existing migration file: %w", err)
}
defer file.Close()
var migrations []tsdbMigrationRecord
if err = json.NewDecoder(file).Decode(&migrations); err != nil {
c.Errorf("Existing migration file is corrupted: %v", err)
return fmt.Errorf("existing migration file contains invalid JSON: %w", err)
}
c.Debugf("Found existing migration file with %d migrations", len(migrations))
return nil
}
// createEmptyMigrationFile creates a new empty migration file.
func (om *openTSDBMigrator) createEmptyMigrationFile(c *container.Container) error {
f, err := os.Create(om.filePath)
if err != nil {
return fmt.Errorf("failed to create migration file: %w", err)
}
if f == nil {
return errNilFileHandle
}
defer func() {
if cerr := f.Close(); cerr != nil {
c.Debugf("Error closing migration file: %v", cerr)
}
}()
if _, err = f.WriteString("[]"); err != nil {
return fmt.Errorf("failed to initialize migration file: %w", err)
}
c.Debugf("Created new migration file: %s", om.filePath)
return nil
}
// getLastMigration reads JSON file to find the highest applied migration version.
func (om *openTSDBMigrator) getLastMigration(c *container.Container) (int64, error) {
om.mu.Lock()
defer om.mu.Unlock()
var lastMigration int64
migrations, err := om.loadMigrationsUnsafe()
if err != nil {
return -1, fmt.Errorf("opentsdb: %w", err)
}
for _, m := range migrations {
if m.Version > lastMigration {
lastMigration = m.Version
}
}
c.Debugf("JSON migration file last migration: %v", lastMigration)
baseMigration, err := om.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, baseMigration), nil
}
// beginTransaction delegates to base migrator.
func (om *openTSDBMigrator) beginTransaction(c *container.Container) transactionData {
return om.migrator.beginTransaction(c)
}
// commitMigration records a new migration in a JSON file in a thread-safe manner.
// It prevents duplicates and delegates the actual migration logic to the embedded migrator.
func (om *openTSDBMigrator) commitMigration(c *container.Container, data transactionData) error {
// First, delegate to base migrator to perform the actual migration
if err := om.migrator.commitMigration(c, data); err != nil {
return err
}
// Then record it in our JSON file
om.mu.Lock()
defer om.mu.Unlock()
// Load existing migrations from file
migrations, err := om.loadMigrationsUnsafe()
if err != nil {
return fmt.Errorf("failed to load existing migrations: %w", err)
}
// Skip if migration already exists
if migrationExists(migrations, data.MigrationNumber) {
c.Debugf("Migration %v already exists in JSON file, skipping JSON update", data.MigrationNumber)
return nil
}
// Add new migration entry
newRecord := tsdbMigrationRecord{
Version: data.MigrationNumber,
Method: "UP",
StartTime: data.StartTime.Format(time.RFC3339),
Duration: time.Since(data.StartTime).Milliseconds(),
}
migrations = append(migrations, newRecord)
// Atomically write updated migration list to file
if err := om.writeMigrationsAtomically(migrations); err != nil {
c.Errorf("Failed to write migration to JSON file: %v", err)
return fmt.Errorf("failed to record migration in JSON file: %w", err)
}
c.Debugf("Committed migration %v to JSON file", data.MigrationNumber)
return nil
}
// loadMigrationsUnsafe loads migrations without acquiring the mutex.
// Should only be called when the mutex is already held.
func (om *openTSDBMigrator) loadMigrationsUnsafe() ([]tsdbMigrationRecord, error) {
var migrations []tsdbMigrationRecord
file, err := os.Open(om.filePath)
if err != nil {
if os.IsNotExist(err) {
// File does not exist yet, return empty list
return migrations, nil
}
return nil, fmt.Errorf("failed to open migration file: %w", err)
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&migrations); err != nil {
return nil, fmt.Errorf("failed to decode existing migrations: %w", err)
}
return migrations, nil
}
// migrationExists checks if a given migration version already exists in the list.
func migrationExists(migrations []tsdbMigrationRecord, version int64) bool {
for _, existing := range migrations {
if existing.Version == version {
return true
}
}
return false
}
// writeMigrationsAtomically writes the migration list to disk using a temp file.
// ensuring that the operation is atomic and safe against partial writes.
func (om *openTSDBMigrator) writeMigrationsAtomically(migrations []tsdbMigrationRecord) error {
tmpFilePath := om.filePath + ".tmp"
tmpFile, err := os.Create(tmpFilePath)
if err != nil {
return fmt.Errorf("failed to create temporary file: %w", err)
}
defer func() {
tmpFile.Close()
if err != nil {
os.Remove(tmpFilePath) // Clean up temp file on failure
}
}()
// Write JSON with indentation for readability
enc := json.NewEncoder(tmpFile)
enc.SetIndent("", " ")
if err = enc.Encode(migrations); err != nil {
return fmt.Errorf("failed to encode migrations to JSON: %w", err)
}
// Ensure data is flushed to disk
if err = tmpFile.Sync(); err != nil {
return fmt.Errorf("failed to sync temporary file: %w", err)
}
// Atomically replace original file with temp file
if err = os.Rename(tmpFilePath, om.filePath); err != nil {
return fmt.Errorf("failed to rename temporary file: %w", err)
}
return nil
}
// rollback logs the failure and handles cleanup.
func (om *openTSDBMigrator) rollback(c *container.Container, data transactionData) {
// Clean up any temporary files
tmpFilePath := om.filePath + ".tmp"
if _, err := os.Stat(tmpFilePath); err == nil {
if removeErr := os.Remove(tmpFilePath); removeErr != nil {
c.Debugf("Failed to clean up temporary migration file: %v", removeErr)
} else {
c.Debugf("Cleaned up temporary migration file: %s", tmpFilePath)
}
}
// Delegate to base migrator
om.migrator.rollback(c, data)
c.Fatalf("Migration %v failed.", data.MigrationNumber)
}
func (om *openTSDBMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return om.migrator.lock(ctx, cancel, c, ownerID)
}
func (om *openTSDBMigrator) unlock(c *container.Container, ownerID string) error {
return om.migrator.unlock(c, ownerID)
}
func (*openTSDBMigrator) name() string {
return "OpenTSDB"
}
================================================
FILE: pkg/gofr/migration/opentsdb_test.go
================================================
package migration
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/container"
)
var (
errCheckAndCreateMigrationTablePanic = errors.New("panic occurred during checkAndCreateMigrationTable")
)
// openTSDBSetup creates a test setup for OpenTSDB migration tests.
func openTSDBSetup(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockOpenTSDB := mocks.OpenTSDB
if mockOpenTSDB == nil {
t.Fatal("mockOpenTSDB is nil - check container.NewMockContainer implementation")
}
tmpDir := t.TempDir()
filePath := filepath.Join(tmpDir, "test_migrations.json")
ds := Datasource{OpenTSDB: mockOpenTSDB}
openTSDBInstance := openTSDBDS{OpenTSDB: mockOpenTSDB, filePath: filePath}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
if migratorWithOpenTSDB == nil {
t.Fatal("migratorWithOpenTSDB is nil - check openTsdbDS.apply implementation")
}
mockContainer.OpenTSDB = mockOpenTSDB
return migratorWithOpenTSDB, mockContainer, filePath
}
func findMigrationFile(t *testing.T, baseDir string) string {
t.Helper()
var migrationFile string
err := filepath.Walk(baseDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !info.IsDir() && filepath.Ext(path) == ".json" {
migrationFile = path
return filepath.SkipDir
}
return nil
})
require.NoError(t, err)
require.NotEmpty(t, migrationFile, "Migration file should exist")
return migrationFile
}
// Test_OpenTSDBCheckAndCreateMigrationTable_Enhanced tests enhanced scenarios for creating migration table.
func Test_OpenTSDBCheckAndCreateMigrationTable_Enhanced(t *testing.T) {
testCases := getEnhancedTestCases()
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
runEnhancedTestCase(t, tc, i)
})
}
}
func getEnhancedTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
} {
var cases []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}
cases = append(cases, getSuccessTestCases()...)
cases = append(cases, getFilePermissionTestCases()...)
cases = append(cases, getDirectoryTestCases()...)
cases = append(cases, getEdgeCaseTestCases()...)
return cases
}
func getSuccessTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}{
{
desc: "creates new migration file successfully",
setupFunc: func(_ *testing.T, _ string) {
// No setup - file doesn't exist
},
expectedErr: "",
shouldFileExist: true,
verifyFunc: func(t *testing.T, filePath string) {
t.Helper()
content, err := os.ReadFile(filePath)
require.NoError(t, err)
assert.Equal(t, "[]", string(content), "File should contain empty JSON array")
},
},
{
desc: "file already exists with valid JSON",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
err := os.MkdirAll(dir, dirPerm)
require.NoError(t, err)
// Create valid migration file with existing data
migrations := []tsdbMigrationRecord{
{Version: 1, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 100},
}
data, err := json.MarshalIndent(migrations, "", " ")
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
},
expectedErr: "",
shouldFileExist: true,
verifyFunc: func(t *testing.T, filePath string) {
t.Helper()
content, err := os.ReadFile(filePath)
require.NoError(t, err)
var migrations []tsdbMigrationRecord
err = json.Unmarshal(content, &migrations)
require.NoError(t, err)
require.Len(t, migrations, 1)
assert.Equal(t, int64(1), migrations[0].Version)
},
},
}
}
func getFilePermissionTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}{
{
desc: "file exists but contains invalid JSON",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
err := os.MkdirAll(dir, dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("invalid json"), 0600)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldFileExist: true,
},
{
desc: "file exists but cannot be opened (permission denied)",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
err := os.MkdirAll(dir, dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("[]"), 0600)
require.NoError(t, err)
// Remove read permissions from the file
err = os.Chmod(filePath, 0000)
require.NoError(t, err)
},
expectedErr: "failed to open existing migration file",
shouldFileExist: true,
cleanupFunc: func(_ *testing.T, filePath string) {
// Restore permissions for cleanup
_ = os.Chmod(filePath, 0600)
},
},
{
desc: "file creation fails due to permission denied on directory",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
// Create directory with no write permissions
err := os.MkdirAll(dir, dirPerm)
require.NoError(t, err)
err = os.Chmod(dir, 0555) // Read and execute only
require.NoError(t, err)
},
expectedErr: "failed to create migration file",
shouldFileExist: false,
cleanupFunc: func(_ *testing.T, filePath string) {
// Restore permissions for cleanup
dir := filepath.Dir(filePath)
_ = os.Chmod(dir, 0755)
},
},
}
}
func getDirectoryTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}{
{
desc: "directory creation fails due to existing file with same name",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
parentDir := filepath.Dir(dir)
err := os.MkdirAll(parentDir, dirPerm)
require.NoError(t, err)
// Create a regular file where directory should be
err = os.WriteFile(dir, []byte("blocking file"), 0600)
require.NoError(t, err)
},
expectedErr: "failed to create migration directory",
shouldFileExist: false,
},
{
desc: "directory creation fails due to permission denied on parent",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
parentDir := filepath.Dir(dir)
// Create parent directory with no write permissions
err := os.MkdirAll(parentDir, dirPerm)
require.NoError(t, err)
err = os.Chmod(parentDir, 0555) // Read and execute only
require.NoError(t, err)
},
expectedErr: "failed to create migration directory",
shouldFileExist: false,
cleanupFunc: func(_ *testing.T, filePath string) {
// Restore permissions for cleanup
dir := filepath.Dir(filePath)
parentDir := filepath.Dir(dir)
_ = os.Chmod(parentDir, 0755)
},
},
}
}
func getEdgeCaseTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}{
{
desc: "migration file path is a directory",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
dir := filepath.Dir(filePath)
err := os.MkdirAll(dir, dirPerm)
require.NoError(t, err)
// Create a directory with the same name as the file
err = os.MkdirAll(filePath, dirPerm)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldFileExist: false,
},
{
desc: "empty file path directory (current directory)",
setupFunc: func(t *testing.T, _ string) {
t.Helper()
// This tests the case where filepath.Dir returns "."
// File will be created in current directory
},
expectedErr: "",
shouldFileExist: true,
verifyFunc: func(t *testing.T, filePath string) {
t.Helper()
content, err := os.ReadFile(filePath)
require.NoError(t, err)
assert.Equal(t, "[]", string(content))
},
},
}
}
func runEnhancedTestCase(t *testing.T, tc struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldFileExist bool
verifyFunc func(t *testing.T, filePath string)
cleanupFunc func(t *testing.T, filePath string)
}, i int) {
t.Helper()
// Setup test environment
var migratorWithOpenTSDB migrator
var mockContainer *container.Container
var filePath string
if tc.desc == "empty file path directory (current directory)" {
// Special setup for current directory test
tmpDir := t.TempDir()
originalDir, err := os.Getwd()
require.NoError(t, err)
t.Chdir(tmpDir)
t.Cleanup(func() {
t.Chdir(originalDir)
})
// Create migrator with just filename (no directory path)
mockContainer2, mocks := container.NewMockContainer(t)
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: "test_migrations.json"}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB = openTSDBInstance.apply(&ds)
mockContainer = mockContainer2
filePath = "test_migrations.json"
} else {
migratorWithOpenTSDB, mockContainer, filePath = openTSDBSetup(t)
}
// Clean up any existing files/directories
if tc.desc != "empty file path directory (current directory)" {
os.RemoveAll(filepath.Dir(filePath))
}
// Setup test scenario
tc.setupFunc(t, filePath)
// Execute the function under test
err := func() (err error) {
defer func() {
if r := recover(); r != nil {
err = errCheckAndCreateMigrationTablePanic
}
}()
return migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
}()
// Verify results
if tc.expectedErr != "" {
require.Error(t, err, "TEST[%v] %v Failed! Expected error but got none", i, tc.desc)
assert.Contains(t, err.Error(), tc.expectedErr, "TEST[%v] %v Failed! Error message mismatch", i, tc.desc)
} else {
require.NoError(t, err, "TEST[%v] %v Failed! Unexpected error: %v", i, tc.desc, err)
}
// Verify file existence
if tc.shouldFileExist {
_, err := os.Stat(filePath)
require.NoError(t, err, "Migration file should exist at: %s", filePath)
}
// Run custom verification if provided
if tc.verifyFunc != nil {
tc.verifyFunc(t, filePath)
}
// Run cleanup if provided
if tc.cleanupFunc != nil {
tc.cleanupFunc(t, filePath)
}
}
// Test_OpenTSDBCheckAndCreateMigrationTable_ConcurrentAccess tests concurrent access to checkAndCreateMigrationTable.
func Test_OpenTSDBCheckAndCreateMigrationTable_ConcurrentAccess(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up any existing files
os.RemoveAll(filepath.Dir(filePath))
const numGoroutines = 10
var wg sync.WaitGroup
errCh := make(chan error, numGoroutines)
// Run multiple goroutines concurrently
for range numGoroutines {
wg.Add(1)
go func() {
defer wg.Done()
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
errCh <- err
}()
}
wg.Wait()
close(errCh)
// Verify all goroutines succeeded
successCount := 0
for err := range errCh {
if err == nil {
successCount++
} else {
t.Logf("Goroutine error: %v", err)
}
}
// At least one should succeed (the first one to create the file)
require.Positive(t, successCount, "At least one goroutine should succeed")
// Verify file was created
actualFile := findMigrationFile(t, filepath.Dir(filePath))
content, err := os.ReadFile(actualFile)
require.NoError(t, err)
assert.Equal(t, "[]", string(content), "File should contain empty JSON array")
}
// Test_OpenTSDBCheckAndCreateMigrationTable_MutexProtection verifies mutex protection.
func Test_OpenTSDBCheckAndCreateMigrationTable_MutexProtection(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up any existing files
os.RemoveAll(filepath.Dir(filePath))
// Cast to access the mutex directly for verification
openTSDBMig, ok := migratorWithOpenTSDB.(*openTSDBMigrator)
require.True(t, ok, "Failed to cast to openTSDBMigrator")
// This test verifies that the mutex is properly protecting the critical section
// We'll run the function multiple times and verify consistent behavior
for i := range 5 {
err := openTSDBMig.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err, "Iteration %d should succeed", i)
// Verify file exists and has correct content
actualFile := findMigrationFile(t, filepath.Dir(filePath))
content, err := os.ReadFile(actualFile)
require.NoError(t, err)
assert.Equal(t, "[]", string(content), "File should always contain empty JSON array")
}
}
// Test_OpenTSDBCheckAndCreateMigrationTable_EdgeCases tests additional edge cases.
func Test_OpenTSDBCheckAndCreateMigrationTable_EdgeCases(t *testing.T) {
testCases := []struct {
desc string
setupFunc func(t *testing.T) (migrator, *container.Container, string)
expectedErr string
}{
{
desc: "very long file path",
setupFunc: setupVeryLongPath,
expectedErr: "",
},
{
desc: "file path with special characters",
setupFunc: setupSpecialCharPath,
expectedErr: "",
},
{
desc: "file path with unicode characters",
setupFunc: setupUnicodePath,
expectedErr: "",
},
{
desc: "nested deep directory structure",
setupFunc: setupDeepPath,
expectedErr: "",
},
{
desc: "file path with dots and relative components",
setupFunc: setupDotPath,
expectedErr: "",
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := tc.setupFunc(t)
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
if tc.expectedErr != "" {
require.Error(t, err, "TEST[%v] %v Failed! Expected error but got none", i, tc.desc)
assert.Contains(t, err.Error(), tc.expectedErr, "TEST[%v] %v Failed!", i, tc.desc)
} else {
require.NoError(t, err, "TEST[%v] %v Failed! Unexpected error: %v", i, tc.desc, err)
// Verify file was created successfully
_, err := os.Stat(filePath)
require.NoError(t, err, "Migration file should exist at: %s", filePath)
content, err := os.ReadFile(filePath)
require.NoError(t, err)
assert.Equal(t, "[]", string(content), "File should contain empty JSON array")
}
})
}
}
func setupVeryLongPath(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
// Create a very long path with valid characters
tmpDir := t.TempDir()
longDirName := strings.Repeat("a", 100) // 100 'a' characters instead of null bytes
longPath := filepath.Join(tmpDir, longDirName, "migrations.json")
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: longPath}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
return migratorWithOpenTSDB, mockContainer, longPath
}
func setupSpecialCharPath(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
tmpDir := t.TempDir()
specialPath := filepath.Join(tmpDir, "test with spaces & symbols!@#", "migrations.json")
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: specialPath}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
return migratorWithOpenTSDB, mockContainer, specialPath
}
func setupUnicodePath(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
tmpDir := t.TempDir()
// Test with various unicode characters
unicodePath := filepath.Join(tmpDir, "测试目录-🚀-مجلد", "migrations.json")
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: unicodePath}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
return migratorWithOpenTSDB, mockContainer, unicodePath
}
func setupDeepPath(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
tmpDir := t.TempDir()
// Create a deeply nested path
deepPath := tmpDir
for i := 0; i < 10; i++ {
deepPath = filepath.Join(deepPath, fmt.Sprintf("level%d", i))
}
deepPath = filepath.Join(deepPath, "migrations.json")
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: deepPath}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
return migratorWithOpenTSDB, mockContainer, deepPath
}
func setupDotPath(t *testing.T) (migrator, *container.Container, string) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
tmpDir := t.TempDir()
// Path with dots (should be cleaned by filepath.Join)
dotPath := filepath.Join(tmpDir, "dir1", "..", "dir2", ".", "migrations.json")
openTSDBInstance := openTSDBDS{OpenTSDB: mocks.OpenTSDB, filePath: dotPath}
ds := Datasource{OpenTSDB: mocks.OpenTSDB}
migratorWithOpenTSDB := openTSDBInstance.apply(&ds)
return migratorWithOpenTSDB, mockContainer, dotPath
}
func Test_OpenTSDBGetLastMigration(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
testCases := []struct {
desc string
setupFunc func()
expectedResult int64
}{
{
desc: "empty migration file",
setupFunc: func() {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("[]"), 0600)
require.NoError(t, err)
},
expectedResult: 0,
},
{
desc: "file with migrations",
setupFunc: func() {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
migrations := []tsdbMigrationRecord{
{Version: 1, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 0},
{Version: 3, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 100},
{Version: 2, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 50},
}
data, err := json.Marshal(migrations)
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
},
expectedResult: 3,
},
{
desc: "file doesn't exist",
setupFunc: func() {
t.Helper()
// No file
},
expectedResult: 0,
},
{
desc: "invalid JSON file",
setupFunc: func() {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("invalid json"), 0600)
require.NoError(t, err)
},
expectedResult: -1,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
os.RemoveAll(filepath.Dir(filePath))
tc.setupFunc()
result, err := migratorWithOpenTSDB.getLastMigration(mockContainer)
assert.Equal(t, tc.expectedResult, result, "TEST[%v] %v Failed!", i, tc.desc)
if tc.expectedResult == -1 {
assert.Error(t, err, "TEST[%v] %v Failed! Expected error", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v] %v Failed! Unexpected error", i, tc.desc)
}
})
}
}
// Test_OpenTSDBCommitMigration_ConcurrentAccess tests concurrent commits.
func Test_OpenTSDBCommitMigration_ConcurrentAccess(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up and setup empty migration file
os.RemoveAll(filepath.Dir(filePath))
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err)
const numGoroutines = 20
var wg sync.WaitGroup
errCh := make(chan error, numGoroutines)
// Launch multiple goroutines trying to commit different migrations
for i := 1; i <= numGoroutines; i++ {
wg.Add(1)
go func(migrationNum int) {
defer wg.Done()
txData := transactionData{
StartTime: time.Now().Add(-time.Duration(migrationNum) * time.Millisecond),
MigrationNumber: int64(migrationNum),
}
err := migratorWithOpenTSDB.commitMigration(mockContainer, txData)
errCh <- err
}(i)
}
wg.Wait()
close(errCh)
// Verify all commits succeeded
var errs []error
for err := range errCh {
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
t.Logf("Concurrent commit errors: %v", errs)
}
// Verify all migrations were recorded
verifyMigrationsCount(t, filePath, numGoroutines)
// Verify each migration number is present
for i := 1; i <= numGoroutines; i++ {
verifyMigrationFileContains(t, filePath, int64(i))
}
}
// Test_OpenTSDBCommitMigration_ConcurrentDuplicates tests concurrent commits of same migration.
func Test_OpenTSDBCommitMigration_ConcurrentDuplicates(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up and setup empty migration file
os.RemoveAll(filepath.Dir(filePath))
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err)
const numGoroutines = 10
const migrationNumber = 42
var wg sync.WaitGroup
errCh := make(chan error, numGoroutines)
// Launch multiple goroutines trying to commit the same migration
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
txData := transactionData{
StartTime: time.Now(),
MigrationNumber: migrationNumber,
}
err := migratorWithOpenTSDB.commitMigration(mockContainer, txData)
errCh <- err
}()
}
wg.Wait()
close(errCh)
// All should succeed (duplicates are skipped, not errors)
for err := range errCh {
require.NoError(t, err, "Duplicate migration commits should not error")
}
// Should only have one migration recorded
verifyMigrationsCount(t, filePath, 1)
verifyMigrationFileContains(t, filePath, migrationNumber)
}
// verifyMigrationsCount verifies the total number of migrations in the file.
func verifyMigrationsCount(t *testing.T, basePath string, expectedCount int) {
t.Helper()
file := findMigrationFile(t, filepath.Dir(basePath))
data, err := os.ReadFile(file)
require.NoError(t, err)
var migrations []tsdbMigrationRecord
require.NoError(t, json.Unmarshal(data, &migrations))
assert.Len(t, migrations, expectedCount,
"Expected %d migrations but found %d", expectedCount, len(migrations))
}
// Test_OpenTSDBCommitMigration_JSONFormatValidation tests that output JSON is properly formatted.
func Test_OpenTSDBCommitMigration_JSONFormatValidation(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up and setup
os.RemoveAll(filepath.Dir(filePath))
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err)
// Commit a migration
txData := transactionData{
StartTime: time.Now().Add(-100 * time.Millisecond),
MigrationNumber: 1,
}
err = migratorWithOpenTSDB.commitMigration(mockContainer, txData)
require.NoError(t, err)
// Read the file and verify JSON formatting
file := findMigrationFile(t, filepath.Dir(filePath))
content, err := os.ReadFile(file)
require.NoError(t, err)
// Should be properly indented JSON
var rawData []tsdbMigrationRecord
err = json.Unmarshal(content, &rawData)
require.NoError(t, err)
// Re-marshal with same formatting and compare
expectedContent, err := json.MarshalIndent(rawData, "", " ")
require.NoError(t, err)
// The content should match properly formatted JSON (with trailing newline from encoder)
assert.JSONEq(t, string(expectedContent), strings.TrimSpace(string(content)),
"JSON should be properly formatted with indentation")
}
// Test_OpenTSDBCommitMigration_TimestampAccuracy tests timestamp handling accuracy.
func Test_OpenTSDBCommitMigration_TimestampAccuracy(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up and setup
os.RemoveAll(filepath.Dir(filePath))
err := migratorWithOpenTSDB.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err)
// Use a specific time for accuracy testing
specificTime := time.Date(2025, 7, 14, 13, 6, 27, 123456789, time.UTC)
txData := transactionData{
StartTime: specificTime,
MigrationNumber: 1,
}
// Record time just before commit for duration calculation
beforeCommit := time.Now()
err = migratorWithOpenTSDB.commitMigration(mockContainer, txData)
afterCommit := time.Now()
require.NoError(t, err)
// Verify the timestamp and duration
file := findMigrationFile(t, filepath.Dir(filePath))
data, err := os.ReadFile(file)
require.NoError(t, err)
var migrations []tsdbMigrationRecord
require.NoError(t, json.Unmarshal(data, &migrations))
require.Len(t, migrations, 1)
migration := migrations[0]
// Verify timestamp format and accuracy
assert.Equal(t, specificTime.Format(time.RFC3339), migration.StartTime,
"Start time should be formatted as RFC3339")
// Verify duration is reasonable (between our before/after measurements)
expectedMinDuration := beforeCommit.Sub(specificTime).Milliseconds()
expectedMaxDuration := afterCommit.Sub(specificTime).Milliseconds()
assert.GreaterOrEqual(t, migration.Duration, expectedMinDuration,
"Duration should be at least the minimum expected")
assert.LessOrEqual(t, migration.Duration, expectedMaxDuration,
"Duration should not exceed the maximum expected")
}
// verifyMigrationFileContains checks if the migration file contains a specific version.
func verifyMigrationFileContains(t *testing.T, basePath string, expectedVersion int64) {
t.Helper()
file := findMigrationFile(t, filepath.Dir(basePath))
data, err := os.ReadFile(file)
require.NoError(t, err)
var migrations []tsdbMigrationRecord
require.NoError(t, json.Unmarshal(data, &migrations))
found := false
for _, migration := range migrations {
if migration.Version == expectedVersion {
found = true
break
}
}
assert.True(t, found, "Expected migration version %d not found in file", expectedVersion)
}
// Test_OpenTSDBValidateExistingFile tests various scenarios for validating existing migration files.
func Test_OpenTSDBValidateExistingFile(t *testing.T) {
testCases := getValidateExistingFileTestCases()
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
runValidateExistingFileTestCase(t, tc, i)
})
}
}
func getValidateExistingFileTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
} {
var cases []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
}
cases = append(cases, getValidJSONTestCases()...)
cases = append(cases, getInvalidJSONTestCases()...)
cases = append(cases, getFileAccessTestCases()...)
return cases
}
func getValidJSONTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
}{
{
desc: "valid empty JSON array",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("[]"), 0600)
require.NoError(t, err)
},
expectedErr: "",
shouldLogError: false,
shouldLogDebug: true,
debugMessage: "Found existing migration file with 0 migrations",
},
{
desc: "valid JSON with single migration",
setupFunc: setupSingleMigration,
expectedErr: "",
shouldLogError: false,
shouldLogDebug: true,
debugMessage: "Found existing migration file with 1 migrations",
},
{
desc: "valid JSON with multiple migrations",
setupFunc: setupMultipleMigrations,
expectedErr: "",
shouldLogError: false,
shouldLogDebug: true,
debugMessage: "Found existing migration file with 3 migrations",
},
{
desc: "valid JSON with mixed field types",
setupFunc: setupMixedFieldTypes,
expectedErr: "",
shouldLogError: false,
shouldLogDebug: true,
debugMessage: "Found existing migration file with 2 migrations",
},
}
}
func getInvalidJSONTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
}{
{
desc: "invalid JSON - malformed",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("invalid json content"), 0600)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldLogError: true,
shouldLogDebug: false,
},
{
desc: "invalid JSON - incomplete array",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("[{\"version\": 1"), 0600)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldLogError: true,
shouldLogDebug: false,
},
{
desc: "invalid JSON - wrong structure",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("{\"not\": \"an array\"}"), 0600)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldLogError: true,
shouldLogDebug: false,
},
{
desc: "empty file",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte(""), 0600)
require.NoError(t, err)
},
expectedErr: "existing migration file contains invalid JSON",
shouldLogError: true,
shouldLogDebug: false,
},
}
}
func getFileAccessTestCases() []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
} {
return []struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
}{
{
desc: "file exists but cannot be opened (permission denied)",
setupFunc: func(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
err = os.WriteFile(filePath, []byte("[]"), 0000) // No read permissions
require.NoError(t, err)
},
expectedErr: "failed to open existing migration file",
shouldLogError: false,
shouldLogDebug: false,
},
}
}
func setupSingleMigration(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
migrations := []tsdbMigrationRecord{
{
Version: 1,
Method: "UP",
StartTime: "2025-07-14T13:06:27Z",
Duration: 100,
},
}
data, err := json.MarshalIndent(migrations, "", " ")
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
}
func setupMultipleMigrations(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
migrations := []tsdbMigrationRecord{
{Version: 1, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 50},
{Version: 2, Method: "UP", StartTime: "2025-07-14T13:06:28Z", Duration: 75},
{Version: 3, Method: "UP", StartTime: "2025-07-14T13:06:29Z", Duration: 100},
}
data, err := json.MarshalIndent(migrations, "", " ")
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
}
func setupMixedFieldTypes(t *testing.T, filePath string) {
t.Helper()
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
// JSON with some fields missing or different types (but still valid for our struct)
jsonContent := `[
{
"version": 1,
"method": "UP",
"start_time": "2025-07-14T13:06:27Z",
"duration": 100
},
{
"version": 2,
"method": "DOWN",
"start_time": "2025-07-14T13:06:28Z"
}
]`
err = os.WriteFile(filePath, []byte(jsonContent), 0600)
require.NoError(t, err)
}
func runValidateExistingFileTestCase(t *testing.T, tc struct {
desc string
setupFunc func(t *testing.T, filePath string)
expectedErr string
shouldLogError bool
shouldLogDebug bool
debugMessage string
}, i int) {
t.Helper()
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Clean up any existing files
os.RemoveAll(filepath.Dir(filePath))
// Setup the test scenario
tc.setupFunc(t, filePath)
// Cast to access the validateExistingFile method
openTSDBMig, ok := migratorWithOpenTSDB.(*openTSDBMigrator)
require.True(t, ok, "Failed to cast to openTSDBMigrator")
// Call the method under test
err := openTSDBMig.validateExistingFile(mockContainer)
// Verify error expectations
if tc.expectedErr != "" {
require.Error(t, err, "TEST[%v] %v Failed! Expected error but got none", i, tc.desc)
assert.Contains(t, err.Error(), tc.expectedErr, "TEST[%v] %v Failed! Error message mismatch", i, tc.desc)
} else {
require.NoError(t, err, "TEST[%v] %v Failed! Unexpected error: %v", i, tc.desc, err)
}
}
// Test_OpenTSDBValidateExistingFile_ConcurrentAccess tests the validateExistingFile method under concurrent access.
func Test_OpenTSDBValidateExistingFile_ConcurrentAccess(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Setup a valid migration file
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
migrations := []tsdbMigrationRecord{
{Version: 1, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 100},
}
data, err := json.MarshalIndent(migrations, "", " ")
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
// Cast to access the validateExistingFile method
openTSDBMig, ok := migratorWithOpenTSDB.(*openTSDBMigrator)
require.True(t, ok, "Failed to cast to openTSDBMigrator")
// Run multiple goroutines concurrently
const numGoroutines = 10
var wg sync.WaitGroup
errCh := make(chan error, numGoroutines)
for range numGoroutines {
wg.Add(1)
go func() {
defer wg.Done()
err := openTSDBMig.validateExistingFile(mockContainer)
errCh <- err
}()
}
wg.Wait()
close(errCh)
// Verify all goroutines succeeded
for err := range errCh {
require.NoError(t, err, "Concurrent access should not cause errors")
}
}
// Test_OpenTSDBValidateExistingFile_FileModifiedDuringRead tests behavior when file is modified during read.
func Test_OpenTSDBValidateExistingFile_FileModifiedDuringRead(t *testing.T) {
migratorWithOpenTSDB, mockContainer, filePath := openTSDBSetup(t)
// Setup initial valid migration file
err := os.MkdirAll(filepath.Dir(filePath), dirPerm)
require.NoError(t, err)
migrations := []tsdbMigrationRecord{
{Version: 1, Method: "UP", StartTime: "2025-07-14T13:06:27Z", Duration: 100},
}
data, err := json.MarshalIndent(migrations, "", " ")
require.NoError(t, err)
err = os.WriteFile(filePath, data, 0600)
require.NoError(t, err)
// Cast to access the validateExistingFile method
openTSDBMig, ok := migratorWithOpenTSDB.(*openTSDBMigrator)
require.True(t, ok, "Failed to cast to openTSDBMigrator")
// This test verifies that the function handles the case where
// the file exists and is readable at the time of the call
err = openTSDBMig.validateExistingFile(mockContainer)
require.NoError(t, err, "Should successfully validate existing file")
// Test with file that gets corrupted
err = os.WriteFile(filePath, []byte("corrupted"), 0600)
require.NoError(t, err)
err = openTSDBMig.validateExistingFile(mockContainer)
require.Error(t, err, "Should fail to validate corrupted file")
assert.Contains(t, err.Error(), "existing migration file contains invalid JSON")
}
================================================
FILE: pkg/gofr/migration/oracle.go
================================================
package migration
import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
"gofr.dev/pkg/gofr/container"
)
var (
errInvalidOracleTransaction = errors.New("invalid Oracle transaction")
errNestedTransactionNotSupported = errors.New("nested transactions not supported")
)
type oracleDS struct {
Oracle
}
type oracleMigrator struct {
Oracle
migrator
}
// Provides a wrapper to apply the oracle migrator logic.
func (od oracleDS) apply(m migrator) migrator {
return oracleMigrator{
Oracle: od.Oracle,
migrator: m,
}
}
const (
checkAndCreateOracleMigrationTable = `
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE gofr_migrations (
version NUMBER NOT NULL,
method VARCHAR2(64) NOT NULL,
start_time TIMESTAMP NOT NULL,
duration NUMBER NULL,
PRIMARY KEY (version, method)
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
`
getLastOracleGoFrMigration = `
SELECT NVL(MAX(version), 0) AS last_migration
FROM gofr_migrations
`
insertOracleGoFrMigrationRow = `
INSERT INTO gofr_migrations (version, method, start_time, duration)
VALUES (:1, :2, :3, :4)
`
checkAndCreateOracleMigrationLocksTable = `
BEGIN
EXECUTE IMMEDIATE 'CREATE TABLE gofr_migration_locks (
lock_key VARCHAR2(64) PRIMARY KEY,
owner_id VARCHAR2(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
`
deleteExpiredOracleLocks = `DELETE FROM gofr_migration_locks WHERE expires_at < :1`
insertOracleLock = `INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (:1, :2, :3)`
updateOracleLock = `
BEGIN
UPDATE gofr_migration_locks SET expires_at = :1 WHERE lock_key = :2 AND owner_id = :3;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'lock refresh failed: no rows updated');
END IF;
END;
`
deleteOracleLock = `
BEGIN
DELETE FROM gofr_migration_locks WHERE lock_key = :1 AND owner_id = :2;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20002, 'lock release failed: lock was already released or stolen');
END IF;
END;
`
)
// Create migration table if it doesn't exist.
func (om oracleMigrator) checkAndCreateMigrationTable(c *container.Container) error {
if err := om.Oracle.Exec(context.Background(), checkAndCreateOracleMigrationTable); err != nil {
c.Errorf("failed to create Oracle migration table: %v", err)
return err
}
if err := om.Oracle.Exec(context.Background(), checkAndCreateOracleMigrationLocksTable); err != nil {
c.Errorf("failed to create Oracle migration locks table: %v", err)
return err
}
return om.migrator.checkAndCreateMigrationTable(c)
}
// Get the last applied migration version.
func (om oracleMigrator) getLastMigration(c *container.Container) (int64, error) {
var (
results []map[string]any
oracleLastMigration int64
)
err := om.Oracle.Select(context.Background(), &results, getLastOracleGoFrMigration)
if err != nil {
return -1, fmt.Errorf("oracle: %w", err)
}
if len(results) != 0 {
oracleLastMigration = om.extractLastMigrationFromResults(results)
}
c.Debugf("Oracle last migration fetched value is: %v", oracleLastMigration)
baseLastMigration, err := om.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(baseLastMigration, oracleLastMigration), nil
}
// extractLastMigrationFromResults handles Oracle number type conversion.
func (om oracleMigrator) extractLastMigrationFromResults(results []map[string]any) int64 {
if len(results) == 0 {
return 0
}
lastMigVal, exists := results[0]["LAST_MIGRATION"]
if !exists {
return 0
}
return om.convertToInt64(lastMigVal)
}
// convertToInt64 converts various Oracle number types to int64.
func (om oracleMigrator) convertToInt64(value any) int64 {
switch v := value.(type) {
case float64:
return int64(v)
case int64:
return v
case int:
return int64(v)
default:
return om.parseStringValue(v)
}
}
// parseStringValue handles godror.Number type by converting to string then parsing.
func (oracleMigrator) parseStringValue(value any) int64 {
str := fmt.Sprintf("%v", value)
if str == "" || str == "" {
return 0
}
parsed, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0
}
return parsed
}
// Commit the migration and insert a record into the migration table.
func (om oracleMigrator) commitMigration(c *container.Container, data transactionData) error {
if data.OracleTx == nil {
c.Error("invalid Oracle transaction")
return errInvalidOracleTransaction
}
// Insert migration record using the transaction.
err := data.OracleTx.ExecContext(context.Background(), insertOracleGoFrMigrationRow,
data.MigrationNumber, "UP", data.StartTime, time.Since(data.StartTime).Milliseconds())
if err != nil {
c.Errorf("failed to insert migration record: %v", err)
return err
}
c.Debugf("inserted record for migration %v in Oracle gofr_migrations table", data.MigrationNumber)
// Commit the transaction.
if err := data.OracleTx.Commit(); err != nil {
c.Errorf("failed to commit Oracle transaction: %v", err)
return err
}
return om.migrator.commitMigration(c, data)
}
// Rollback the migration transaction.
func (om oracleMigrator) rollback(c *container.Container, data transactionData) {
if data.OracleTx != nil {
if err := data.OracleTx.Rollback(); err != nil {
c.Fatalf("unable to rollback Oracle transaction: %v", err)
} else {
c.Fatalf("Oracle migration failed, transaction rolled back - exiting application")
}
}
// Call the base migrator's rollback.
om.migrator.rollback(c, data)
}
// Begin a new migration transaction.
func (om oracleMigrator) beginTransaction(c *container.Container) transactionData {
// Begin a proper transaction
tx, err := om.Oracle.Begin()
if err != nil {
c.Errorf("unable to begin Oracle transaction: %v", err)
return transactionData{}
}
td := om.migrator.beginTransaction(c)
td.OracleTx = tx // Store the transaction in transactionData
c.Debug("Oracle Transaction begin successful")
return td
}
func (om oracleMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
for i := 0; ; i++ {
if err := om.Oracle.Exec(ctx, deleteExpiredOracleLocks, time.Now().UTC()); err != nil {
c.Debugf("failed to clean up expired Oracle locks: %v", err)
}
expiresAt := time.Now().UTC().Add(defaultLockTTL)
err := om.Oracle.Exec(ctx, insertOracleLock, lockKey, ownerID, expiresAt)
if err == nil {
c.Debug("Oracle lock acquired successfully")
go om.startRefresh(ctx, cancel, c, ownerID)
return om.migrator.lock(ctx, cancel, c, ownerID)
}
if !isOracleDuplicateKeyError(err) {
c.Errorf("error while acquiring Oracle lock: %v", err)
return errLockAcquisitionFailed
}
c.Debugf("Oracle lock already held, retrying in %v... (attempt %d)", defaultRetry, i+1)
select {
case <-time.After(defaultRetry):
case <-ctx.Done():
return ctx.Err()
}
}
}
func (om oracleMigrator) startRefresh(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) {
ticker := time.NewTicker(defaultRefresh)
defer ticker.Stop()
for {
select {
case <-ticker.C:
expiresAt := time.Now().UTC().Add(defaultLockTTL)
if err := om.Oracle.Exec(ctx, updateOracleLock, expiresAt, lockKey, ownerID); err != nil {
c.Errorf("failed to refresh Oracle lock (lock may have been stolen): %v", err)
cancel()
return
}
c.Debugf("Oracle lock refreshed successfully")
case <-ctx.Done():
return
}
}
}
// isOracleDuplicateKeyError checks for Oracle unique constraint violation (ORA-00001) in addition
// to the generic patterns checked by isDuplicateKeyError, to avoid relying solely on driver-specific
// message formatting.
func isOracleDuplicateKeyError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "ora-00001") || isDuplicateKeyError(err)
}
func (om oracleMigrator) unlock(c *container.Container, ownerID string) error {
if err := om.Oracle.Exec(context.Background(), deleteOracleLock, lockKey, ownerID); err != nil {
c.Errorf("unable to release Oracle lock: %v", err)
return errLockReleaseFailed
}
c.Debug("Oracle lock released successfully")
return om.migrator.unlock(c, ownerID)
}
func (oracleMigrator) name() string {
return "Oracle"
}
type oracleTransactionWrapper struct {
tx container.OracleTx
}
func (otw *oracleTransactionWrapper) Exec(ctx context.Context, query string, args ...any) error {
return otw.tx.ExecContext(ctx, query, args...)
}
func (otw *oracleTransactionWrapper) Select(ctx context.Context, dest any, query string, args ...any) error {
return otw.tx.SelectContext(ctx, dest, query, args...)
}
func (*oracleTransactionWrapper) Begin() (container.OracleTx, error) {
return nil, errNestedTransactionNotSupported
}
================================================
FILE: pkg/gofr/migration/oracle_test.go
================================================
package migration
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
)
var (
errOracleDuplicateKey = errors.New("unique constraint violated")
errOracleSystemError = errors.New("ORA-01017: invalid username/password")
errOracleLockLost = errors.New("ORA-20001: lock refresh failed: no rows updated")
errOracleLockStolen = errors.New("ORA-20002: lock release failed: lock was already released or stolen")
)
func oracleSetup(t *testing.T) (migrator, *container.MockOracleDB, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockOracle := mocks.Oracle
ds := Datasource{Oracle: mockOracle}
oracleDB := oracleDS{Oracle: mockOracle}
migrationWithOracle := oracleDB.apply(&ds)
mockContainer.Oracle = mockOracle
return migrationWithOracle, mockOracle, mockContainer
}
func Test_OracleCheckAndCreateMigrationTable(t *testing.T) {
testCases := []struct {
desc string
migTableErr error
lockTableErr error
expectedErr error
}{
{"no error", nil, nil, nil},
{"migration table creation failed", sql.ErrConnDone, nil, sql.ErrConnDone},
{"lock table creation failed", nil, sql.ErrConnDone, sql.ErrConnDone},
}
for i, tc := range testCases {
mg, mockOracle, mockContainer := oracleSetup(t)
mockOracle.EXPECT().Exec(gomock.Any(), checkAndCreateOracleMigrationTable).Return(tc.migTableErr)
if tc.migTableErr == nil {
mockOracle.EXPECT().Exec(gomock.Any(), checkAndCreateOracleMigrationLocksTable).Return(tc.lockTableErr)
}
err := mg.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.expectedErr, err, "TEST[%d]: %s failed", i, tc.desc)
}
}
func Test_OracleGetLastMigration(t *testing.T) {
mg, mockOracle, mockContainer := oracleSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 0},
{"connection failed", sql.ErrConnDone, -1},
}
for i, tc := range testCases {
mockOracle.EXPECT().Select(gomock.Any(), gomock.Any(), getLastOracleGoFrMigration).Return(tc.err)
resp, err := mg.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%d]: %s failed", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%d]: %s failed", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%d]: %s failed", i, tc.desc)
}
}
}
func Test_OracleCommitMigration(t *testing.T) {
mg, _, mockContainer := oracleSetup(t)
ctrl := gomock.NewController(t)
timeNow := time.Now()
// Success case
mockTxSuccess := container.NewMockOracleTx(ctrl)
tdSuccess := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
OracleTx: mockTxSuccess,
}
mockTxSuccess.EXPECT().
ExecContext(gomock.Any(), insertOracleGoFrMigrationRow,
tdSuccess.MigrationNumber, "UP", tdSuccess.StartTime, gomock.Any()).
Return(nil)
mockTxSuccess.EXPECT().Commit().Return(nil)
err := mg.commitMigration(mockContainer, tdSuccess)
require.NoError(t, err, "Success case failed")
// Error case
mockTxError := container.NewMockOracleTx(ctrl)
tdError := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
OracleTx: mockTxError,
}
mockTxError.EXPECT().
ExecContext(gomock.Any(), insertOracleGoFrMigrationRow,
tdError.MigrationNumber, "UP", tdError.StartTime, gomock.Any()).
Return(sql.ErrConnDone)
mockTxError.EXPECT().Rollback().Return(nil).AnyTimes()
err = mg.commitMigration(mockContainer, tdError)
assert.Equal(t, sql.ErrConnDone, err, "Error case failed")
}
func TestOracleMigration_RunMigrationSuccess(t *testing.T) {
mockOracle, mockContainer := initializeOracleRunMocks(t)
ctrl := gomock.NewController(t)
mockTx := container.NewMockOracleTx(ctrl)
migrationMap := map[int64]Migrate{
1: {UP: func(d Datasource) error {
return d.Oracle.Exec(context.Background(), "CREATE TABLE test (id INT)")
}},
}
// 1. Create migration table
mockOracle.EXPECT().Exec(gomock.Any(), checkAndCreateOracleMigrationTable).Return(nil)
// 2. Create migration lock table
mockOracle.EXPECT().Exec(gomock.Any(), checkAndCreateOracleMigrationLocksTable).Return(nil)
// 3. Optimistic pre-check + re-fetch under lock: get last migration
mockOracle.EXPECT().Select(gomock.Any(), gomock.Any(), getLastOracleGoFrMigration).
DoAndReturn(func(_ context.Context, dest any, _ string, _ ...any) error {
results := dest.(*[]map[string]any)
*results = []map[string]any{
{"LAST_MIGRATION": int64(0)},
}
return nil
}).Times(2)
// 4. Acquire lock: clean up expired rows
mockOracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
// 5. Acquire lock: insert lock row
mockOracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, gomock.Any(), gomock.Any()).Return(nil)
// 6. Begin transaction
mockOracle.EXPECT().Begin().Return(mockTx, nil)
// 7. Execute migration via transaction wrapper
mockTx.EXPECT().ExecContext(gomock.Any(), "CREATE TABLE test (id INT)").Return(nil)
// 8. Insert migration record
mockTx.EXPECT().ExecContext(gomock.Any(), insertOracleGoFrMigrationRow,
int64(1), "UP", gomock.Any(), gomock.Any()).Return(nil)
// 9. Commit transaction
mockTx.EXPECT().Commit().Return(nil)
// 10. Unlock: delete lock row
mockOracle.EXPECT().Exec(gomock.Any(), deleteOracleLock, lockKey, gomock.Any()).Return(nil)
Run(migrationMap, mockContainer)
}
func TestOracleMigration_FailCreateMigrationTable(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockContainer, mocks := container.NewMockContainer(t)
mockOracle := mocks.Oracle
mockContainer.Oracle = mockOracle
ds := Datasource{Oracle: mockOracle}
od := oracleDS{Oracle: mockOracle}
mg := od.apply(&ds)
mockOracle.EXPECT().Exec(gomock.Any(), checkAndCreateOracleMigrationTable).Return(sql.ErrConnDone)
err := mg.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, sql.ErrConnDone, err)
}
func TestOracleMigration_GetLastMigration_ReturnsZeroOnError(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockContainer, mocks := container.NewMockContainer(t)
mockOracle := mocks.Oracle
mockContainer.Oracle = mockOracle
ds := Datasource{Oracle: mockOracle}
od := oracleDS{Oracle: mockOracle}
mg := od.apply(&ds)
mockOracle.EXPECT().Select(gomock.Any(), gomock.Any(), getLastOracleGoFrMigration).Return(sql.ErrConnDone)
lastMigration, err := mg.getLastMigration(mockContainer)
assert.Equal(t, int64(-1), lastMigration)
assert.ErrorContains(t, err, sql.ErrConnDone.Error())
}
func TestOracleMigrator_Lock(t *testing.T) {
t.Run("LockSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// Cleanup expired locks
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
// Insert lock succeeds
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).Return(nil)
// Chain to inner migrator
mockMigrator.EXPECT().lock(ctx, gomock.Any(), mockContainer, "owner-1").Return(nil)
err := m.lock(ctx, cancel, mockContainer, "owner-1")
require.NoError(t, err)
})
t.Run("LockRetryThenSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// First attempt: cleanup OK, insert fails with duplicate key
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).
Return(errOracleDuplicateKey)
// Second attempt: cleanup OK, insert succeeds
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).Return(nil)
mockMigrator.EXPECT().lock(ctx, gomock.Any(), mockContainer, "owner-1").Return(nil)
err := m.lock(ctx, cancel, mockContainer, "owner-1")
require.NoError(t, err)
})
t.Run("LockAcquireSystemError", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).
Return(errOracleSystemError)
err := m.lock(ctx, cancel, mockContainer, "owner-1")
assert.Equal(t, errLockAcquisitionFailed, err)
})
t.Run("LockContextCancelledWhileRetrying", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// Lock is held; insert fails with duplicate key
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(nil)
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).
Return(errOracleDuplicateKey)
// Cancel the context while the lock is retrying
go func() {
time.Sleep(100 * time.Millisecond)
cancel()
}()
err := m.lock(ctx, cancel, mockContainer, "owner-1")
require.ErrorIs(t, err, context.Canceled)
})
}
func TestOracleMigrator_Unlock(t *testing.T) {
t.Run("UnlockSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteOracleLock, lockKey, "owner-1").Return(nil)
mockMigrator.EXPECT().unlock(mockContainer, "owner-1").Return(nil)
err := m.unlock(mockContainer, "owner-1")
require.NoError(t, err)
})
t.Run("UnlockError", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteOracleLock, lockKey, "owner-1").
Return(sql.ErrConnDone)
err := m.unlock(mockContainer, "owner-1")
assert.Equal(t, errLockReleaseFailed, err)
})
t.Run("UnlockLockStolen", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
// PL/SQL raises error when 0 rows deleted (lock was stolen)
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteOracleLock, lockKey, "owner-1").
Return(errOracleLockStolen)
err := m.unlock(mockContainer, "owner-1")
assert.Equal(t, errLockReleaseFailed, err)
})
}
func TestOracleMigrator_StartRefresh(t *testing.T) {
t.Run("RefreshSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// Expect at least one refresh tick within the test window
mocks.Oracle.EXPECT().
Exec(gomock.Any(), updateOracleLock, gomock.Any(), lockKey, "owner-1").
Return(nil).MinTimes(1)
go m.startRefresh(ctx, cancel, mockContainer, "owner-1")
time.Sleep(defaultRefresh + 100*time.Millisecond)
cancel()
// Allow the goroutine to exit
time.Sleep(50 * time.Millisecond)
select {
case <-ctx.Done():
require.ErrorIs(t, ctx.Err(), context.Canceled)
default:
t.Error("expected context to be done after cancel")
}
})
t.Run("RefreshError", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
mocks.Oracle.EXPECT().
Exec(gomock.Any(), updateOracleLock, gomock.Any(), lockKey, "owner-1").
Return(sql.ErrConnDone).Times(1)
go m.startRefresh(ctx, cancel, mockContainer, "owner-1")
select {
case <-ctx.Done():
require.Error(t, ctx.Err())
case <-time.After(defaultRefresh * 2):
t.Error("expected context to be canceled after refresh error, but timed out")
}
})
t.Run("RefreshLockLost", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// PL/SQL raises error when 0 rows updated (lock was stolen)
mocks.Oracle.EXPECT().
Exec(gomock.Any(), updateOracleLock, gomock.Any(), lockKey, "owner-1").
Return(errOracleLockLost).Times(1)
go m.startRefresh(ctx, cancel, mockContainer, "owner-1")
select {
case <-ctx.Done():
require.Error(t, ctx.Err())
case <-time.After(defaultRefresh * 2):
t.Error("expected context to be canceled after lock loss, but timed out")
}
})
}
func TestOracleMigrator_Name(t *testing.T) {
m := oracleMigrator{}
assert.Equal(t, "Oracle", m.name())
}
func TestOracleMigrator_LockWithCleanupError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{Oracle: mocks.Oracle, migrator: mockMigrator}
mockContainer.Oracle = mocks.Oracle
ctx, cancel := context.WithCancel(t.Context())
// Cleanup fails but should not block lock acquisition
mocks.Oracle.EXPECT().Exec(gomock.Any(), deleteExpiredOracleLocks, gomock.Any()).Return(sql.ErrConnDone)
// Insert succeeds
mocks.Oracle.EXPECT().Exec(gomock.Any(), insertOracleLock, lockKey, "owner-1", gomock.Any()).Return(nil)
mockMigrator.EXPECT().lock(ctx, gomock.Any(), mockContainer, "owner-1").Return(nil)
err := m.lock(ctx, cancel, mockContainer, "owner-1")
require.NoError(t, err)
}
func TestOracleMigrator_Rollback(t *testing.T) {
t.Run("NilTransaction", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, _ := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := oracleMigrator{migrator: mockMigrator}
data := transactionData{MigrationNumber: 1}
mockMigrator.EXPECT().rollback(mockContainer, data)
m.rollback(mockContainer, data)
})
t.Run("RollbackSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, _ := container.NewMockContainer(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
mockMigrator := NewMockmigrator(ctrl)
mockTx := container.NewMockOracleTx(ctrl)
m := oracleMigrator{migrator: mockMigrator}
data := transactionData{MigrationNumber: 1, OracleTx: mockTx}
mockTx.EXPECT().Rollback().Return(nil)
mockLogger.EXPECT().Fatalf(gomock.Any())
mockMigrator.EXPECT().rollback(mockContainer, data)
m.rollback(mockContainer, data)
})
t.Run("RollbackError", func(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, _ := container.NewMockContainer(t)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
mockMigrator := NewMockmigrator(ctrl)
mockTx := container.NewMockOracleTx(ctrl)
m := oracleMigrator{migrator: mockMigrator}
data := transactionData{MigrationNumber: 1, OracleTx: mockTx}
mockTx.EXPECT().Rollback().Return(sql.ErrConnDone)
mockLogger.EXPECT().Fatalf(gomock.Any(), gomock.Any())
mockMigrator.EXPECT().rollback(mockContainer, data)
m.rollback(mockContainer, data)
})
}
func TestOracleMigrator_ConvertToInt64(t *testing.T) {
m := oracleMigrator{}
tests := []struct {
desc string
value any
expected int64
}{
{"float64", float64(42.7), 42},
{"int64", int64(99), 99},
{"int", int(7), 7},
{"parseable string", "123", 123},
{"negative string", "-5", -5},
{"unparsable string", "not-a-number", 0},
{"empty string", "", 0},
{"nil (becomes )", nil, 0},
}
for i, tc := range tests {
result := m.convertToInt64(tc.value)
assert.Equal(t, tc.expected, result, "TEST[%d]: %s", i, tc.desc)
}
}
func TestOracleMigrator_ParseStringValue(t *testing.T) {
m := oracleMigrator{}
tests := []struct {
desc string
value any
expected int64
}{
{"valid integer string", "42", 42},
{"empty string", "", 0},
{"nil-representation", "", 0},
{"nil value", nil, 0},
{"invalid string", "not-a-number", 0},
{"negative string", "-10", -10},
}
for i, tc := range tests {
result := m.parseStringValue(tc.value)
assert.Equal(t, tc.expected, result, "TEST[%d]: %s", i, tc.desc)
}
}
func TestOracleTransactionWrapper_Select(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockTx := container.NewMockOracleTx(ctrl)
wrapper := &oracleTransactionWrapper{tx: mockTx}
t.Run("SelectSuccess", func(t *testing.T) {
var dest []map[string]any
mockTx.EXPECT().SelectContext(gomock.Any(), &dest, "SELECT 1", gomock.Any()).Return(nil)
err := wrapper.Select(t.Context(), &dest, "SELECT 1")
require.NoError(t, err)
})
t.Run("SelectError", func(t *testing.T) {
var dest []map[string]any
mockTx.EXPECT().SelectContext(gomock.Any(), &dest, "SELECT 1", gomock.Any()).Return(sql.ErrConnDone)
err := wrapper.Select(t.Context(), &dest, "SELECT 1")
assert.Equal(t, sql.ErrConnDone, err)
})
}
func TestOracleTransactionWrapper_Begin(t *testing.T) {
wrapper := &oracleTransactionWrapper{}
tx, err := wrapper.Begin()
assert.Nil(t, tx)
assert.Equal(t, errNestedTransactionNotSupported, err)
}
func initializeOracleRunMocks(t *testing.T) (*container.MockOracleDB, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockOracle := mocks.Oracle
// Disable all other datasources by setting to nil.
mockContainer.SQL = nil
mockContainer.Redis = nil
mockContainer.Mongo = nil
mockContainer.Cassandra = nil
mockContainer.PubSub = nil
mockContainer.ArangoDB = nil
mockContainer.SurrealDB = nil
mockContainer.DGraph = nil
mockContainer.Elasticsearch = nil
mockContainer.OpenTSDB = nil
mockContainer.ScyllaDB = nil
mockContainer.Clickhouse = nil
// Initialize Oracle mock and Logger.
mockContainer.Oracle = mockOracle
mockContainer.Logger = logging.NewMockLogger(logging.DEBUG)
return mockOracle, mockContainer
}
================================================
FILE: pkg/gofr/migration/pubsub.go
================================================
package migration
import (
"context"
"gofr.dev/pkg/gofr/container"
)
type pubsubDS struct {
client PubSub
}
// pubsubMigrator wraps the next migrator in the chain.
// It is kept for structural consistency but no longer manages its own migration state
// to prevent "ghost data" conflicts on persistent PubSub backends.
type pubsubMigrator struct {
PubSub
migrator migrator
}
func (ds pubsubDS) CreateTopic(ctx context.Context, name string) error {
return ds.client.CreateTopic(ctx, name)
}
func (ds pubsubDS) DeleteTopic(ctx context.Context, name string) error {
return ds.client.DeleteTopic(ctx, name)
}
func (ds pubsubDS) Query(ctx context.Context, query string, args ...any) ([]byte, error) {
return ds.client.Query(ctx, query, args...)
}
func (ds pubsubDS) apply(m migrator) migrator {
return pubsubMigrator{
PubSub: ds,
migrator: m,
}
}
func (pm pubsubMigrator) checkAndCreateMigrationTable(c *container.Container) error {
return pm.migrator.checkAndCreateMigrationTable(c)
}
func (pm pubsubMigrator) getLastMigration(c *container.Container) (int64, error) {
// PubSub no longer participates in version tracking.
// We delegate directly to the next migrator in the chain (SQL, Redis, etc.).
return pm.migrator.getLastMigration(c)
}
func (pm pubsubMigrator) beginTransaction(c *container.Container) transactionData {
return pm.migrator.beginTransaction(c)
}
func (pm pubsubMigrator) commitMigration(c *container.Container, data transactionData) error {
// No migration entry is added to PubSub anymore.
// We only commit the migration in the primary data source.
return pm.migrator.commitMigration(c, data)
}
func (pm pubsubMigrator) rollback(c *container.Container, data transactionData) {
pm.migrator.rollback(c, data)
}
func (pm pubsubMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return pm.migrator.lock(ctx, cancel, c, ownerID)
}
func (pm pubsubMigrator) unlock(c *container.Container, ownerID string) error {
return pm.migrator.unlock(c, ownerID)
}
func (pubsubMigrator) name() string {
return "PubSub"
}
================================================
FILE: pkg/gofr/migration/pubsub_test.go
================================================
package migration
import (
"context"
"errors"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/container"
)
var errTopic = errors.New("error topic")
func Test_pubsubDS_Methods(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockPubSub := NewMockPubSub(ctrl)
ds := pubsubDS{client: mockPubSub}
ctx := t.Context()
t.Run("CreateTopic", func(t *testing.T) {
mockPubSub.EXPECT().CreateTopic(ctx, "test").Return(nil)
require.NoError(t, ds.CreateTopic(ctx, "test"))
mockPubSub.EXPECT().CreateTopic(ctx, "test").Return(errTopic)
assert.Equal(t, errTopic, ds.CreateTopic(ctx, "test"))
})
t.Run("DeleteTopic", func(t *testing.T) {
mockPubSub.EXPECT().DeleteTopic(ctx, "test").Return(nil)
require.NoError(t, ds.DeleteTopic(ctx, "test"))
mockPubSub.EXPECT().DeleteTopic(ctx, "test").Return(errTopic)
assert.Equal(t, errTopic, ds.DeleteTopic(ctx, "test"))
})
t.Run("Query", func(t *testing.T) {
mockPubSub.EXPECT().Query(ctx, "query", "arg1").Return([]byte("result"), nil)
res, err := ds.Query(ctx, "query", "arg1")
require.NoError(t, err)
assert.Equal(t, []byte("result"), res)
mockPubSub.EXPECT().Query(ctx, "query").Return(nil, errTopic)
_, err = ds.Query(ctx, "query")
assert.Equal(t, errTopic, err)
})
}
func Test_pubsubDS_apply(t *testing.T) {
c, _ := container.NewMockContainer(t)
ds := &Datasource{}
p := pubsubDS{client: c.PubSub}
// apply should return a pubsubMigrator that wraps the passed migrator
result := p.apply(ds)
pm, ok := result.(pubsubMigrator)
assert.True(t, ok, "result should be a pubsubMigrator")
assert.Equal(t, ds, pm.migrator, "pubsubMigrator should wrap the passed migrator")
}
func Test_pubsubMigrator_Delegation(t *testing.T) {
c, _ := container.NewMockContainer(t)
ds := &Datasource{}
p := pubsubMigrator{PubSub: pubsubDS{client: c.PubSub}, migrator: ds}
// All these methods should delegate to the base migrator (ds) without error
require.NoError(t, p.checkAndCreateMigrationTable(c))
v, err := p.getLastMigration(c)
require.NoError(t, err)
assert.Equal(t, int64(0), v)
assert.NotNil(t, p.beginTransaction(c))
require.NoError(t, p.commitMigration(c, transactionData{}))
// Should not panic
p.rollback(c, transactionData{})
require.NoError(t, p.lock(context.TODO(), nil, c, "owner"))
require.NoError(t, p.unlock(c, "owner"))
assert.Equal(t, "PubSub", p.name())
}
func Test_PubSub_GhostDataConflict(t *testing.T) {
// Setup miniredis
s := miniredis.RunT(t)
// Setup Container with Redis and PubSub pointing to the same miniredis using built-in MockConfig
conf := config.NewMockConfig(map[string]string{
"APP_NAME": "integration-test",
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
"REDIS_DB": "0",
"PUBSUB_BACKEND": "REDIS",
"REDIS_PUBSUB_DB": "1",
"REDIS_PUBSUB_MODE": "streams",
})
c := container.NewContainer(conf)
defer c.Close()
// Seed Ghost Data in PubSub DB (DB 1)
client := redis.NewClient(&redis.Options{Addr: s.Addr(), DB: 1})
err := client.XAdd(context.Background(), &redis.XAddArgs{
Stream: "gofr_migrations",
Values: map[string]any{
"payload": `{"version":20260101000000,"method":"UP","start_time":1700000000,"duration":100}`,
},
}).Err()
require.NoError(t, err)
// Run Migration Check
base := &Datasource{Redis: c.Redis}
rm := redisMigrator{Redis: c.Redis, migrator: base}
ps := pubsubDS{client: c.PubSub}
pm := ps.apply(rm)
version, err := pm.getLastMigration(c)
require.NoError(t, err)
assert.Equal(t, int64(0), version, "Migration version should be 0, ignoring ghost data in PubSub")
}
func Test_PubSub_NoEntryAdded(t *testing.T) {
s := miniredis.RunT(t)
conf := config.NewMockConfig(map[string]string{
"REDIS_HOST": s.Host(),
"REDIS_PORT": s.Port(),
"PUBSUB_BACKEND": "REDIS",
"REDIS_PUBSUB_DB": "1",
})
c := container.NewContainer(conf)
defer c.Close()
base := &Datasource{Redis: c.Redis}
rm := redisMigrator{Redis: c.Redis, migrator: base}
ps := pubsubDS{client: c.PubSub}
pm := ps.apply(rm)
data := pm.beginTransaction(c)
data.MigrationNumber = 20240304
data.StartTime = time.Now()
err := pm.commitMigration(c, data)
require.NoError(t, err)
// Check if entry was added to DB 1 (it should NOT be)
client := redis.NewClient(&redis.Options{Addr: s.Addr(), DB: 1})
count, err := client.XLen(context.Background(), "gofr_migrations").Result()
require.NoError(t, err)
assert.Equal(t, int64(0), count, "No migration entry should be added to PubSub backend")
// Check if entry was added to DB 0
client0 := redis.NewClient(&redis.Options{Addr: s.Addr(), DB: 0})
exists, err := client0.HExists(context.Background(), "gofr_migrations", "20240304").Result()
require.NoError(t, err)
assert.True(t, exists, "Migration entry should be added to primary Redis DB")
}
================================================
FILE: pkg/gofr/migration/redis.go
================================================
package migration
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"gofr.dev/pkg/gofr/container"
)
var errRedisLockRefreshFailed = errors.New("failed to refresh Redis lock: lock lost or stolen")
type redisDS struct {
Redis
}
func (r redisDS) apply(m migrator) migrator {
return redisMigrator{
Redis: r.Redis,
migrator: m,
}
}
type redisMigrator struct {
Redis
migrator
}
type redisData struct {
Method string `json:"method"`
StartTime time.Time `json:"startTime"`
Duration int64 `json:"duration"`
}
func (m redisMigrator) getLastMigration(c *container.Container) (int64, error) {
var lastMigration int64
table, err := c.Redis.HGetAll(context.Background(), "gofr_migrations").Result()
if err != nil {
return -1, fmt.Errorf("redis: %w", err)
}
for key, value := range table {
integerValue, _ := strconv.ParseInt(key, 10, 64)
if integerValue > lastMigration {
lastMigration = integerValue
}
var data redisData
err = json.Unmarshal([]byte(value), &data)
if err != nil {
return -1, fmt.Errorf("redis: %w", err)
}
}
c.Debugf("Redis last migration fetched value is: %v", lastMigration)
last, err := m.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, last), nil
}
func (m redisMigrator) beginTransaction(c *container.Container) transactionData {
redisTx := c.Redis.TxPipeline()
cmt := m.migrator.beginTransaction(c)
cmt.RedisTx = redisTx
c.Debug("Redis Transaction begin successful")
return cmt
}
func (m redisMigrator) commitMigration(c *container.Container, data transactionData) error {
migrationVersion := strconv.FormatInt(data.MigrationNumber, 10)
jsonData, err := json.Marshal(redisData{
Method: "UP",
StartTime: data.StartTime,
Duration: time.Since(data.StartTime).Milliseconds(),
})
if err != nil {
c.Logger.Errorf("migration %v for Redis failed with err: %v", migrationVersion, err)
return err
}
_, err = data.RedisTx.HSet(context.Background(), "gofr_migrations", map[string]string{migrationVersion: string(jsonData)}).Result()
if err != nil {
c.Logger.Errorf("migration %v for Redis failed with err: %v", migrationVersion, err)
return err
}
_, err = data.RedisTx.Exec(context.Background())
if err != nil {
c.Logger.Errorf("migration %v for Redis failed with err: %v", migrationVersion, err)
return err
}
return m.migrator.commitMigration(c, data)
}
func (m redisMigrator) rollback(c *container.Container, data transactionData) {
data.RedisTx.Discard()
m.migrator.rollback(c, data)
c.Fatalf("Migration %v for Redis failed and rolled back", data.MigrationNumber)
}
func (m redisMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
for i := 0; ; i++ {
status, err := c.Redis.SetNX(ctx, lockKey, ownerID, defaultLockTTL).Result()
if err == nil && status {
c.Debug("Redis lock acquired successfully")
go m.startRefresh(ctx, cancel, c, ownerID)
return m.migrator.lock(ctx, cancel, c, ownerID)
}
if err != nil {
c.Errorf("error while acquiring redis lock: %v", err)
return errLockAcquisitionFailed
}
c.Debugf("Redis lock already held, retrying in %v... (attempt %d)", defaultRetry, i+1)
select {
case <-time.After(defaultRetry):
case <-ctx.Done():
return ctx.Err()
}
}
}
func (redisMigrator) startRefresh(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) {
ticker := time.NewTicker(defaultRefresh)
defer ticker.Stop()
for {
select {
case <-ticker.C:
// Use Lua script to ensure we only refresh the lock if we own it
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
`
val, err := c.Redis.Eval(ctx, script, []string{lockKey}, ownerID, int(defaultLockTTL.Seconds())).Result()
if err != nil {
c.Errorf("failed to refresh Redis lock: %v", err)
cancel()
return
}
if val == int64(0) {
c.Errorf("%v", errRedisLockRefreshFailed)
cancel()
return
}
c.Debug("Redis lock refreshed successfully")
case <-ctx.Done():
return
}
}
}
func (m redisMigrator) unlock(c *container.Container, ownerID string) error {
// Use Lua script to ensure we only delete the lock if we own it
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
result, err := c.Redis.Eval(context.Background(), script, []string{lockKey}, ownerID).Result()
if err != nil {
c.Errorf("unable to release redis lock: %v", err)
return errLockReleaseFailed
}
// Check if the lock was actually deleted (result should be 1)
deleted, ok := result.(int64)
if !ok || deleted == 0 {
c.Errorf("failed to release Redis lock: lock was already released or stolen")
return errLockReleaseFailed
}
c.Debug("Redis lock released successfully")
return m.migrator.unlock(c, ownerID)
}
func (redisMigrator) name() string {
return "Redis"
}
================================================
FILE: pkg/gofr/migration/redis_test.go
================================================
package migration
import (
"context"
"encoding/json"
"errors"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
goRedis "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
)
var (
errRefreshFailed = errors.New("refresh failed")
errRedis = errors.New("redis error")
errEval = errors.New("eval error")
)
func TestRedis_Get(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockCmd := NewMockRedis(ctrl)
mockCmd.EXPECT().Get(t.Context(), "test_key").Return(&goRedis.StringCmd{})
r := redisDS{mockCmd}
_, err := r.Get(t.Context(), "test_key").Result()
require.NoError(t, err, "TEST Failed.\n")
}
func TestRedis_Set(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockCmd := NewMockRedis(ctrl)
mockCmd.EXPECT().Set(t.Context(), "test_key", "test_value", time.Duration(0)).Return(&goRedis.StatusCmd{})
r := redisDS{mockCmd}
_, err := r.Set(t.Context(), "test_key", "test_value", 0).Result()
require.NoError(t, err, "TEST Failed.\n")
}
func TestRedis_Del(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockCmd := NewMockRedis(ctrl)
mockCmd.EXPECT().Del(t.Context(), "test_key").Return(&goRedis.IntCmd{})
r := redisDS{mockCmd}
_, err := r.Del(t.Context(), "test_key").Result()
require.NoError(t, err, "TEST Failed.\n")
}
func TestRedis_Rename(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockCmd := NewMockRedis(ctrl)
mockCmd.EXPECT().Rename(t.Context(), "test_key", "test_new_key").Return(&goRedis.StatusCmd{})
r := redisDS{mockCmd}
_, err := r.Rename(t.Context(), "test_key", "test_new_key").Result()
require.NoError(t, err, "TEST Failed.\n")
}
func TestRedisMigrator_GetLastMigration(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{
Redis: mocks.Redis,
migrator: mockMigrator,
}
tests := []struct {
desc string
mockedData map[string]string
redisErr error
migratorLastMigration int64
migratorErr error
expectedLastMigration int64
expectedErr error
}{
{
desc: "Successful",
mockedData: map[string]string{
"1": `{"method":"UP","startTime":"2024-01-01T00:00:00Z","duration":1000}`,
"2": `{"method":"UP","startTime":"2024-01-02T00:00:00Z","duration":2000}`,
},
migratorLastMigration: 3,
expectedLastMigration: 3,
},
{
desc: "ErrorFromHGetAll",
redisErr: goRedis.ErrClosed,
expectedLastMigration: -1,
expectedErr: goRedis.ErrClosed,
},
{
desc: "UnmarshalError",
mockedData: map[string]string{
"1": `{"method":"UP","startTime":"2024-01-01T00:00:00Z","duration":1000}`,
"2": "invalid JSON data",
},
expectedLastMigration: -1,
expectedErr: &json.SyntaxError{},
},
{
desc: "lm2IsLessThanLastMigration",
mockedData: map[string]string{
"1": `{"method":"UP","startTime":"2024-01-01T00:00:00Z","duration":1000}`,
"2": `{"method":"UP","startTime":"2024-01-02T00:00:00Z","duration":2000}`,
},
migratorLastMigration: 1,
expectedLastMigration: 3,
},
}
for i, tc := range tests {
mocks.Redis.EXPECT().HGetAll(gomock.Any(), "gofr_migrations").Return(
goRedis.NewMapStringStringResult(tc.mockedData, tc.redisErr))
if tc.redisErr == nil && tc.desc != "UnmarshalError" {
mockMigrator.EXPECT().getLastMigration(gomock.Any()).Return(tc.migratorLastMigration, tc.migratorErr).MaxTimes(2)
}
lastMigration, err := m.getLastMigration(c)
assert.Equal(t, tc.expectedLastMigration, lastMigration, "TEST[%d], Failed.\n%s", i, tc.desc)
if tc.expectedErr != nil {
assert.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
}
}
}
func TestRedisMigrator_beginTransaction(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
s, _ := miniredis.Run()
defer s.Close()
client := goRedis.NewClient(&goRedis.Options{Addr: s.Addr()})
pipeliner := client.TxPipeline()
m := redisMigrator{
Redis: client,
migrator: mockMigrator,
}
mocks.Redis.EXPECT().TxPipeline().Return(pipeliner)
mockMigrator.EXPECT().beginTransaction(gomock.Any()).Return(transactionData{})
data := m.beginTransaction(c)
assert.NotNil(t, data.RedisTx)
}
func TestRedisMigrator_StartRefreshSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
// The refresh happens every defaultRefresh interval (5 seconds)
// We expect at least one call within our test window
mocks.Redis.EXPECT().Eval(gomock.Any(), gomock.Any(), []string{lockKey}, "1", int(defaultLockTTL.Seconds())).
Return(goRedis.NewCmdResult(int64(1), nil)).MinTimes(1).MaxTimes(2)
go m.startRefresh(ctx, cancel, c, "1")
// Wait enough time for at least one refresh cycle
time.Sleep(defaultRefresh + 100*time.Millisecond)
cancel()
// Give goroutine time to exit gracefully
time.Sleep(50 * time.Millisecond)
select {
case <-ctx.Done():
// Check if it was canceled by us (success) or something else
if !errors.Is(ctx.Err(), context.Canceled) {
t.Errorf("Unexpected context error: %v", ctx.Err())
}
default:
t.Error("Expected context to be done")
}
}
func TestRedisMigrator_StartRefreshError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
mocks.Redis.EXPECT().Eval(gomock.Any(), gomock.Any(), []string{lockKey}, "1", int(defaultLockTTL.Seconds())).
Return(goRedis.NewCmdResult(int64(0), errRefreshFailed)).Times(1)
go m.startRefresh(ctx, cancel, c, "1")
select {
case <-ctx.Done():
// In this version, cancel() doesn't pass the error, but it does cancel the context.
require.Error(t, ctx.Err())
case <-time.After(defaultRefresh * 2):
t.Error("Expected context to be canceled, but timed out")
}
}
func TestRedisMigrator_StartRefreshLockLost(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
// Lock returns 0, indicating lock was lost
mocks.Redis.EXPECT().Eval(gomock.Any(), gomock.Any(), []string{lockKey}, "1", int(defaultLockTTL.Seconds())).
Return(goRedis.NewCmdResult(int64(0), nil)).Times(1)
go m.startRefresh(ctx, cancel, c, "1")
select {
case <-ctx.Done():
require.Error(t, ctx.Err())
case <-time.After(defaultRefresh * 2):
t.Error("Expected context to be canceled, but timed out")
}
}
func TestRedisMigrator_Lock(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
// Test Success
mocks.Redis.EXPECT().SetNX(gomock.Any(), lockKey, "owner-1", defaultLockTTL).Return(goRedis.NewBoolResult(true, nil))
mockMigrator.EXPECT().lock(ctx, gomock.Any(), gomock.Any(), "owner-1").Return(nil)
err := m.lock(ctx, cancel, c, "owner-1")
require.NoError(t, err)
// Test Error
mocks.Redis.EXPECT().SetNX(gomock.Any(), lockKey, "owner-1", defaultLockTTL).Return(goRedis.NewBoolResult(false, errRedis))
err = m.lock(ctx, cancel, c, "owner-1")
assert.Equal(t, errLockAcquisitionFailed, err)
}
func TestRedisMigrator_Unlock(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
mocks.Redis.EXPECT().Eval(gomock.Any(), gomock.Any(), []string{lockKey}, "owner-1").Return(goRedis.NewCmdResult(int64(1), nil))
mockMigrator.EXPECT().unlock(gomock.Any(), "owner-1").Return(nil)
err := m.unlock(c, "owner-1")
assert.NoError(t, err)
}
func TestRedisMigrator_CommitMigration(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, _ := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
s, _ := miniredis.Run()
defer s.Close()
client := goRedis.NewClient(&goRedis.Options{Addr: s.Addr()})
m := redisMigrator{Redis: client, migrator: mockMigrator}
pipeliner := client.TxPipeline()
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now().Add(-1 * time.Second),
RedisTx: pipeliner,
}
mockMigrator.EXPECT().commitMigration(c, data).Return(nil)
err := m.commitMigration(c, data)
require.NoError(t, err)
// Verify data was written to miniredis
val := s.HGet("gofr_migrations", "1")
assert.NotEmpty(t, val)
}
func TestRedisMigrator_CommitMigration_ExecError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, _ := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
mockLogger := container.NewMockLogger(ctrl)
c.Logger = mockLogger
s, _ := miniredis.Run()
defer s.Close()
client := goRedis.NewClient(&goRedis.Options{Addr: s.Addr()})
m := redisMigrator{Redis: client, migrator: mockMigrator}
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now(),
RedisTx: client.TxPipeline(),
}
// We close the miniredis to simulate an execution error
s.Close()
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
err := m.commitMigration(c, data)
assert.Error(t, err)
}
func TestRedisMigrator_Rollback(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, _ := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
mockLogger := container.NewMockLogger(ctrl)
c.Logger = mockLogger
s, _ := miniredis.Run()
defer s.Close()
client := goRedis.NewClient(&goRedis.Options{Addr: s.Addr()})
m := redisMigrator{Redis: client, migrator: mockMigrator}
data := transactionData{
MigrationNumber: 1,
RedisTx: client.TxPipeline(),
}
mockMigrator.EXPECT().rollback(c, data)
mockLogger.EXPECT().Fatalf(gomock.Any(), gomock.Any())
m.rollback(c, data)
}
func TestRedisMigrator_UnlockError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
c, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
mockLogger := container.NewMockLogger(ctrl)
c.Logger = mockLogger
m := redisMigrator{Redis: mocks.Redis, migrator: mockMigrator}
testErr := errEval
mocks.Redis.EXPECT().Eval(gomock.Any(), gomock.Any(), []string{lockKey}, "owner-1").Return(goRedis.NewCmdResult(nil, testErr))
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).AnyTimes()
err := m.unlock(c, "owner-1")
assert.Equal(t, errLockReleaseFailed, err)
}
func TestRedisMigrator_Name(t *testing.T) {
m := redisMigrator{}
assert.Equal(t, "Redis", m.name())
}
================================================
FILE: pkg/gofr/migration/scylla_db.go
================================================
package migration
import (
"context"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
type scyllaDS struct {
ScyllaDB
}
type scyllaMigrator struct {
ScyllaDB
migrator
}
func (ds scyllaDS) apply(m migrator) migrator {
return scyllaMigrator{
ScyllaDB: ds.ScyllaDB,
migrator: m,
}
}
const (
scyllaDBMigrationTable = "gofr_migrations"
)
func (s scyllaMigrator) checkAndCreateMigrationTable(c *container.Container) error {
createTableQuery := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
version bigint PRIMARY KEY,
method text,
start_time timestamp,
duration bigint
);
`, scyllaDBMigrationTable)
err := s.ScyllaDB.Exec(createTableQuery)
if err != nil {
c.Errorf("Failed to create migration table: %v", err)
return err
}
return s.migrator.checkAndCreateMigrationTable(c)
}
type migrationRow struct {
Version int64 `db:"version"`
}
func (s scyllaMigrator) getLastMigration(c *container.Container) (int64, error) {
var (
migrations []migrationRow
lastVersion int64
)
query := fmt.Sprintf("SELECT version FROM %s", scyllaDBMigrationTable)
err := s.ScyllaDB.Query(&migrations, query)
if err != nil {
return -1, fmt.Errorf("scylladb: %w", err)
}
for _, m := range migrations {
if m.Version > lastVersion {
lastVersion = m.Version
}
}
c.Debugf("ScyllaDB last migration fetched value is: %v", lastVersion)
lm2, err := s.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastVersion, lm2), nil
}
func (s scyllaMigrator) beginTransaction(c *container.Container) transactionData {
return s.migrator.beginTransaction(c)
}
func (s scyllaMigrator) commitMigration(c *container.Container, data transactionData) error {
insertStmt := fmt.Sprintf(`
INSERT INTO %s (version, method, start_time, duration)
VALUES (?, ?, ?, ?);
`, scyllaDBMigrationTable)
err := s.ScyllaDB.Exec(insertStmt,
data.MigrationNumber,
"UP",
data.StartTime,
time.Since(data.StartTime).Milliseconds(),
)
if err != nil {
c.Errorf("Failed to insert migration record: %v", err)
return err
}
c.Debugf("Inserted migration record for version %v into ScyllaDB", data.MigrationNumber)
return s.migrator.commitMigration(c, data)
}
func (s scyllaMigrator) rollback(c *container.Container, data transactionData) {
s.migrator.rollback(c, data)
c.Fatalf("Migration %v failed.", data.MigrationNumber)
}
func (s scyllaMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return s.migrator.lock(ctx, cancel, c, ownerID)
}
func (s scyllaMigrator) unlock(c *container.Container, ownerID string) error {
return s.migrator.unlock(c, ownerID)
}
func (scyllaMigrator) name() string {
return "ScyllaDB"
}
================================================
FILE: pkg/gofr/migration/scylla_db_test.go
================================================
package migration
import (
"errors"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/logging"
)
var errScyllaConn = errors.New("error connecting to ScyllaDB")
type panicLogger struct{}
func (*panicLogger) Fatalf(format string, args ...any) {
panic(fmt.Sprintf(format, args...))
}
func (*panicLogger) Fatal(args ...any) {
panic(fmt.Sprint(args...))
}
func (*panicLogger) Errorf(_ string, _ ...any) {}
func (*panicLogger) Error(_ ...any) {}
func (*panicLogger) Debugf(_ string, _ ...any) {}
func (*panicLogger) Noticef(_ string, _ ...any) {}
func (*panicLogger) Debug(_ ...any) {}
func (*panicLogger) Infof(_ string, _ ...any) {}
func (*panicLogger) Info(_ ...any) {}
func (*panicLogger) Notice(_ ...any) {}
func (*panicLogger) Warn(_ ...any) {}
func (*panicLogger) Warnf(_ string, _ ...any) {}
func (*panicLogger) Log(_ ...any) {}
func (*panicLogger) Logf(_ string, _ ...any) {}
func (*panicLogger) ChangeLevel(_ logging.Level) {}
type NoopLogger struct{}
func (*NoopLogger) Fatalf(format string, args ...any) {
panic(fmt.Sprintf(format, args...))
}
func (*NoopLogger) Fatal(args ...any) {
panic(fmt.Sprint(args...))
}
func (*NoopLogger) Errorf(_ string, _ ...any) {}
func (*NoopLogger) Error(_ ...any) {}
func (*NoopLogger) Debugf(_ string, _ ...any) {}
func (*NoopLogger) Noticef(_ string, _ ...any) {}
func (*NoopLogger) Debug(_ ...any) {}
func (*NoopLogger) Infof(_ string, _ ...any) {}
func (*NoopLogger) Info(_ ...any) {}
func (*NoopLogger) Notice(_ ...any) {}
func (*NoopLogger) Warn(_ ...any) {}
func (*NoopLogger) Warnf(_ string, _ ...any) {}
func (*NoopLogger) Log(_ ...any) {}
func (*NoopLogger) Logf(_ string, _ ...any) {}
func (*NoopLogger) ChangeLevel(_ logging.Level) {}
func scyllaSetup(t *testing.T) (migrator, *container.MockScyllaDB, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockScylla := mocks.ScyllaDB
mockContainer.Logger = &NoopLogger{}
ds := Datasource{ScyllaDB: mockContainer.ScyllaDB}
scylla := scyllaDS{ScyllaDB: mockContainer.ScyllaDB}
migratorWithScylla := scylla.apply(&ds)
return migratorWithScylla, mockScylla, mockContainer
}
func TestScyllaCheckAndCreateMigrationTable(t *testing.T) {
migratorWithScylla, mockScylla, mockContainer := scyllaSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"create table failed", errScyllaConn},
}
for i, tc := range testCases {
mockScylla.EXPECT().
Exec(gomock.Any()).
Return(tc.err)
err := migratorWithScylla.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v] %s failed", i, tc.desc)
}
}
func TestScyllaGetLastMigration(t *testing.T) {
migratorWithScylla, mockScylla, mockContainer := scyllaSetup(t)
testCases := []struct {
desc string
err error
versions []int64
expectedV int64
}{
{"no error with multiple versions", nil, []int64{1, 3, 9, 4}, 9},
{"query failed", errScyllaConn, nil, -1},
{"empty result", nil, []int64{}, 0},
}
for i, tc := range testCases {
var rows []migrationRow
for _, v := range tc.versions {
rows = append(rows, migrationRow{Version: v})
}
mockScylla.EXPECT().
Query(gomock.Any(), gomock.Any()).
DoAndReturn(func(dest any, _ string, _ ...any) error {
if tc.err != nil {
return tc.err
}
ptr := dest.(*[]migrationRow)
*ptr = rows
return nil
})
got, err := migratorWithScylla.getLastMigration(mockContainer)
assert.Equal(t, tc.expectedV, got, "TEST[%v] %s failed", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v] %s failed", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v] %s failed", i, tc.desc)
}
}
}
func TestScyllaCommitMigration(t *testing.T) {
migratorWithScylla, mockScylla, mockContainer := scyllaSetup(t)
testCases := []struct {
desc string
err error
}{
{"successful insert", nil},
{"insert fails", errScyllaConn},
}
for i, tc := range testCases {
td := transactionData{
MigrationNumber: 123,
StartTime: time.Now(),
}
mockScylla.EXPECT().
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(tc.err)
err := migratorWithScylla.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v] %s failed", i, tc.desc)
}
}
func TestScyllaBeginTransaction(t *testing.T) {
migratorWithScylla, _, mockContainer := scyllaSetup(t)
data := migratorWithScylla.beginTransaction(mockContainer)
assert.NotNil(t, data)
}
func TestScyllaMigrator_Rollback(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer := &container.Container{
Logger: &panicLogger{},
}
mockMigrator := NewMockmigrator(ctrl)
s := scyllaMigrator{
ScyllaDB: nil,
migrator: mockMigrator,
}
data := transactionData{MigrationNumber: 123}
mockMigrator.EXPECT().rollback(mockContainer, data).Times(1)
defer func() {
if r := recover(); r == nil {
t.Errorf("Expected panic from Fatalf, but none occurred")
}
}()
s.rollback(mockContainer, data)
}
================================================
FILE: pkg/gofr/migration/sql.go
================================================
package migration
import (
"context"
"errors"
"fmt"
"strings"
"time"
"gofr.dev/pkg/gofr/container"
gofrSql "gofr.dev/pkg/gofr/datasource/sql"
)
var errSQLLockRefreshFailed = errors.New("failed to refresh SQL lock: lock lost or stolen")
const (
createSQLGoFrMigrationsTable = `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createSQLGoFrMigrationLocksTable = `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
getLastSQLGoFrMigration = `SELECT COALESCE(MAX(version), 0) FROM gofr_migrations;`
insertGoFrMigrationRowMySQL = `INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES (?, ?, ?, ?);`
insertGoFrMigrationRowPostgres = `INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES ($1, $2, $3, $4);`
deleteExpiredLocksMySQL = "DELETE FROM gofr_migration_locks WHERE expires_at < ?"
deleteExpiredLocksPostgres = "DELETE FROM gofr_migration_locks WHERE expires_at < $1"
insertLockMySQL = "INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)"
insertLockPostgres = "INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES ($1, $2, $3)"
updateLockMySQL = "UPDATE gofr_migration_locks SET expires_at = ? WHERE lock_key = ? AND owner_id = ?"
updateLockPostgres = "UPDATE gofr_migration_locks SET expires_at = $1 WHERE lock_key = $2 AND owner_id = $3"
deleteLockMySQL = "DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?"
deleteLockPostgres = "DELETE FROM gofr_migration_locks WHERE lock_key = $1 AND owner_id = $2"
mysql = "mysql"
postgres = "postgres"
sqlite = "sqlite"
)
// database/sql is the package imported so named it sqlDS.
type sqlDS struct {
SQL
}
func (s *sqlDS) apply(m migrator) migrator {
return sqlMigrator{
SQL: s.SQL,
migrator: m,
}
}
type sqlMigrator struct {
SQL
migrator
}
func (d sqlMigrator) checkAndCreateMigrationTable(c *container.Container) error {
if _, err := c.SQL.Exec(createSQLGoFrMigrationsTable); err != nil {
return err
}
if _, err := c.SQL.Exec(createSQLGoFrMigrationLocksTable); err != nil {
return err
}
return d.migrator.checkAndCreateMigrationTable(c)
}
func (d sqlMigrator) getLastMigration(c *container.Container) (int64, error) {
var lastMigration int64
err := c.SQL.QueryRowContext(context.Background(), getLastSQLGoFrMigration).Scan(&lastMigration)
if err != nil {
return -1, fmt.Errorf("sql: %w", err)
}
c.Debugf("SQL last migration fetched value is: %v", lastMigration)
lm2, err := d.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (d sqlMigrator) commitMigration(c *container.Container, data transactionData) error {
dialect := c.SQL.Dialect()
switch dialect {
case mysql, sqlite:
err := insertMigrationRecord(data.SQLTx, insertGoFrMigrationRowMySQL, data.MigrationNumber, data.StartTime)
if err != nil {
return err
}
c.Debugf("inserted record for migration %v in gofr_migrations table", data.MigrationNumber)
case postgres:
err := insertMigrationRecord(data.SQLTx, insertGoFrMigrationRowPostgres, data.MigrationNumber, data.StartTime)
if err != nil {
return err
}
c.Debugf("inserted record for migration %v in gofr_migrations table", data.MigrationNumber)
}
// Commit transaction
if err := data.SQLTx.Commit(); err != nil {
return err
}
return d.migrator.commitMigration(c, data)
}
func insertMigrationRecord(tx *gofrSql.Tx, query string, version int64, startTime time.Time) error {
_, err := tx.Exec(query, version, "UP", startTime, time.Since(startTime).Milliseconds())
return err
}
func (d sqlMigrator) beginTransaction(c *container.Container) transactionData {
sqlTx, err := c.SQL.Begin()
if err != nil {
c.Errorf("unable to begin transaction: %v", err)
return transactionData{}
}
cmt := d.migrator.beginTransaction(c)
cmt.SQLTx = sqlTx
c.Debug("SQL Transaction begin successful")
return cmt
}
func (d sqlMigrator) rollback(c *container.Container, data transactionData) {
if data.SQLTx == nil {
return
}
if err := data.SQLTx.Rollback(); err != nil {
c.Error("unable to rollback transaction: %v", err)
}
d.migrator.rollback(c, data)
c.Fatalf("Migration %v failed and rolled back", data.MigrationNumber)
}
func (d sqlMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
dialect := c.SQL.Dialect()
var cleanupQuery, insertQuery string
if dialect == postgres {
cleanupQuery = deleteExpiredLocksPostgres
insertQuery = insertLockPostgres
} else {
cleanupQuery = deleteExpiredLocksMySQL
insertQuery = insertLockMySQL
}
for i := 0; ; i++ {
_, err := c.SQL.ExecContext(ctx, cleanupQuery, time.Now().UTC())
if err != nil {
c.Errorf("failed to clean up expired locks: %v", err)
}
expiresAt := time.Now().UTC().Add(defaultLockTTL)
_, err = c.SQL.ExecContext(ctx, insertQuery, lockKey, ownerID, expiresAt)
if err == nil {
c.Debug("SQL lock acquired successfully")
go d.startRefresh(ctx, cancel, c, ownerID, dialect)
return d.migrator.lock(ctx, cancel, c, ownerID)
}
if !isDuplicateKeyError(err) {
c.Errorf("error while acquiring sql lock: %v", err)
return errLockAcquisitionFailed
}
c.Debugf("SQL lock already held, retrying in %v... (attempt %d)", defaultRetry, i+1)
select {
case <-time.After(defaultRetry):
case <-ctx.Done():
return ctx.Err()
}
}
}
func isDuplicateKeyError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "duplicate") ||
strings.Contains(msg, "unique constraint") ||
strings.Contains(msg, "integrity constraint") ||
strings.Contains(msg, "primary key constraint") ||
strings.Contains(msg, "constraint failed") // SQLite often returns "UNIQUE constraint failed" or "PRIMARY KEY constraint failed"
}
func (sqlMigrator) startRefresh(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID, dialect string) {
ticker := time.NewTicker(defaultRefresh)
defer ticker.Stop()
var updateQuery string
if dialect == postgres {
updateQuery = updateLockPostgres
} else {
updateQuery = updateLockMySQL
}
for {
select {
case <-ticker.C:
expiresAt := time.Now().UTC().Add(defaultLockTTL)
res, err := c.SQL.Exec(updateQuery, expiresAt, lockKey, ownerID)
if err != nil {
c.Errorf("failed to refresh SQL lock: %v", err)
cancel()
return
}
rows, err := res.RowsAffected()
if err != nil {
c.Errorf("failed to check rows affected for SQL lock: %v", err)
cancel()
return
}
if rows == 0 {
c.Errorf("%v", errSQLLockRefreshFailed)
cancel()
return
}
c.Debug("SQL lock refreshed successfully")
case <-ctx.Done():
return
}
}
}
func (d sqlMigrator) unlock(c *container.Container, ownerID string) error {
dialect := c.SQL.Dialect()
var deleteQuery string
if dialect == postgres {
deleteQuery = deleteLockPostgres
} else {
deleteQuery = deleteLockMySQL
}
result, err := c.SQL.Exec(deleteQuery, lockKey, ownerID)
if err != nil {
c.Errorf("unable to release SQL lock: %v", err)
return errLockReleaseFailed
}
// Check if we actually deleted the lock (i.e., we still owned it)
rowsAffected, err := result.RowsAffected()
if err != nil {
c.Errorf("unable to check SQL lock release status: %v", err)
return errLockReleaseFailed
}
if rowsAffected == 0 {
c.Errorf("failed to release SQL lock: lock was already released or stolen")
return errLockReleaseFailed
}
c.Debug("SQL lock released successfully")
return d.migrator.unlock(c, ownerID)
}
func (sqlMigrator) name() string {
return "SQL"
}
================================================
FILE: pkg/gofr/migration/sql_test.go
================================================
package migration
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
)
var (
errCreateTable = errors.New("create table error")
errDuplicateKey = errors.New("duplicate key")
errDB = errors.New("db error")
errUpdateFailed = errors.New("update failed")
errSQLExec = errors.New("exec error")
errSQLCommit = errors.New("commit error")
)
func TestQuery(t *testing.T) {
t.Run("successful query", func(t *testing.T) {
var id int
var name string
expectedResult := []struct {
id int
name string
}{
{1, "Alex"},
{2, "John"},
}
mockContainer, mocks := container.NewMockContainer(t)
expectedRows := mocks.SQL.NewRows([]string{"id", "name"}).
AddRow(expectedResult[0].id, expectedResult[0].name).
AddRow(expectedResult[1].id, expectedResult[1].name)
mocks.SQL.ExpectQuery("SELECT * FROM users").WithoutArgs().WillReturnRows(expectedRows)
rows, err := mockContainer.SQL.Query("SELECT * FROM users")
require.NoError(t, err, "TestQuery : error executing mock query")
i := 0
for rows.Next() {
require.NoError(t, rows.Err(), "TestQuery: row error")
err = rows.Scan(&id, &name)
require.NoError(t, err, "TestQuery: row scan error")
require.Equal(t, expectedResult[i].id, id, "TestQuery: resultant ID & expected ID are not same")
require.Equal(t, expectedResult[i].name, name, "TestQuery: resultant name & expected name are not same")
i++
}
})
t.Run("query error", func(t *testing.T) {
var id int
var name string
mockContainer, mocks := container.NewMockContainer(t)
expectedErr := sql.ErrNoRows
expectedRows := mocks.SQL.NewRows([]string{"id", "name"})
mocks.SQL.ExpectQuery("SELECT * FROM unknown_table").WithoutArgs().WillReturnRows(expectedRows)
sqlMockDB := mockContainer.SQL
rows, err := sqlMockDB.Query("SELECT * FROM unknown_table")
require.NoError(t, err, "TestQuery : error executing mock query")
for rows.Next() {
require.NoError(t, rows.Err(), "TestQuery: row error")
err = rows.Scan(&id, &name)
require.Error(t, err, "TestQuery: row scan error")
require.Equal(t, expectedErr, err, "TestQuery: expected error is not equal to resultant error")
}
})
}
func TestQueryRow(t *testing.T) {
t.Run("successful query row", func(t *testing.T) {
var name string
var id int
mockContainer, mocks := container.NewMockContainer(t)
expectedRows := mocks.SQL.NewRows([]string{"id", "name"}).AddRow(1, "Alex")
mocks.SQL.ExpectQuery("SELECT * FROM users WHERE id = ?").WithArgs(1).WillReturnRows(expectedRows)
sqlMockDB := mockContainer.SQL
err := sqlMockDB.QueryRow("SELECT * FROM users WHERE id = ?", 1).Scan(&id, &name)
require.NoError(t, err, "TestQueryRow: row scan error")
require.Equal(t, 1, id, "TestQueryRow: expected id to be equal to 1")
require.Equal(t, "Alex", name, "TestQueryRow: expected name to be equal to 'Alex'")
})
}
func TestQueryRowContext(t *testing.T) {
ctx := t.Context()
t.Run("successful query row context", func(t *testing.T) {
var id int
var name string
mockContainer, mocks := container.NewMockContainer(t)
expectedRows := mocks.SQL.NewRows([]string{"id", "name"}).AddRow(1, "Alex")
mocks.SQL.ExpectQuery("SELECT * FROM users WHERE id = ?").WithArgs(1).WillReturnRows(expectedRows)
sqlMockDB := mockContainer.SQL
err := sqlMockDB.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", 1).Scan(&id, &name)
require.NoError(t, err, "TestQueryRowContext: Error while scanning row")
require.Equal(t, 1, id, "TestQueryRowContext: expected id to be equal to 1")
require.Equal(t, "Alex", name, "TestQueryRowContext: expected name to be equal to 'Alex'")
})
}
func TestExec(t *testing.T) {
t.Run("successful exec", func(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
expectedResult := mocks.SQL.NewResult(10, 1)
mocks.SQL.ExpectExec("DELETE FROM users WHERE id = ?").WithArgs(1).WillReturnResult(expectedResult)
sqlDB := mockContainer.SQL
result, err := sqlDB.Exec("DELETE FROM users WHERE id = ?", 1)
require.NoError(t, err, "TestExec: error while executing mock query")
expectedLastInserted, err := expectedResult.LastInsertId()
require.NoError(t, err, "TestExec: error while retrieving last inserted id from expected sqlresult")
resultLastInserted, err := result.LastInsertId()
require.NoError(t, err, "TestExec: error while retrieving last inserted id from mock sqlresult")
expectedRowsAffected, err := expectedResult.RowsAffected()
require.NoError(t, err, "TestExec: error while retrieving rows affected from expected sqlresult")
resultRowsAffected, err := result.RowsAffected()
require.NoError(t, err, "TestExec: error while retrieving rows affected from mock sqlresult")
require.Equal(t, expectedLastInserted, resultLastInserted, "TestExec: expected last inserted id to be equal to 10")
require.Equal(t, expectedRowsAffected, resultRowsAffected, "TestExec: expected rows affected to be equal to 1")
})
t.Run("exec error", func(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
expectedErr := sql.ErrNoRows
mocks.SQL.ExpectExec("UPDATE unknown_table SET name = ?").WithArgs("John").WillReturnError(expectedErr)
sqlMockDB := mockContainer.SQL
_, err := sqlMockDB.Exec("UPDATE unknown_table SET name = ?", "John")
require.Error(t, err, "TestExec: expected error while executing mock query")
require.Equal(t, expectedErr, err, "TestExec: Exec should return the expected error, got: %v", err)
})
}
func TestExecContext(t *testing.T) {
ctx := t.Context()
t.Run("successful exec context", func(t *testing.T) {
mockContainer, mocks := container.NewMockContainer(t)
expectedResult := mocks.SQL.NewResult(10, 1)
mocks.SQL.ExpectExec("DELETE FROM users WHERE id = ?").WithArgs(1).WillReturnResult(expectedResult)
sqlMockDB := mockContainer.SQL
result, err := sqlMockDB.ExecContext(ctx, "DELETE FROM users WHERE id = ?", 1)
require.NoError(t, err, "TestExecContext: error while executing mock query")
expectedLastInserted, err := expectedResult.LastInsertId()
require.NoError(t, err, "TestExecContext: error while retrieving last inserted id from expected sqlresult")
resultLastInserted, err := result.LastInsertId()
require.NoError(t, err, "TestExecContext: error while retrieving last inserted id from mock sqlresult")
expectedRowsAffected, err := expectedResult.RowsAffected()
require.NoError(t, err, "TestExecContext: error while retrieving rows affected from expected sqlresult")
resultRowsAffected, err := result.RowsAffected()
require.NoError(t, err, "TestExecContext: error while retrieving rows affected from mock sqlresult")
require.Equal(t, expectedLastInserted, resultLastInserted, "TestExecContext: expected last inserted id to be equal to 10")
require.Equal(t, expectedRowsAffected, resultRowsAffected, "TestExecContext: expected rows affected to be equal to 1")
})
}
func TestCheckAndCreateMigrationTableSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator.EXPECT().checkAndCreateMigrationTable(mockContainer)
mocks.SQL.ExpectExec(createSQLGoFrMigrationsTable).WillReturnResult(mocks.SQL.NewResult(1, 1))
mocks.SQL.ExpectExec(createSQLGoFrMigrationLocksTable).WillReturnResult(mocks.SQL.NewResult(1, 1))
migrator := sqlMigrator{
SQL: mockContainer.SQL,
migrator: mockMigrator,
}
err := migrator.checkAndCreateMigrationTable(mockContainer)
require.NoError(t, err, "TestCheckAndCreateMigrationTable: error while executing mock query")
}
func TestCheckAndCreateMigrationTableExecError(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
expectedErr := sql.ErrNoRows
mocks.SQL.ExpectExec(createSQLGoFrMigrationsTable).WillReturnError(expectedErr)
migrator := sqlMigrator{
SQL: mockContainer.SQL,
migrator: mockMigrator,
}
err := migrator.checkAndCreateMigrationTable(mockContainer)
require.Error(t, err, "TestCheckAndCreateMigrationTable: expected an error while executing mock query")
require.Equal(t, expectedErr, err, "TestCheckAndCreateMigrationTable: resultant error is not eual to expected error")
}
func TestBeginTransactionSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectBegin()
mockMigrator.EXPECT().beginTransaction(mockContainer)
migrator := sqlMigrator{
SQL: mockContainer.SQL,
migrator: mockMigrator,
}
data := migrator.beginTransaction(mockContainer)
require.NotNil(t, data.SQLTx.Tx, "TestBeginTransaction: SQLTX.tx should not be nil")
}
var (
errBeginTx = errors.New("failed to begin transaction")
)
func TestBeginTransactionDBError(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectBegin().WillReturnError(errBeginTx)
migrator := sqlMigrator{
SQL: mockContainer.SQL,
migrator: mockMigrator,
}
data := migrator.beginTransaction(mockContainer)
require.Nil(t, data.SQLTx, "TestBeginTransaction: beginTransaction should not return a transaction on DB error")
}
func TestRollbackNoTransaction(t *testing.T) {
mockContainer, _ := container.NewMockContainer(t)
migrator := sqlMigrator{}
migrator.rollback(mockContainer, transactionData{})
}
func TestApply(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, _ := container.NewMockContainer(t)
ds := &sqlDS{SQL: mockContainer.SQL}
result := ds.apply(mockMigrator)
sqlMig, ok := result.(sqlMigrator)
require.True(t, ok, "Result should be an sqlMigrator")
require.Equal(t, mockContainer.SQL, sqlMig.SQL, "SQL field should match")
require.Equal(t, mockMigrator, sqlMig.migrator, "Migrator field should match")
}
func TestGetLastMigration_UseMigratorFallback(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectQuery(getLastSQLGoFrMigration).
WillReturnRows(mocks.SQL.NewRows([]string{"version"}).AddRow(2))
mockMigrator.EXPECT().getLastMigration(mockContainer).Return(int64(5), nil)
migrator := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
last, err := migrator.getLastMigration(mockContainer)
require.NoError(t, err)
require.Equal(t, int64(5), last, "Expected getLastMigration to return higher value from embedded migrator")
}
func TestGetLastMigration_MigratorReturnsLesser(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectQuery(getLastSQLGoFrMigration).
WillReturnRows(mocks.SQL.NewRows([]string{"version"}).AddRow(7))
mockMigrator.EXPECT().getLastMigration(mockContainer).Return(int64(5), nil)
migrator := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
last, err := migrator.getLastMigration(mockContainer)
require.NoError(t, err)
require.Equal(t, int64(7), last, "Should return SQL migration value as it's higher")
}
func TestBeginTransaction_ReplaceSQLTx(t *testing.T) {
ctrl := gomock.NewController(t)
mockMigrator := NewMockmigrator(ctrl)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectBegin() // this returns a usable SQLTx
mockMigrator.EXPECT().beginTransaction(mockContainer).Return(transactionData{
MigrationNumber: 123,
})
migrator := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
data := migrator.beginTransaction(mockContainer)
require.NotNil(t, data.SQLTx, "SQLTx should not be nil")
require.Equal(t, int64(123), data.MigrationNumber, "Expected migration number from embedded migrator")
}
func TestCheckAndCreateMigrationTable_ErrorCreatingTable(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mocks.SQL.ExpectExec(createSQLGoFrMigrationsTable).WillReturnError(errCreateTable)
m := sqlMigrator{}
err := m.checkAndCreateMigrationTable(mockContainer)
require.Error(t, err)
assert.Contains(t, err.Error(), "create table error")
}
func TestSQLMigrator_Lock(t *testing.T) {
t.Run("LockSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs(lockKey, "1", sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(1, 1))
mockMigrator.EXPECT().lock(ctx, gomock.Any(), mockContainer, "1").Return(nil)
err := m.lock(ctx, cancel, mockContainer, "1")
require.NoError(t, err)
})
}
func TestSQLMigrator_Unlock(t *testing.T) {
t.Run("UnlockSuccess", func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?").
WithArgs(lockKey, "1").
WillReturnResult(mocks.SQL.NewResult(0, 1))
mockMigrator.EXPECT().unlock(mockContainer, "1").Return(nil)
err := m.unlock(mockContainer, "1")
require.NoError(t, err)
})
}
func TestSQLMigrator_LockRetrySuccess(t *testing.T) {
ctrl := gomock.NewController(t)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mocks.SQL.ExpectDialect().WillReturnString("mysql")
// First attempt: cleanup succeeds, but insert fails (lock held)
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs(lockKey, "1", sqlmock.AnyArg()).
WillReturnError(errDuplicateKey)
// Second attempt succeeds
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs(lockKey, "1", sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(1, 1))
mockMigrator.EXPECT().lock(ctx, gomock.Any(), mockContainer, "1").Return(nil)
err := m.lock(ctx, cancel, mockContainer, "1")
require.NoError(t, err)
}
func TestSQLMigrator_LockAcquireError(t *testing.T) {
ctrl := gomock.NewController(t)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE expires_at < ?").
WithArgs(sqlmock.AnyArg()).
WillReturnResult(mocks.SQL.NewResult(0, 0))
mocks.SQL.ExpectExec("INSERT INTO gofr_migration_locks (lock_key, owner_id, expires_at) VALUES (?, ?, ?)").
WithArgs(lockKey, "1", sqlmock.AnyArg()).
WillReturnError(errDB)
err := m.lock(ctx, cancel, mockContainer, "1")
require.Error(t, err)
assert.Equal(t, errLockAcquisitionFailed, err)
}
func TestSQLMigrator_StartRefreshSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
mockContainer, mocks := container.NewMockContainer(t)
m := sqlMigrator{
SQL: mockContainer.SQL,
migrator: NewMockmigrator(ctrl),
}
ctx, cancel := context.WithCancel(t.Context())
// Expect at least one refresh within the defaultRefresh interval
mocks.SQL.ExpectExec("UPDATE gofr_migration_locks SET expires_at = ? WHERE lock_key = ? AND owner_id = ?").
WithArgs(sqlmock.AnyArg(), lockKey, "1").
WillReturnResult(mocks.SQL.NewResult(0, 1))
go m.startRefresh(ctx, cancel, mockContainer, "1", "mysql")
// Wait for at least one refresh cycle
time.Sleep(defaultRefresh + 100*time.Millisecond)
cancel()
// Give goroutine time to exit
time.Sleep(50 * time.Millisecond)
select {
case <-ctx.Done():
if !errors.Is(ctx.Err(), context.Canceled) {
t.Errorf("Unexpected context error: %v", ctx.Err())
}
default:
t.Error("Expected context to be done")
}
// Verify all expectations were met (at least one refresh happened)
if err := mocks.SQL.ExpectationsWereMet(); err != nil {
t.Errorf("unfulfilled expectations: %s", err)
}
}
func TestSQLMigrator_StartRefreshError(t *testing.T) {
ctrl := gomock.NewController(t)
mockContainer, mocks := container.NewMockContainer(t)
m := sqlMigrator{
SQL: mockContainer.SQL,
migrator: NewMockmigrator(ctrl),
}
ctx, cancel := context.WithCancel(t.Context())
mocks.SQL.ExpectExec("UPDATE gofr_migration_locks SET expires_at = ? WHERE lock_key = ? AND owner_id = ?").
WithArgs(sqlmock.AnyArg(), lockKey, "1").
WillReturnError(errUpdateFailed)
go m.startRefresh(ctx, cancel, mockContainer, "1", "mysql")
select {
case <-ctx.Done():
require.Error(t, ctx.Err())
case <-time.After(defaultRefresh * 2):
t.Error("Expected context to be canceled, but timed out")
}
}
func TestSQLMigrator_CommitMigration(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectBegin()
tx, _ := mockContainer.SQL.Begin()
data := transactionData{
SQLTx: tx,
MigrationNumber: 1,
StartTime: time.Now().UTC(),
}
mocks.SQL.ExpectExec("INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES (?, ?, ?, ?);").
WithArgs(int64(1), "UP", data.StartTime, sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
mocks.SQL.ExpectCommit()
mockMigrator.EXPECT().commitMigration(mockContainer, data).Return(nil)
err := m.commitMigration(mockContainer, data)
assert.NoError(t, err)
}
func TestSQLMigrator_CommitMigration_Postgres(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
mocks.SQL.ExpectBegin()
tx, _ := mockContainer.SQL.Begin()
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now().UTC(),
SQLTx: tx,
}
mocks.SQL.ExpectDialect().WillReturnString("postgres")
mocks.SQL.ExpectExec("INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES ($1, $2, $3, $4);").
WithArgs(int64(1), "UP", data.StartTime, sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1))
mocks.SQL.ExpectCommit()
mockMigrator.EXPECT().commitMigration(mockContainer, data).Return(nil)
err := m.commitMigration(mockContainer, data)
assert.NoError(t, err)
}
func TestSQLMigrator_CommitMigration_ExecError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
mocks.SQL.ExpectBegin()
tx, _ := mockContainer.SQL.Begin()
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now().UTC(),
SQLTx: tx,
}
testErr := errSQLExec
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES (?, ?, ?, ?);").
WillReturnError(testErr)
err := m.commitMigration(mockContainer, data)
assert.Equal(t, testErr, err)
}
func TestSQLMigrator_CommitMigration_CommitError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
mocks.SQL.ExpectBegin()
tx, _ := mockContainer.SQL.Begin()
data := transactionData{
MigrationNumber: 1,
StartTime: time.Now().UTC(),
SQLTx: tx,
}
testErr := errSQLCommit
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("INSERT INTO gofr_migrations (version, method, start_time,duration) VALUES (?, ?, ?, ?);").
WillReturnResult(sqlmock.NewResult(1, 1))
mocks.SQL.ExpectCommit().WillReturnError(testErr)
err := m.commitMigration(mockContainer, data)
assert.Equal(t, testErr, err)
}
func TestSQLMigrator_RollbackSuccess(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
// Set mock logger to avoid os.Exit(1)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
mocks.SQL.ExpectBegin()
tx, _ := mockContainer.SQL.Begin()
data := transactionData{
SQLTx: tx,
}
mocks.SQL.ExpectRollback()
mockMigrator.EXPECT().rollback(mockContainer, data)
// Fatalf is expected on rollback
mockLogger.EXPECT().Fatalf(gomock.Any(), gomock.Any())
assert.NotPanics(t, func() {
m.rollback(mockContainer, data)
})
}
func TestSQLMigrator_UnlockError(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
mockLogger := container.NewMockLogger(ctrl)
mockContainer.Logger = mockLogger
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
testErr := errDB
mocks.SQL.ExpectDialect().WillReturnString("mysql")
mocks.SQL.ExpectExec("DELETE FROM gofr_migration_locks WHERE lock_key = ? AND owner_id = ?").
WithArgs("gofr_migrations_lock", "owner-1").WillReturnError(testErr)
mockLogger.EXPECT().Errorf(gomock.Any(), gomock.Any()).Times(1)
err := m.unlock(mockContainer, "owner-1")
assert.Equal(t, errLockReleaseFailed, err)
}
func TestSQLMigrator_CheckAndCreateMigrationTable_Error(t *testing.T) {
ctrl := gomock.NewController(t)
t.Cleanup(ctrl.Finish)
mockContainer, mocks := container.NewMockContainer(t)
mockMigrator := NewMockmigrator(ctrl)
m := sqlMigrator{SQL: mockContainer.SQL, migrator: mockMigrator}
createMigrations := `CREATE TABLE IF NOT EXISTS gofr_migrations (
version BIGINT not null ,
method VARCHAR(4) not null ,
start_time TIMESTAMP not null ,
duration BIGINT,
constraint primary_key primary key (version, method)
);`
createLocks := `CREATE TABLE IF NOT EXISTS gofr_migration_locks (
lock_key VARCHAR(64) PRIMARY KEY,
owner_id VARCHAR(64) NOT NULL,
expires_at TIMESTAMP NOT NULL
);`
testErr := errCreateTable
mocks.SQL.ExpectExec(createMigrations).WillReturnError(testErr)
err := m.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, testErr, err)
mocks.SQL.ExpectExec(createMigrations).WillReturnResult(sqlmock.NewResult(0, 0))
mocks.SQL.ExpectExec(createLocks).WillReturnError(testErr)
err = m.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, testErr, err)
}
func TestSQLMigrator_Name(t *testing.T) {
m := sqlMigrator{}
assert.Equal(t, "SQL", m.name())
}
================================================
FILE: pkg/gofr/migration/surreal_db.go
================================================
package migration
import (
"context"
"errors"
"fmt"
"time"
"gofr.dev/pkg/gofr/container"
)
var errExecuteQuery = errors.New("failed to execute migration query")
type surrealDS struct {
client SurrealDB
}
func (s surrealDS) Query(ctx context.Context, query string, vars map[string]any) ([]any, error) {
return s.client.Query(ctx, query, vars)
}
func (s surrealDS) CreateNamespace(ctx context.Context, namespace string) error {
return s.client.CreateNamespace(ctx, namespace)
}
func (s surrealDS) CreateDatabase(ctx context.Context, database string) error {
return s.client.CreateDatabase(ctx, database)
}
func (s surrealDS) DropNamespace(ctx context.Context, namespace string) error {
return s.client.DropNamespace(ctx, namespace)
}
func (s surrealDS) DropDatabase(ctx context.Context, database string) error {
return s.client.DropDatabase(ctx, database)
}
type surrealMigrator struct {
SurrealDB
migrator
}
func (s surrealDS) apply(m migrator) migrator {
return surrealMigrator{
SurrealDB: s.client,
migrator: m,
}
}
const (
getLastSurrealDBGoFrMigration = `SELECT version FROM gofr_migrations ORDER BY version DESC LIMIT 1;`
insertSurrealDBGoFrMigrationRow = `CREATE gofr_migrations SET version = $version, method = $method, ` +
`start_time = $start_time, duration = $duration;`
)
func getMigrationTableQueries() []string {
return []string{
"DEFINE TABLE gofr_migrations SCHEMAFULL;",
"DEFINE FIELD id ON gofr_migrations TYPE string;",
"DEFINE FIELD version ON gofr_migrations TYPE number;",
"DEFINE FIELD method ON gofr_migrations TYPE string;",
"DEFINE FIELD start_time ON gofr_migrations TYPE datetime;",
"DEFINE FIELD duration ON gofr_migrations TYPE number;",
"DEFINE INDEX version_method ON gofr_migrations COLUMNS version, method UNIQUE;",
}
}
func (s surrealMigrator) checkAndCreateMigrationTable(c *container.Container) error {
if _, err := s.SurrealDB.Query(context.Background(), "USE NS test DB test", nil); err != nil {
return err
}
// Create migration table directly
for _, q := range getMigrationTableQueries() {
if _, err := s.SurrealDB.Query(context.Background(), q, nil); err != nil {
return fmt.Errorf("%w: %s: %w", errExecuteQuery, q, err)
}
}
return s.migrator.checkAndCreateMigrationTable(c)
}
func (s surrealMigrator) getLastMigration(c *container.Container) (int64, error) {
var lastMigration int64
result, err := s.SurrealDB.Query(context.Background(), getLastSurrealDBGoFrMigration, nil)
if err != nil {
return -1, fmt.Errorf("surrealdb: %w", err)
}
if len(result) > 0 {
if version, ok := result[0].(map[string]any)["version"].(float64); ok {
lastMigration = int64(version)
}
}
c.Debugf("surrealDB last migration fetched value is: %v", lastMigration)
lm2, err := s.migrator.getLastMigration(c)
if err != nil {
return -1, err
}
return max(lastMigration, lm2), nil
}
func (s surrealMigrator) beginTransaction(c *container.Container) transactionData {
data := s.migrator.beginTransaction(c)
c.Debug("surrealDB migrator begin successfully")
return data
}
func (s surrealMigrator) commitMigration(c *container.Container, data transactionData) error {
_, err := s.SurrealDB.Query(context.Background(), insertSurrealDBGoFrMigrationRow, map[string]any{
"version": data.MigrationNumber,
"method": "UP",
"start_time": data.StartTime,
"duration": time.Since(data.StartTime).Milliseconds(),
})
if err != nil {
return err
}
c.Debugf("inserted record for migration %v in surrealDB gofr_migrations table", data.MigrationNumber)
return s.migrator.commitMigration(c, data)
}
func (s surrealMigrator) rollback(c *container.Container, data transactionData) {
s.migrator.rollback(c, data)
c.Fatalf("migration %v failed and rolled back", data.MigrationNumber)
}
func (s surrealMigrator) lock(ctx context.Context, cancel context.CancelFunc, c *container.Container, ownerID string) error {
return s.migrator.lock(ctx, cancel, c, ownerID)
}
func (s surrealMigrator) unlock(c *container.Container, ownerID string) error {
return s.migrator.unlock(c, ownerID)
}
func (surrealMigrator) name() string {
return "SurrealDB"
}
================================================
FILE: pkg/gofr/migration/surreal_db_test.go
================================================
package migration
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/testutil"
)
func surrealSetup(t *testing.T) (migrator, *container.MockSurrealDB, *container.Container) {
t.Helper()
mockContainer, mocks := container.NewMockContainer(t)
mockSurreal := mocks.SurrealDB
ds := Datasource{SurrealDB: mockSurreal}
surrealDB := surrealDS{client: mockSurreal}
migratorWithSurreal := surrealDB.apply(&ds)
mockContainer.SurrealDB = mockSurreal
return migratorWithSurreal, mockSurreal, mockContainer
}
func Test_SurrealCheckAndCreateMigrationTable(t *testing.T) {
migratorWithSurreal, mockSurreal, mockContainer := surrealSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"table already exists", nil},
}
for i, tc := range testCases {
mockSurreal.EXPECT().Query(gomock.Any(), gomock.Any(), nil).Return([]any{}, tc.err).MaxTimes(8)
err := migratorWithSurreal.checkAndCreateMigrationTable(mockContainer)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_SurrealGetLastMigration(t *testing.T) {
migratorWithSurreal, mockSurreal, mockContainer := surrealSetup(t)
testCases := []struct {
desc string
err error
resp int64
}{
{"no error", nil, 1},
{"query failed", context.DeadlineExceeded, -1},
}
for i, tc := range testCases {
mockSurreal.EXPECT().Query(gomock.Any(), getLastSurrealDBGoFrMigration, nil).Return([]any{
map[string]any{"version": float64(tc.resp)},
}, tc.err)
resp, err := migratorWithSurreal.getLastMigration(mockContainer)
assert.Equal(t, tc.resp, resp, "TEST[%v]\n %v Failed! ", i, tc.desc)
if tc.err != nil {
assert.ErrorContains(t, err, tc.err.Error(), "TEST[%v]\n %v Failed! ", i, tc.desc)
} else {
assert.NoError(t, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
}
func Test_SurrealCommitMigration(t *testing.T) {
migratorWithSurreal, mockSurreal, mockContainer := surrealSetup(t)
testCases := []struct {
desc string
err error
}{
{"no error", nil},
{"insert failed", context.DeadlineExceeded},
}
timeNow := time.Now()
td := transactionData{
StartTime: timeNow,
MigrationNumber: 10,
}
for i, tc := range testCases {
bindVars := map[string]any{
"version": td.MigrationNumber,
"method": "UP",
"start_time": td.StartTime,
"duration": time.Since(td.StartTime).Milliseconds(),
}
mockSurreal.EXPECT().Query(gomock.Any(), insertSurrealDBGoFrMigrationRow, bindVars).Return([]any{}, tc.err)
err := migratorWithSurreal.commitMigration(mockContainer, td)
assert.Equal(t, tc.err, err, "TEST[%v]\n %v Failed! ", i, tc.desc)
}
}
func Test_SurrealBeginTransaction(t *testing.T) {
logs := testutil.StdoutOutputForFunc(func() {
migratorWithSurreal, _, mockContainer := surrealSetup(t)
migratorWithSurreal.beginTransaction(mockContainer)
})
assert.Contains(t, logs, "surrealDB migrator begin successfully")
}
func TestSurrealDS_Query(t *testing.T) {
_, mockSurreal, _ := surrealSetup(t)
query := "SELECT * FROM table"
vars := map[string]any{"key": "value"}
expectedResult := []any{"result"}
mockSurreal.EXPECT().Query(t.Context(), query, vars).Return(expectedResult, nil)
surreal := surrealDS{client: mockSurreal}
result, err := surreal.Query(t.Context(), query, vars)
require.NoError(t, err)
assert.Equal(t, expectedResult, result)
}
func TestSurrealDS_CreateNamespace(t *testing.T) {
_, mockSurreal, _ := surrealSetup(t)
namespace := "test_namespace"
mockSurreal.EXPECT().CreateNamespace(t.Context(), namespace).Return(nil)
surreal := surrealDS{client: mockSurreal}
err := surreal.CreateNamespace(t.Context(), namespace)
assert.NoError(t, err)
}
func TestSurrealDS_CreateDatabase(t *testing.T) {
_, mockSurreal, _ := surrealSetup(t)
database := "test_database"
mockSurreal.EXPECT().CreateDatabase(t.Context(), database).Return(nil)
surreal := surrealDS{client: mockSurreal}
err := surreal.CreateDatabase(t.Context(), database)
assert.NoError(t, err)
}
func TestSurrealDS_DropNamespace(t *testing.T) {
_, mockSurreal, _ := surrealSetup(t)
namespace := "test_namespace"
mockSurreal.EXPECT().DropNamespace(t.Context(), namespace).Return(nil)
surreal := surrealDS{client: mockSurreal}
err := surreal.DropNamespace(t.Context(), namespace)
assert.NoError(t, err)
}
func TestSurrealDS_DropDatabase(t *testing.T) {
_, mockSurreal, _ := surrealSetup(t)
database := "test_database"
mockSurreal.EXPECT().DropDatabase(t.Context(), database).Return(nil)
surreal := surrealDS{client: mockSurreal}
err := surreal.DropDatabase(t.Context(), database)
assert.NoError(t, err)
}
================================================
FILE: pkg/gofr/otel.go
================================================
package gofr
import (
"context"
"fmt"
"strconv"
"strings"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/zipkin" //nolint:staticcheck // deprecated but kept for backward compatibility
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"gofr.dev/pkg/gofr/logging"
)
func (a *App) initTracer() {
traceRatio, err := strconv.ParseFloat(a.Config.GetOrDefault("TRACER_RATIO", "1"), 64)
if err != nil {
a.container.Error(err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(a.container.GetAppName()),
)),
sdktrace.WithSampler(sdktrace.ParentBased(sdktrace.TraceIDRatioBased(traceRatio))),
)
otel.SetTracerProvider(tp)
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 {
a.container.Error(err)
}
batcher := sdktrace.NewBatchSpanProcessor(exporter)
tp.RegisterSpanProcessor(batcher)
}
func isValidConfig(logger logging.Logger, name, url, host, port string) bool {
if url == "" && name == "" {
logger.Debug("tracing is disabled, as configs are not provided")
return false
}
if url != "" && name == "" {
logger.Error("missing TRACE_EXPORTER config, should be provided with TRACER_URL to enable tracing")
return false
}
//nolint:revive // early-return is not possible here, as below is the intentional logging flow
if url == "" && name != "" && !strings.EqualFold(name, "gofr") {
if host != "" && port != "" {
logger.Warn("TRACER_HOST and TRACER_PORT are deprecated, use TRACER_URL instead")
} else {
logger.Error("missing TRACER_URL config, should be provided with TRACE_EXPORTER to enable tracing")
return false
}
}
return true
}
// parseHeaders converts comma-separated key=value pairs to headers map.
// Format follows OTEL standard: "Key1=Value1,Key2=Value2".
// Splits only on first '=' to allow '=' in values.
func parseHeaders(headerStr string) map[string]string {
headers := make(map[string]string)
if headerStr == "" {
return headers
}
const keyValueParts = 2
// Split by comma
pairs := strings.Split(headerStr, ",")
for _, pair := range pairs {
pair = strings.TrimSpace(pair)
// Split only on first '=' to allow '=' in values
kv := strings.SplitN(pair, "=", keyValueParts)
if len(kv) == keyValueParts {
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if key != "" && value != "" {
headers[key] = value
}
}
}
return headers
}
// getTracerHeaders returns headers map from TRACER_HEADERS or TRACER_AUTH_KEY config.
func (a *App) getTracerHeaders() map[string]string {
headers := make(map[string]string)
// Check for TRACER_HEADERS first (supports multiple custom headers)
if headerStr := a.Config.Get("TRACER_HEADERS"); headerStr != "" {
headers = parseHeaders(headerStr)
} else if authKey := a.Config.Get("TRACER_AUTH_KEY"); authKey != "" {
headers["Authorization"] = authKey
}
return headers
}
func (a *App) getExporter(name, host, port, url string) (sdktrace.SpanExporter, error) {
var (
exporter sdktrace.SpanExporter
err error
)
headers := a.getTracerHeaders()
switch strings.ToLower(name) {
case "otlp", "jaeger":
exporter, err = buildOtlpExporter(a.Logger(), name, url, host, port, headers)
case "zipkin":
a.Logger().Warn("TRACE_EXPORTER=zipkin is deprecated and will be removed in a future release. " +
"Zipkin supports OTLP natively (v2.24+) — to migrate, switch to TRACE_EXPORTER=otlp " +
"and point TRACER_URL to your Zipkin OTLP gRPC endpoint (default: :4317)")
exporter, err = buildZipkinExporter(a.Logger(), url, host, port, headers)
case gofrTraceExporter:
exporter = buildGoFrExporter(a.Logger(), url)
default:
a.container.Errorf("unsupported TRACE_EXPORTER: %s", name)
}
return exporter, err
}
// buildOpenTelemetryProtocol using OpenTelemetryProtocol as the trace exporter
// jaeger accept OpenTelemetry Protocol (OTLP) over gRPC to upload trace data.
func buildOtlpExporter(logger logging.Logger, name, url, host, port string, headers map[string]string) (sdktrace.SpanExporter, error) {
if url == "" {
url = fmt.Sprintf("%s:%s", host, port)
}
logger.Infof("Exporting traces to %s at %s", strings.ToLower(name), url)
opts := []otlptracegrpc.Option{otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint(url)}
if len(headers) > 0 {
opts = append(opts, otlptracegrpc.WithHeaders(headers))
}
return otlptracegrpc.New(context.Background(), opts...)
}
func buildZipkinExporter(logger logging.Logger, url, host, port string, headers map[string]string) (sdktrace.SpanExporter, error) {
if url == "" {
url = fmt.Sprintf("http://%s:%s/api/v2/spans", host, port)
}
logger.Infof("Exporting traces to zipkin at %s", url)
var opts []zipkin.Option
if len(headers) > 0 {
opts = append(opts, zipkin.WithHeaders(headers))
}
return zipkin.New(url, opts...)
}
func buildGoFrExporter(logger logging.Logger, url string) sdktrace.SpanExporter {
if url == "" {
url = "https://tracer-api.gofr.dev/api/spans"
}
logger.Infof("Exporting traces to GoFr at %s", url)
return NewExporter(url, logging.NewLogger(logging.INFO))
}
type otelErrorHandler struct {
logger logging.Logger
}
func (o *otelErrorHandler) Handle(e error) {
if e == nil {
return
}
msg := e.Error()
// Fast check: if a message contains "status 2", it's a 2xx code.
if strings.Contains(msg, "status 2") {
return
}
o.logger.Error(msg)
}
================================================
FILE: pkg/gofr/otel_test.go
================================================
package gofr
import (
"errors"
"testing"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/config"
"gofr.dev/pkg/gofr/logging"
)
func TestParseHeaders(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
name: "empty string",
input: "",
expected: map[string]string{},
},
{
name: "single header",
input: "Key=Value",
expected: map[string]string{
"Key": "Value",
},
},
{
name: "multiple headers",
input: "K1=V1,K2=V2",
expected: map[string]string{
"K1": "V1",
"K2": "V2",
},
},
{
name: "value with equals sign",
input: "Hash=sha256=abc123,Key=value",
expected: map[string]string{
"Hash": "sha256=abc123",
"Key": "value",
},
},
{
name: "skip invalid entries",
input: "NoEquals,Valid=value,=EmptyKey",
expected: map[string]string{
"Valid": "value",
},
},
{
name: "trim whitespace",
input: " Key1 = Value1 , Key2 = Value2 ",
expected: map[string]string{
"Key1": "Value1",
"Key2": "Value2",
},
},
{
name: "empty key",
input: "=Value,Valid=value",
expected: map[string]string{
"Valid": "value",
},
},
{
name: "empty value",
input: "Key=,Valid=value",
expected: map[string]string{
"Valid": "value",
},
},
{
name: "base64 authorization header",
input: "Authorization=Basic dXNlcjpwYXNz",
expected: map[string]string{
"Authorization": "Basic dXNlcjpwYXNz",
},
},
{
name: "multiple headers with special characters",
input: "X-Api-Key=abc123xyz,Authorization=Bearer token123,X-Scope-OrgID=tenant-1",
expected: map[string]string{
"X-Api-Key": "abc123xyz",
"Authorization": "Bearer token123",
"X-Scope-OrgID": "tenant-1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := parseHeaders(tt.input)
require.Equal(t, tt.expected, result)
})
}
}
func TestApp_getTracerHeaders_WithTracerHeaders(t *testing.T) {
tests := []struct {
name string
tracerHeaders string
expectedHeaders map[string]string
expectedHeaderLen int
}{
{
name: "multiple headers",
tracerHeaders: "X-Api-Key=secret123,Authorization=Bearer token",
expectedHeaderLen: 2,
expectedHeaders: map[string]string{
"X-Api-Key": "secret123",
"Authorization": "Bearer token",
},
},
{
name: "single header",
tracerHeaders: "X-Honeycomb-Team=abc123",
expectedHeaderLen: 1,
expectedHeaders: map[string]string{
"X-Honeycomb-Team": "abc123",
},
},
{
name: "priority over TRACER_AUTH_KEY",
tracerHeaders: "X-Custom-Header=value",
expectedHeaderLen: 1,
expectedHeaders: map[string]string{
"X-Custom-Header": "value",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configData := map[string]string{
"TRACER_HEADERS": tt.tracerHeaders,
}
app := &App{
Config: config.NewMockConfig(configData),
}
headers := app.getTracerHeaders()
require.Equal(t, tt.expectedHeaders, headers)
require.Len(t, headers, tt.expectedHeaderLen)
})
}
}
func TestApp_getTracerHeaders_WithAuthKey(t *testing.T) {
tests := []struct {
name string
tracerAuthKey string
expectedHeaders map[string]string
expectedHeaderLen int
}{
{
name: "backward compatibility",
tracerAuthKey: "Bearer legacy-token",
expectedHeaderLen: 1,
expectedHeaders: map[string]string{
"Authorization": "Bearer legacy-token",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
configData := map[string]string{
"TRACER_AUTH_KEY": tt.tracerAuthKey,
}
app := &App{
Config: config.NewMockConfig(configData),
}
headers := app.getTracerHeaders()
require.Equal(t, tt.expectedHeaders, headers)
require.Len(t, headers, tt.expectedHeaderLen)
})
}
}
func TestApp_getTracerHeaders_NoConfig(t *testing.T) {
app := &App{
Config: config.NewMockConfig(map[string]string{}),
}
headers := app.getTracerHeaders()
require.Empty(t, headers)
}
var (
errOtelStatus200 = errors.New("rpc error: code = Unknown desc = status 200")
errOtelStatus204 = errors.New("rpc error: status 204")
errOtelStatus201 = errors.New("status 201: ok")
errOtelStatus500 = errors.New("rpc error: status 500")
)
type captureLogger struct {
loggedErrors []string
}
func (*captureLogger) Debug(_ ...any) {}
func (*captureLogger) Debugf(_ string, _ ...any) {}
func (*captureLogger) Log(_ ...any) {}
func (*captureLogger) Logf(_ string, _ ...any) {}
func (*captureLogger) Info(_ ...any) {}
func (*captureLogger) Infof(_ string, _ ...any) {}
func (*captureLogger) Notice(_ ...any) {}
func (*captureLogger) Noticef(_ string, _ ...any) {}
func (*captureLogger) Warn(_ ...any) {}
func (*captureLogger) Warnf(_ string, _ ...any) {}
func (l *captureLogger) Error(args ...any) {
// otelErrorHandler passes a single string arg
if len(args) == 1 {
if s, ok := args[0].(string); ok {
l.loggedErrors = append(l.loggedErrors, s)
return
}
}
l.loggedErrors = append(l.loggedErrors, "non-string error")
}
func (*captureLogger) Errorf(_ string, _ ...any) {}
func (*captureLogger) Fatal(_ ...any) {}
func (*captureLogger) Fatalf(_ string, _ ...any) {}
func (*captureLogger) ChangeLevel(_ logging.Level) {}
func TestOtelErrorHandler_Ignores2xxStatusErrors(t *testing.T) {
cl := &captureLogger{}
h := &otelErrorHandler{logger: cl}
h.Handle(errOtelStatus200)
h.Handle(errOtelStatus204)
h.Handle(errOtelStatus201)
require.Empty(t, cl.loggedErrors)
}
func TestOtelErrorHandler_LogsNon2xxErrors(t *testing.T) {
cl := &captureLogger{}
h := &otelErrorHandler{logger: cl}
h.Handle(errOtelStatus500)
require.Len(t, cl.loggedErrors, 1)
require.Equal(t, "rpc error: status 500", cl.loggedErrors[0])
}
func TestOtelErrorHandler_NilErrorNoop(t *testing.T) {
cl := &captureLogger{}
h := &otelErrorHandler{logger: cl}
h.Handle(nil)
require.Empty(t, cl.loggedErrors)
}
================================================
FILE: pkg/gofr/rbac/config.go
================================================
package rbac
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"go.opentelemetry.io/otel/trace"
"gopkg.in/yaml.v3"
"gofr.dev/pkg/gofr/container"
"gofr.dev/pkg/gofr/datasource"
)
var (
// errUnsupportedFormat is returned when the config file format is not supported.
errUnsupportedFormat = errors.New("unsupported config file format")
// ErrEndpointMissingPermissions is returned when an endpoint doesn't specify requiredPermissions and is not public.
ErrEndpointMissingPermissions = errors.New("endpoint must specify requiredPermissions (or be public)")
// errWildcardPatternNotSupported is returned when a wildcard pattern is used.
errWildcardPatternNotSupported = errors.New("wildcard pattern '/*' is not supported, use mux patterns instead")
// errRegexPatternNotSupported is returned when an old regex pattern is used.
errRegexPatternNotSupported = errors.New("regex pattern '^...$' is not supported, use mux patterns instead")
// errRegexIndicatorNotSupported is returned when regex indicators are used outside variable constraints.
errRegexIndicatorNotSupported = errors.New("regex pattern is not supported, use mux patterns instead")
)
// RoleDefinition defines a role with its permissions and inheritance.
// Pure config-based: only role->permission mapping is supported.
type RoleDefinition struct {
// Name is the role name (required)
Name string `json:"name" yaml:"name"`
// Permissions is a list of permissions for this role (format: "resource:action")
// Example: ["users:read", "users:write"]
Permissions []string `json:"permissions,omitempty" yaml:"permissions,omitempty"`
// InheritsFrom lists roles this role inherits permissions from
// Example: ["viewer"] - editor inherits all viewer permissions
InheritsFrom []string `json:"inheritsFrom,omitempty" yaml:"inheritsFrom,omitempty"`
}
// EndpointMapping defines authorization requirements for an API endpoint.
// Pure config-based: only route&method->permission mapping is supported.
// No direct route to role mapping - all authorization is permission-based.
type EndpointMapping struct {
// Path is the route path pattern using gorilla/mux syntax.
// Examples:
// - "/api/users" (exact match)
// - "/api/users/{id}" (matches any single segment)
// - "/api/users/{id:[0-9]+}" (matches numeric IDs only)
// - "/api/{resource}" (single-level wildcard: matches /api/users, /api/posts)
// - "/api/{path:.*}" (multi-level wildcard: matches /api/users/123, /api/posts/comments)
// - "/api/{category}/posts" (middle variable: matches /api/tech/posts, /api/news/posts)
// Only mux-style patterns are supported. Wildcards (/*) and regex (^...$) are not supported.
Path string `json:"path,omitempty" yaml:"path,omitempty"`
// Methods is a list of HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.)
// Use ["*"] to match all methods
// Example: ["GET", "POST"]
Methods []string `json:"methods" yaml:"methods"`
// RequiredPermissions is a list of permissions required to access this endpoint (format: "resource:action")
// User needs to have ANY of these permissions (OR logic)
// Example: ["users:read"] or ["users:read", "users:admin"]
// This is checked against the role's permissions
// REQUIRED: All endpoints must specify requiredPermissions (except public endpoints)
RequiredPermissions []string `json:"requiredPermissions,omitempty" yaml:"requiredPermissions,omitempty"`
// Public indicates this endpoint is publicly accessible (bypasses authorization)
// Example: true for /health, /metrics endpoints
Public bool `json:"public,omitempty" yaml:"public,omitempty"`
}
// Config represents the unified RBAC configuration structure.
type Config struct {
// Roles defines all roles with their permissions and inheritance
// This is the unified way to define roles (replaces RouteWithPermissions, RoleHierarchy)
Roles []RoleDefinition `json:"roles,omitempty" yaml:"roles,omitempty"`
// Endpoints maps API endpoints to authorization requirements
// This is the unified way to define endpoint access (replaces RouteWithPermissions, OverRides)
Endpoints []EndpointMapping `json:"endpoints,omitempty" yaml:"endpoints,omitempty"`
// RoleHeader specifies the HTTP header key for header-based role extraction
// Example: "X-User-Role"
// If set, role is extracted from this header
RoleHeader string `json:"roleHeader,omitempty" yaml:"roleHeader,omitempty"`
// JWTClaimPath specifies the JWT claim path for JWT-based role extraction
// Examples: "role", "roles[0]", "permissions.role"
// If set, role is extracted from JWT claims in request context
JWTClaimPath string `json:"jwtClaimPath,omitempty" yaml:"jwtClaimPath,omitempty"`
// ErrorHandler is called when authorization fails
// If nil, default error response is sent
ErrorHandler func(w http.ResponseWriter, r *http.Request, role, route string, err error)
// Logger is the logger instance for audit logging
// Set automatically by EnableRBAC - users don't need to configure this
// Audit logging is automatically performed when RBAC is enabled
Logger datasource.Logger `json:"-" yaml:"-"`
// Metrics is the metrics instance for RBAC metrics
// Set automatically by EnableRBAC
Metrics container.Metrics `json:"-" yaml:"-"`
// Tracer is the tracer instance for RBAC tracing
// Set automatically by EnableRBAC
Tracer trace.Tracer `json:"-" yaml:"-"`
// Internal maps built from unified config (not in JSON/YAML)
// These are populated by processUnifiedConfig()
rolePermissionsMap map[string][]string `json:"-" yaml:"-"`
endpointPermissionMap map[string][]string `json:"-" yaml:"-"` // Key: "METHOD:/path", Value: []permissions
publicEndpointsMap map[string]bool `json:"-" yaml:"-"` // Key: "METHOD:/path", Value: true if public
endpointMap map[string]*EndpointMapping `json:"-" yaml:"-"` // Key: "METHOD:/path", Value: endpoint object
muxRouter *mux.Router `json:"-" yaml:"-"` // Used for mux pattern matching
}
// LoadPermissions loads RBAC configuration from a JSON or YAML file.
// The file format is automatically detected based on the file extension.
// Supported formats: .json, .yaml, .yml.
// Dependencies (logger, metrics, tracer) are optional and can be set after loading.
func LoadPermissions(path string, logger datasource.Logger, metrics container.Metrics, tracer trace.Tracer) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read RBAC config file %s: %w", path, err)
}
var config Config
// Detect file format by extension
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".yaml", ".yml":
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse YAML config file %s: %w", path, err)
}
case ".json", "":
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse JSON config file %s: %w", path, err)
}
default:
return nil, fmt.Errorf("unsupported config file format: %s (supported: .json, .yaml, .yml): %w", ext, errUnsupportedFormat)
}
// Set dependencies
config.Logger = logger
config.Metrics = metrics
config.Tracer = tracer
// Initialize mux router for pattern matching
// Use StrictSlash(false) to match the application router's behavior
config.muxRouter = mux.NewRouter().StrictSlash(false)
// Validate config before processing
if err := config.validate(); err != nil {
return nil, fmt.Errorf("invalid RBAC config: %w", err)
}
// Process unified config to build internal maps
if err := config.processUnifiedConfig(); err != nil {
return nil, fmt.Errorf("failed to process unified config: %w", err)
}
return &config, nil
}
// validate validates the RBAC configuration.
func (c *Config) validate() error {
// Validate endpoints: non-public endpoints must have RequiredPermissions
// Also validate that paths use mux patterns only (no wildcards or old regex)
for i, endpoint := range c.Endpoints {
if !endpoint.Public && len(endpoint.RequiredPermissions) == 0 {
return fmt.Errorf("endpoint[%d]: %w: %s", i, ErrEndpointMissingPermissions, endpoint.Path)
}
// Validate path pattern
if err := c.validateEndpointPath(endpoint.Path, i); err != nil {
return err
}
}
return nil
}
// validateEndpointPath validates that an endpoint path uses mux patterns only.
// Rejects wildcard patterns (/*) and old regex patterns (^...$).
func (c *Config) validateEndpointPath(path string, index int) error {
if path == "" {
return nil // Empty path is handled elsewhere
}
// Reject wildcard patterns
if err := c.checkWildcardPattern(path, index); err != nil {
return err
}
// Reject old regex patterns
if err := c.checkRegexPattern(path, index); err != nil {
return err
}
// Reject regex indicators outside of variable constraints
if err := c.checkRegexIndicators(path, index); err != nil {
return err
}
// Validate mux pattern syntax if it contains variables
if isMuxPattern(path) {
if err := validateMuxPattern(path); err != nil {
return fmt.Errorf("endpoint[%d]: invalid mux pattern: %w", index, err)
}
}
return nil
}
// checkWildcardPattern checks if path contains wildcard pattern.
func (*Config) checkWildcardPattern(path string, index int) error {
if strings.Contains(path, "/*") {
return fmt.Errorf("endpoint[%d]: %w: %s. Examples: /api/{resource} for single-level or /api/{path:.*} for multi-level",
index, errWildcardPatternNotSupported, path)
}
return nil
}
// checkRegexPattern checks if path contains old regex pattern.
func (*Config) checkRegexPattern(path string, index int) error {
if strings.HasPrefix(path, "^") || strings.HasSuffix(path, "$") {
return fmt.Errorf("endpoint[%d]: %w: %s. Example: /api/users/{id:[0-9]+} instead of ^/api/users/\\d+$",
index, errRegexPatternNotSupported, path)
}
return nil
}
// checkRegexIndicators checks if path contains regex indicators outside variable constraints.
func (*Config) checkRegexIndicators(path string, index int) error {
if strings.Contains(path, "\\d") || strings.Contains(path, "\\w") || strings.Contains(path, "\\s") {
// Only allow if it's inside a variable constraint like {id:[0-9]+}
if !strings.Contains(path, "{") || !strings.Contains(path, ":") {
return fmt.Errorf("endpoint[%d]: %w: %s. Example: /api/users/{id:[0-9]+}",
index, errRegexIndicatorNotSupported, path)
}
}
return nil
}
// processUnifiedConfig processes the unified Roles and Endpoints config
// and builds internal maps for efficient lookup.
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) processUnifiedConfig() error {
c.initializeMaps()
c.buildRolePermissionsMap()
return c.buildEndpointPermissionMap()
}
// initializeMaps initializes internal maps.
func (c *Config) initializeMaps() {
c.rolePermissionsMap = make(map[string][]string)
c.endpointPermissionMap = make(map[string][]string)
c.publicEndpointsMap = make(map[string]bool)
c.endpointMap = make(map[string]*EndpointMapping)
// Use StrictSlash(false) to match the application router's behavior
c.muxRouter = mux.NewRouter().StrictSlash(false)
}
// buildRolePermissionsMap builds the role permissions map from Roles.
// Uses getEffectivePermissions() for consistent inheritance logic.
func (c *Config) buildRolePermissionsMap() {
for _, roleDef := range c.Roles {
// Use getEffectivePermissions() for consistent inheritance handling
permissions := c.getEffectivePermissions(roleDef.Name)
c.rolePermissionsMap[roleDef.Name] = permissions
}
}
// buildEndpointPermissionMap builds the endpoint permission map from Endpoints.
func (c *Config) buildEndpointPermissionMap() error {
for _, endpoint := range c.Endpoints {
methods := endpoint.Methods
if len(methods) == 0 {
methods = []string{"*"}
}
if err := c.processEndpointMethods(&endpoint, methods); err != nil {
return err
}
}
return nil
}
// processEndpointMethods processes methods for an endpoint.
func (c *Config) processEndpointMethods(endpoint *EndpointMapping, methods []string) error {
for _, method := range methods {
methodUpper := strings.ToUpper(method)
key := buildEndpointKey(endpoint, methodUpper)
if err := c.storeEndpointMapping(endpoint, key, methodUpper); err != nil {
return err
}
}
return nil
}
// buildEndpointKey builds the key for an endpoint.
// Uses Path field which may contain mux patterns or exact paths.
func buildEndpointKey(endpoint *EndpointMapping, methodUpper string) string {
pattern := endpoint.Path
return fmt.Sprintf("%s:%s", methodUpper, pattern)
}
// storeEndpointMapping stores an endpoint mapping.
func (c *Config) storeEndpointMapping(endpoint *EndpointMapping, key, methodUpper string) error {
// Store endpoint object for fast lookup
c.endpointMap[key] = endpoint
if endpoint.Public {
c.publicEndpointsMap[key] = true
return nil
}
if len(endpoint.RequiredPermissions) == 0 {
return fmt.Errorf("%w: %s %s", ErrEndpointMissingPermissions, methodUpper, endpoint.Path)
}
// Store all required permissions (not just the first one)
permissions := make([]string, len(endpoint.RequiredPermissions))
copy(permissions, endpoint.RequiredPermissions)
c.endpointPermissionMap[key] = permissions
return nil
}
// getEffectivePermissions recursively gets all permissions for a role including inherited ones.
func (c *Config) getEffectivePermissions(roleName string) []string {
var permissions []string
visited := make(map[string]bool)
var collectPermissions func(string)
collectPermissions = func(name string) {
if visited[name] {
return
}
visited[name] = true
// Find role definition
for _, roleDef := range c.Roles {
if roleDef.Name == name {
permissions = append(permissions, roleDef.Permissions...)
// Recursively collect from inherited roles
for _, inheritedName := range roleDef.InheritsFrom {
collectPermissions(inheritedName)
}
break
}
}
}
collectPermissions(roleName)
return permissions
}
// GetRolePermissions returns the permissions for a role.
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) GetRolePermissions(role string) []string {
return c.rolePermissionsMap[role]
}
// GetEndpointPermission returns the required permissions for an endpoint.
// Returns empty slice if endpoint is public or not found.
// Returns all required permissions (user needs ANY of them - OR logic).
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) GetEndpointPermission(method, path string) ([]string, bool) {
methodUpper := strings.ToUpper(method)
key := fmt.Sprintf("%s:%s", methodUpper, path)
// Try exact match first
if perms, isPublic := c.checkExactMatch(key); isPublic || len(perms) > 0 {
return perms, isPublic
}
// Try pattern and regex matching
return c.checkPatternMatch(methodUpper, path)
}
// getExactEndpoint returns the endpoint for an exact key match (O(1) lookup).
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) getExactEndpoint(key string) (*EndpointMapping, bool) {
if endpoint, ok := c.endpointMap[key]; ok {
isPublic := c.publicEndpointsMap[key]
return endpoint, isPublic
}
return nil, false
}
// checkExactMatch checks for an exact endpoint match.
func (c *Config) checkExactMatch(key string) (permissions []string, isPublic bool) {
if public, ok := c.publicEndpointsMap[key]; ok && public {
return nil, true
}
if perms, ok := c.endpointPermissionMap[key]; ok {
return perms, false
}
return nil, false
}
// findEndpointByPattern finds an endpoint by pattern matching (wildcards/regex).
// Only used when exact match fails, so this is O(n) but only for patterns.
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) findEndpointByPattern(methodUpper, path string) (*EndpointMapping, bool) {
// Try pattern matching for wildcards/regex
// Iterate over endpointMap to find matching patterns
for key, endpoint := range c.endpointMap {
if c.matchesKey(key, methodUpper, path) {
isPublic := c.publicEndpointsMap[key]
return endpoint, isPublic
}
}
return nil, false
}
// checkPatternMatch checks for pattern and regex matches.
// Config is read-only after initialization, so no mutex is needed.
func (c *Config) checkPatternMatch(methodUpper, path string) (permissions []string, isPublic bool) {
// Try pattern matching for wildcards
for key, perms := range c.endpointPermissionMap {
if c.matchesKey(key, methodUpper, path) {
return perms, false
}
}
// Check public endpoints with pattern/regex
for key := range c.publicEndpointsMap {
if c.matchesKey(key, methodUpper, path) {
return nil, true
}
}
return nil, false
}
// matchesKey checks if a key matches the given method and path.
// Keys are built by buildEndpointKey which uses Path (may contain mux patterns).
// Uses mux Route.Match() for mux patterns, exact match for non-pattern paths.
func (c *Config) matchesKey(key, methodUpper, path string) bool {
if !strings.HasPrefix(key, methodUpper+":") {
return false
}
pattern := strings.TrimPrefix(key, methodUpper+":")
// For exact paths (no variables), use string comparison
if !isMuxPattern(pattern) {
return pattern == path
}
// For mux patterns, use Route.Match() from endpoint_matcher
return matchMuxPattern(pattern, methodUpper, path, c.muxRouter)
}
================================================
FILE: pkg/gofr/rbac/config_test.go
================================================
package rbac
import (
"os"
"path/filepath"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadPermissions_ValidConfigs(t *testing.T) {
t.Run("loads valid json config", func(t *testing.T) {
fileContent := `{
"roles": [{"name": "admin", "permissions": ["admin:read", "admin:write"]}],
"endpoints": [{"path": "/api", "methods": ["GET"], "requiredPermissions": ["admin:read"]}]
}`
path, err := createTestConfigFile("test_config.json", fileContent)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_config.json", nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, config.rolePermissionsMap)
})
t.Run("loads valid yaml config", func(t *testing.T) {
fileContent := `roles:
- name: admin
permissions: ["admin:read", "admin:write"]
endpoints:
- path: /api
methods: ["GET"]
requiredPermissions: ["admin:read"]`
path, err := createTestConfigFile("test_config.yaml", fileContent)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_config.yaml", nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, config.rolePermissionsMap)
})
t.Run("loads valid yml config", func(t *testing.T) {
fileContent := `roles:
- name: viewer
permissions: ["users:read"]`
path, err := createTestConfigFile("test_config.yml", fileContent)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_config.yml", nil, nil, nil)
require.NoError(t, err)
require.NotNil(t, config)
assert.NotNil(t, config.rolePermissionsMap)
})
}
func TestLoadPermissions_ErrorCases(t *testing.T) {
t.Run("returns error for non-existent file", func(t *testing.T) {
config, err := LoadPermissions("nonexistent.json", nil, nil, nil)
require.Error(t, err)
require.Nil(t, config)
})
t.Run("returns error for invalid json", func(t *testing.T) {
path, err := createTestConfigFile("test_invalid.json", `invalid json{`)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_invalid.json", nil, nil, nil)
require.Error(t, err)
require.Nil(t, config)
})
t.Run("returns error for invalid yaml", func(t *testing.T) {
path, err := createTestConfigFile("test_invalid.yaml", `invalid: yaml: [`)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_invalid.yaml", nil, nil, nil)
require.Error(t, err)
require.Nil(t, config)
})
t.Run("returns error for unsupported format", func(t *testing.T) {
path, err := createTestConfigFile("test.txt", `some content`)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test.txt", nil, nil, nil)
require.Error(t, err)
require.Nil(t, config)
})
t.Run("returns error for endpoint without requiredPermissions", func(t *testing.T) {
fileContent := `{
"roles": [{"name": "admin", "permissions": ["*:*"]}],
"endpoints": [{"path": "/api", "methods": ["GET"]}]
}`
path, err := createTestConfigFile("test_missing_perm.json", fileContent)
require.NoError(t, err)
defer os.Remove(path)
config, err := LoadPermissions("test_missing_perm.json", nil, nil, nil)
require.Error(t, err)
require.Nil(t, config)
})
}
func TestConfig_GetRolePermissions(t *testing.T) {
testCases := []struct {
desc string
config *Config
role string
expectedPerms []string
}{
{
desc: "returns permissions for existing role",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
{Name: "viewer", Permissions: []string{"users:read"}},
},
},
role: "admin",
expectedPerms: []string{"*:*"},
},
{
desc: "returns empty for non-existent role",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
},
role: "nonexistent",
expectedPerms: nil,
},
{
desc: "returns permissions with inheritance",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
{Name: "editor", Permissions: []string{"users:write"}, InheritsFrom: []string{"viewer"}},
},
},
role: "editor",
expectedPerms: []string{"users:write", "users:read"},
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.config.processUnifiedConfig()
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
result := tc.config.GetRolePermissions(tc.role)
assert.Equal(t, tc.expectedPerms, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestConfig_GetEndpointPermission_ExactMatch(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/api/users")
assert.Equal(t, []string{"users:read"}, perms)
assert.False(t, isPublic)
}
func TestConfig_GetEndpointPermission_PublicEndpoint(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/health", Methods: []string{"GET"}, Public: true},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/health")
assert.Nil(t, perms)
assert.True(t, isPublic)
}
func TestConfig_GetEndpointPermission_NotFound(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("POST", "/api/posts")
assert.Nil(t, perms)
assert.False(t, isPublic)
}
func TestConfig_GetEndpointPermission_MuxPattern(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/api/users")
assert.Equal(t, []string{"api:read"}, perms)
assert.False(t, isPublic)
}
func TestConfig_GetEndpointPermission_MuxPatternWithConstraint(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/api/users/123")
assert.Equal(t, []string{"users:read"}, perms)
assert.False(t, isPublic)
}
func TestConfig_GetEndpointPermission_CaseInsensitive(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"get"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/api")
assert.Equal(t, []string{"api:read"}, perms)
assert.False(t, isPublic)
}
func TestConfig_GetEndpointPermission_MultiplePermissions(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{
Path: "/api/users",
Methods: []string{"GET"},
RequiredPermissions: []string{"users:read", "users:admin"},
},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
perms, isPublic := config.GetEndpointPermission("GET", "/api/users")
assert.Equal(t, []string{"users:read", "users:admin"}, perms)
assert.False(t, isPublic)
}
func TestConfig_processUnifiedConfig(t *testing.T) {
testCases := []struct {
desc string
config *Config
expectError bool
}{
{
desc: "processes config with roles and endpoints",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:*"}},
},
},
expectError: false,
},
{
desc: "processes config with role inheritance",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
{Name: "editor", Permissions: []string{"users:write"}, InheritsFrom: []string{"viewer"}},
},
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
},
expectError: false,
},
{
desc: "returns error for endpoint without requiredPermissions",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}},
},
},
expectError: true,
},
{
desc: "processes config with public endpoints",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
Endpoints: []EndpointMapping{
{Path: "/health", Methods: []string{"GET"}, Public: true},
},
},
expectError: false,
},
{
desc: "processes config with empty methods",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{}, RequiredPermissions: []string{"admin:*"}},
},
},
expectError: false,
},
{
desc: "processes config with regex endpoints",
config: &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
Endpoints: []EndpointMapping{
{Path: "/api/users/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:*"}},
},
},
expectError: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.config.processUnifiedConfig()
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.NotNil(t, tc.config.rolePermissionsMap, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.NotNil(t, tc.config.endpointPermissionMap, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func createTestConfigFile(filename, content string) (string, error) {
dir := filepath.Dir(filename)
if dir != "." && dir != "" {
err := os.MkdirAll(dir, 0755)
if err != nil {
return "", err
}
}
err := os.WriteFile(filename, []byte(content), 0600)
return filename, err
}
func TestConfig_FindEndpointByPattern(t *testing.T) {
t.Run("finds endpoint with mux pattern", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint, isPublic := config.findEndpointByPattern("GET", "/api/users")
assert.NotNil(t, endpoint)
assert.Equal(t, "/api/{resource}", endpoint.Path)
assert.False(t, isPublic)
})
t.Run("finds endpoint with mux pattern constraint", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint, isPublic := config.findEndpointByPattern("GET", "/api/users/123")
assert.NotNil(t, endpoint)
assert.False(t, isPublic)
})
t.Run("finds public endpoint with pattern", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/public/{path:.*}", Methods: []string{"GET"}, Public: true},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint, isPublic := config.findEndpointByPattern("GET", "/public/files")
assert.NotNil(t, endpoint)
assert.True(t, isPublic)
})
t.Run("returns nil when no pattern matches", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint, isPublic := config.findEndpointByPattern("GET", "/other/path")
assert.Nil(t, endpoint)
assert.False(t, isPublic)
})
t.Run("returns nil when method doesn't match", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint, isPublic := config.findEndpointByPattern("POST", "/api/users")
assert.Nil(t, endpoint)
assert.False(t, isPublic)
})
}
func TestConfig_MatchesKey(t *testing.T) {
t.Run("matches exact path", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/api/users", "GET", "/api/users")
assert.True(t, result)
})
t.Run("matches mux pattern", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/api/{resource}", "GET", "/api/users")
assert.True(t, result)
})
t.Run("matches mux pattern with constraint", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/api/users/{id:[0-9]+}", "GET", "/api/users/123")
assert.True(t, result)
})
t.Run("matches mux pattern with constraint", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/test/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"test:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/test/{id:[0-9]+}", "GET", "/test/456")
assert.True(t, result)
})
t.Run("returns false when method doesn't match", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/api/users", "POST", "/api/users")
assert.False(t, result)
})
t.Run("returns false for invalid mux pattern", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/invalid{", Methods: []string{"GET"}, RequiredPermissions: []string{"test:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
result := config.matchesKey("GET:/api/invalid{", "GET", "/test")
assert.False(t, result)
})
}
func TestConfig_getEffectivePermissions(t *testing.T) {
testCases := []struct {
desc string
config *Config
roleName string
expectedPerms []string
}{
{
desc: "returns permissions for role without inheritance",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
},
},
roleName: "viewer",
expectedPerms: []string{"users:read"},
},
{
desc: "returns permissions with single level inheritance",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
{Name: "editor", Permissions: []string{"users:write"}, InheritsFrom: []string{"viewer"}},
},
},
roleName: "editor",
expectedPerms: []string{"users:write", "users:read"},
},
{
desc: "returns permissions with multi-level inheritance",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
{Name: "editor", Permissions: []string{"users:write"}, InheritsFrom: []string{"viewer"}},
{Name: "admin", Permissions: []string{"users:delete"}, InheritsFrom: []string{"editor"}},
},
},
roleName: "admin",
expectedPerms: []string{"users:delete", "users:write", "users:read"},
},
{
desc: "handles circular inheritance gracefully",
config: &Config{
Roles: []RoleDefinition{
{Name: "role1", Permissions: []string{"perm1"}, InheritsFrom: []string{"role2"}},
{Name: "role2", Permissions: []string{"perm2"}, InheritsFrom: []string{"role1"}},
},
},
roleName: "role1",
expectedPerms: []string{"perm1", "perm2"},
},
{
desc: "returns empty for non-existent role",
config: &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
},
},
roleName: "nonexistent",
expectedPerms: nil,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result := tc.config.getEffectivePermissions(tc.roleName)
assert.Equal(t, tc.expectedPerms, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractNestedClaim_Additional(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "extracts claim with jwt.MapClaims nested",
claims: jwt.MapClaims{
"user": jwt.MapClaims{
"role": "admin",
},
},
path: "user.role",
expected: "admin",
expectError: false,
},
{
desc: "returns error when intermediate value is not map",
claims: jwt.MapClaims{
"user": "not a map",
},
path: "user.role",
expected: nil,
expectError: true,
},
{
desc: "extracts from mixed map types",
claims: jwt.MapClaims{
"level1": map[string]any{
"level2": jwt.MapClaims{
"value": "test",
},
},
},
path: "level1.level2.value",
expected: "test",
expectError: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result, err := extractNestedClaim(tc.claims, tc.path)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Nil(t, result, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractArrayClaim_Additional(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "extracts from array with valid index",
claims: jwt.MapClaims{
"roles": []any{"admin", "user", "guest"},
},
path: "roles[2]",
expected: "guest",
expectError: false,
},
{
desc: "returns error for invalid array notation format",
claims: jwt.MapClaims{
"roles": []any{"admin"},
},
path: "roles]0[",
expected: nil,
expectError: true,
},
{
desc: "returns error for non-numeric index",
claims: jwt.MapClaims{
"roles": []any{"admin"},
},
path: "roles[abc]",
expected: nil,
expectError: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
idx := 0
for j, c := range tc.path {
if c == '[' {
idx = j
break
}
}
result, err := extractArrayClaim(tc.claims, tc.path, idx)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Nil(t, result, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
================================================
FILE: pkg/gofr/rbac/endpoint_matcher.go
================================================
package rbac
import (
"errors"
"fmt"
"net/http"
"net/url"
"os"
"strings"
"github.com/gorilla/mux"
)
const (
// DefaultConfigPath is the default config path value (empty string).
// When passed to ResolveRBACConfigPath, it will try default paths: configs/rbac.json, configs/rbac.yaml, configs/rbac.yml.
DefaultConfigPath = ""
// Default RBAC config paths (tried in order).
defaultRBACJSONPath = "configs/rbac.json"
defaultRBACYAMLPath = "configs/rbac.yaml"
defaultRBACYMLPath = "configs/rbac.yml"
)
var (
// errUnbalancedBraces is returned when a mux pattern has unbalanced braces.
errUnbalancedBraces = errors.New("unbalanced braces in pattern")
)
// matchEndpoint checks if the request matches an endpoint configuration.
// This is the primary authorization check using the unified Endpoints configuration.
// Returns the matched endpoint and whether it's public.
func matchEndpoint(method, route string, endpoints []EndpointMapping, config *Config) (*EndpointMapping, bool) {
for i := range endpoints {
endpoint := &endpoints[i]
// Check if endpoint is public
if endpoint.Public {
if matchesEndpointPattern(endpoint, route, config) {
return endpoint, true
}
continue
}
// Check method match
if !matchesHTTPMethod(method, endpoint.Methods) {
continue
}
// Check route match
if matchesEndpointPattern(endpoint, route, config) {
return endpoint, false
}
}
return nil, false
}
// matchesHTTPMethod checks if the HTTP method matches the endpoint's allowed methods.
func matchesHTTPMethod(method string, allowedMethods []string) bool {
// Empty methods or "*" means all methods
if len(allowedMethods) == 0 {
return true
}
for _, m := range allowedMethods {
if m == "*" || strings.EqualFold(m, method) {
return true
}
}
return false
}
// isMuxPattern detects if a pattern contains mux-style variables.
// Returns true if pattern contains { and }.
func isMuxPattern(pattern string) bool {
return strings.Contains(pattern, "{") && strings.Contains(pattern, "}")
}
// matchMuxPattern uses mux Route.Match() to test if a path matches a mux pattern.
// Creates a temporary mux Route and uses Route.Match() to test the pattern.
// Handles all mux pattern types: {id}, {id:[0-9]+}, {path:.*}, etc.
func matchMuxPattern(pattern, method, path string, router *mux.Router) bool {
if router == nil {
return false
}
// Create a temporary route with the pattern
route := router.NewRoute().Path(pattern)
// If method is specified, add it to the route
if method != "" {
route = route.Methods(method)
}
// Create a mock request for matching
req := &http.Request{
Method: method,
URL: &url.URL{
Path: path,
},
}
// Use Route.Match() to test if the request matches the pattern
var match mux.RouteMatch
return route.Match(req, &match)
}
// validateMuxPattern validates mux pattern syntax.
// Ensures balanced braces and validates regex constraints format.
func validateMuxPattern(pattern string) error {
// Check for balanced braces
openCount := strings.Count(pattern, "{")
closeCount := strings.Count(pattern, "}")
if openCount != closeCount {
return fmt.Errorf("%w: %s", errUnbalancedBraces, pattern)
}
// Check that if there are closing braces, there must be opening braces
// A pattern like "/api/id}" should not be valid
if closeCount > 0 && openCount == 0 {
return fmt.Errorf("%w: %s", errUnbalancedBraces, pattern)
}
// Basic validation: check that braces are properly formatted
// More detailed validation would require parsing, which mux will do anyway
return nil
}
// matchesEndpointPattern checks if the route matches the endpoint pattern.
// Method matching is handled separately in matchEndpoint before this function is called.
// Uses mux Route.Match() for mux patterns, exact match for non-pattern paths.
func matchesEndpointPattern(endpoint *EndpointMapping, route string, config *Config) bool {
if endpoint.Path == "" {
return false
}
pattern := endpoint.Path
// Exact match for non-pattern paths
if !isMuxPattern(pattern) {
return pattern == route
}
// Use mux Route.Match() for patterns
// Method is handled separately, so pass empty string here
return matchMuxPattern(pattern, "", route, config.muxRouter)
}
// checkEndpointAuthorization checks if the user's role is authorized for the endpoint.
// Pure permission-based: checks if role has ANY of the required permissions (OR logic).
// Uses the endpoint parameter directly instead of re-looking it up.
func checkEndpointAuthorization(role string, endpoint *EndpointMapping, config *Config) (allowed bool, reason string) {
// Public endpoints are always allowed
if endpoint.Public {
return true, "public-endpoint"
}
// Get required permissions
requiredPerms := endpoint.RequiredPermissions
// If no permission requirement found, deny (fail secure)
if len(requiredPerms) == 0 {
return false, ""
}
// Get role's permissions (thread-safe)
rolePerms := config.GetRolePermissions(role)
if len(rolePerms) == 0 {
return false, ""
}
// Check if role has ANY of the required permissions (OR logic)
// Only exact matches are supported - wildcards are NOT supported in permissions
for _, requiredPerm := range requiredPerms {
for _, perm := range rolePerms {
// Exact match only - no wildcard support
if perm == requiredPerm {
return true, "permission-based"
}
}
}
return false, ""
}
// getEndpointForRequest finds the matching endpoint configuration for a request.
// This is the primary function used by the middleware to determine authorization requirements.
// Uses optimized maps for O(1) exact matches, falls back to pattern matching for mux patterns.
func getEndpointForRequest(r *http.Request, config *Config) (*EndpointMapping, bool) {
if len(config.Endpoints) == 0 {
return nil, false
}
method := strings.ToUpper(r.Method)
path := r.URL.Path
key := fmt.Sprintf("%s:%s", method, path)
// Try exact match first (O(1) lookup)
if endpoint, isPublic := config.getExactEndpoint(key); endpoint != nil {
return endpoint, isPublic
}
// Try pattern matching (O(n) but only for patterns, not exact matches)
return config.findEndpointByPattern(method, path)
}
// ResolveRBACConfigPath resolves the RBAC config file path.
// If configFile is empty, tries default paths in order: configs/rbac.json, configs/rbac.yaml, configs/rbac.yml.
func ResolveRBACConfigPath(configFile string) string {
// If custom path provided, use it
if configFile != "" {
return configFile
}
// Try default paths in order
defaultPaths := []string{
defaultRBACJSONPath,
defaultRBACYAMLPath,
defaultRBACYMLPath,
}
for _, path := range defaultPaths {
if _, err := os.Stat(path); err == nil {
return path
}
}
return ""
}
================================================
FILE: pkg/gofr/rbac/endpoint_matcher_test.go
================================================
package rbac
import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMatchEndpoint_ExactMatch(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("GET", "/api/users", endpoints, config)
require.NotNil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_PublicEndpoint(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/health", Methods: []string{"GET"}, Public: true},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("GET", "/health", endpoints, config)
require.NotNil(t, endpoint)
assert.True(t, isPublic)
}
func TestMatchEndpoint_DifferentMethod(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("POST", "/api/users", endpoints, config)
require.Nil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_WildcardMethod(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"*"}, RequiredPermissions: []string{"api:*"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("POST", "/api", endpoints, config)
require.NotNil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_MuxPatternPath(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/{resource}", Methods: []string{"GET"}, RequiredPermissions: []string{"api:read"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("GET", "/api/users", endpoints, config)
require.NotNil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_MuxPatternWithConstraint(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users/{id:[0-9]+}", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("GET", "/api/users/123", endpoints, config)
require.NotNil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_NotFound(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("GET", "/api/posts", endpoints, config)
require.Nil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchEndpoint_EmptyMethods(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{}, RequiredPermissions: []string{"api:*"}},
},
}
_ = config.processUnifiedConfig()
endpoints := config.Endpoints
endpoint, isPublic := matchEndpoint("POST", "/api", endpoints, config)
require.NotNil(t, endpoint)
assert.False(t, isPublic)
}
func TestMatchesHTTPMethod(t *testing.T) {
testCases := []struct {
desc string
method string
allowedMethods []string
expected bool
}{
{
desc: "matches exact method",
method: "GET",
allowedMethods: []string{"GET"},
expected: true,
},
{
desc: "matches case-insensitive method",
method: "get",
allowedMethods: []string{"GET"},
expected: true,
},
{
desc: "matches wildcard method",
method: "POST",
allowedMethods: []string{"*"},
expected: true,
},
{
desc: "matches empty methods as all",
method: "DELETE",
allowedMethods: []string{},
expected: true,
},
{
desc: "does not match different method",
method: "POST",
allowedMethods: []string{"GET"},
expected: false,
},
{
desc: "matches one of multiple methods",
method: "PUT",
allowedMethods: []string{"GET", "PUT", "POST"},
expected: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result := matchesHTTPMethod(tc.method, tc.allowedMethods)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestMatchesEndpointPattern(t *testing.T) {
testCases := []struct {
desc string
endpoint *EndpointMapping
route string
expected bool
}{
{
desc: "matches exact path",
endpoint: &EndpointMapping{
Path: "/api/users",
},
route: "/api/users",
expected: true,
},
{
desc: "matches mux pattern with constraint",
endpoint: &EndpointMapping{
Path: "/api/users/{id:[0-9]+}",
},
route: "/api/users/123",
expected: true,
},
{
desc: "matches mux pattern single variable",
endpoint: &EndpointMapping{
Path: "/api/{resource}",
},
route: "/api/users",
expected: true,
},
{
desc: "matches mux pattern with exact prefix",
endpoint: &EndpointMapping{
Path: "/api/{resource}",
},
route: "/api",
expected: false, // /api/{resource} requires a segment after /api
},
{
desc: "matches mux pattern multi-level",
endpoint: &EndpointMapping{
Path: "/api/{path:.*}",
},
route: "/api/users/123",
expected: true,
},
{
desc: "does not match different path",
endpoint: &EndpointMapping{
Path: "/api/users",
},
route: "/api/posts",
expected: false,
},
{
desc: "does not match invalid mux pattern",
endpoint: &EndpointMapping{
Path: "/api/{invalid",
},
route: "/api/users",
expected: false,
},
{
desc: "does not match constraint violation",
endpoint: &EndpointMapping{
Path: "/api/users/{id:[0-9]+}",
},
route: "/api/users/abc",
expected: false,
},
}
config := &Config{}
_ = config.processUnifiedConfig()
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result := matchesEndpointPattern(tc.endpoint, tc.route, config)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestCheckEndpointAuthorization_PublicEndpoint(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "any", Permissions: []string{}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{Public: true}
authorized, reason := checkEndpointAuthorization("any", endpoint, config)
assert.True(t, authorized)
assert.Equal(t, "public-endpoint", reason)
}
func TestCheckEndpointAuthorization_ExactPermission(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read"}}
authorized, reason := checkEndpointAuthorization("admin", endpoint, config)
assert.True(t, authorized)
assert.Equal(t, "permission-based", reason)
}
func TestCheckEndpointAuthorization_WildcardsNotSupported(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read"}}
authorized, reason := checkEndpointAuthorization("admin", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestCheckEndpointAuthorization_ResourceWildcardNotSupported(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"users:*"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read"}}
authorized, reason := checkEndpointAuthorization("admin", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestCheckEndpointAuthorization_NoPermission(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:write"}}
authorized, reason := checkEndpointAuthorization("viewer", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestCheckEndpointAuthorization_EmptyRequiredPermissions(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"*:*"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{}}
authorized, reason := checkEndpointAuthorization("admin", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestCheckEndpointAuthorization_NoRolePermissions(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "guest", Permissions: []string{}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read"}}
authorized, reason := checkEndpointAuthorization("guest", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestCheckEndpointAuthorization_InheritedPermissions(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
{Name: "editor", Permissions: []string{"users:write"}, InheritsFrom: []string{"viewer"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read"}}
authorized, reason := checkEndpointAuthorization("editor", endpoint, config)
assert.True(t, authorized)
assert.Equal(t, "permission-based", reason)
}
func TestCheckEndpointAuthorization_MultiplePermissions_OR_First(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read", "users:admin"}}
authorized, reason := checkEndpointAuthorization("viewer", endpoint, config)
assert.True(t, authorized)
assert.Equal(t, "permission-based", reason)
}
func TestCheckEndpointAuthorization_MultiplePermissions_OR_Second(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"users:admin"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read", "users:admin"}}
authorized, reason := checkEndpointAuthorization("admin", endpoint, config)
assert.True(t, authorized)
assert.Equal(t, "permission-based", reason)
}
func TestCheckEndpointAuthorization_MultiplePermissions_None(t *testing.T) {
config := &Config{
Roles: []RoleDefinition{
{Name: "guest", Permissions: []string{"posts:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
endpoint := &EndpointMapping{RequiredPermissions: []string{"users:read", "users:write"}}
authorized, reason := checkEndpointAuthorization("guest", endpoint, config)
assert.False(t, authorized)
assert.Empty(t, reason)
}
func TestGetEndpointForRequest(t *testing.T) {
testCases := []struct {
desc string
request *http.Request
config *Config
expectedMatch bool
expectedPublic bool
}{
{
desc: "matches endpoint for request",
request: httptest.NewRequest(http.MethodGet, "/api/users", http.NoBody),
config: &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
},
expectedMatch: true,
expectedPublic: false,
},
{
desc: "matches public endpoint",
request: httptest.NewRequest(http.MethodGet, "/health", http.NoBody),
config: &Config{
Endpoints: []EndpointMapping{
{Path: "/health", Methods: []string{"GET"}, Public: true},
},
},
expectedMatch: true,
expectedPublic: true,
},
{
desc: "returns nil for empty endpoints",
request: httptest.NewRequest(http.MethodGet, "/api/users", http.NoBody),
config: &Config{
Endpoints: []EndpointMapping{},
},
expectedMatch: false,
expectedPublic: false,
},
{
desc: "returns nil for non-matching request",
request: httptest.NewRequest(http.MethodPost, "/api/posts", http.NoBody),
config: &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
},
expectedMatch: false,
expectedPublic: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
err := tc.config.processUnifiedConfig()
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
endpoint, isPublic := getEndpointForRequest(tc.request, tc.config)
if tc.expectedMatch {
require.NotNil(t, endpoint, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expectedPublic, isPublic, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.Nil(t, endpoint, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.False(t, isPublic, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestIsMuxPattern(t *testing.T) {
testCases := []struct {
desc string
pattern string
expected bool
}{
{
desc: "detects mux pattern with single variable",
pattern: "/api/users/{id}",
expected: true,
},
{
desc: "detects mux pattern with constraint",
pattern: "/api/users/{id:[0-9]+}",
expected: true,
},
{
desc: "detects mux pattern multi-level",
pattern: "/api/{path:.*}",
expected: true,
},
{
desc: "does not detect exact path",
pattern: "/api/users",
expected: false,
},
{
desc: "does not detect wildcard",
pattern: "/api/*",
expected: false,
},
{
desc: "does not detect regex",
pattern: "^/api/users/\\d+$",
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result := isMuxPattern(tc.pattern)
assert.Equal(t, tc.expected, result)
})
}
}
func TestMatchMuxPattern(t *testing.T) {
router := mux.NewRouter()
testCases := []struct {
desc string
pattern string
method string
path string
expected bool
}{
{
desc: "matches single variable",
pattern: "/api/users/{id}",
method: "GET",
path: "/api/users/123",
expected: true,
},
{
desc: "matches variable with constraint",
pattern: "/api/users/{id:[0-9]+}",
method: "GET",
path: "/api/users/123",
expected: true,
},
{
desc: "does not match constraint violation",
pattern: "/api/users/{id:[0-9]+}",
method: "GET",
path: "/api/users/abc",
expected: false,
},
{
desc: "matches multi-level pattern",
pattern: "/api/{path:.*}",
method: "GET",
path: "/api/users/123",
expected: true,
},
{
desc: "matches middle variable",
pattern: "/api/{category}/posts",
method: "GET",
path: "/api/tech/posts",
expected: true,
},
{
desc: "matches multiple variables",
pattern: "/api/{category}/posts/{id:[0-9]+}",
method: "GET",
path: "/api/tech/posts/123",
expected: true,
},
{
desc: "does not match different path",
pattern: "/api/users/{id}",
method: "GET",
path: "/api/posts/123",
expected: false,
},
{
desc: "returns false for nil router",
pattern: "/api/users/{id}",
method: "GET",
path: "/api/users/123",
expected: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
var testRouter *mux.Router
if tc.desc != "returns false for nil router" {
testRouter = router
}
result := matchMuxPattern(tc.pattern, tc.method, tc.path, testRouter)
assert.Equal(t, tc.expected, result)
})
}
}
func TestValidateMuxPattern(t *testing.T) {
testCases := []struct {
desc string
pattern string
expectError bool
}{
{
desc: "validates single variable",
pattern: "/api/users/{id}",
expectError: false,
},
{
desc: "validates variable with constraint",
pattern: "/api/users/{id:[0-9]+}",
expectError: false,
},
{
desc: "validates multi-level pattern",
pattern: "/api/{path:.*}",
expectError: false,
},
{
desc: "validates non-pattern path",
pattern: "/api/users",
expectError: false,
},
{
desc: "rejects unbalanced braces",
pattern: "/api/users/{id",
expectError: true,
},
{
desc: "rejects unbalanced braces close",
pattern: "/api/users/id}",
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
err := validateMuxPattern(tc.pattern)
if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestResolveRBACConfigPath(t *testing.T) {
t.Run("returns custom path when provided", func(t *testing.T) {
customPath := "custom/path/rbac.json"
result := ResolveRBACConfigPath(customPath)
assert.Equal(t, customPath, result)
})
t.Run("returns default json path when file exists", func(t *testing.T) {
// Create a temporary rbac.json file
dir := "configs"
err := os.MkdirAll(dir, 0755)
require.NoError(t, err)
filePath := filepath.Join(dir, "rbac.json")
err = os.WriteFile(filePath, []byte(`{"roles":[]}`), 0600)
require.NoError(t, err)
defer func() {
os.Remove(filePath)
os.Remove(dir)
}()
result := ResolveRBACConfigPath("")
assert.Equal(t, filePath, result)
})
t.Run("returns default yaml path when json doesn't exist", func(t *testing.T) {
// Create a temporary rbac.yaml file
dir := "configs"
err := os.MkdirAll(dir, 0755)
require.NoError(t, err)
filePath := filepath.Join(dir, "rbac.yaml")
err = os.WriteFile(filePath, []byte("roles: []"), 0600)
require.NoError(t, err)
defer func() {
os.Remove(filePath)
os.Remove(dir)
}()
result := ResolveRBACConfigPath("")
assert.Equal(t, filePath, result)
})
t.Run("returns default yml path when json and yaml don't exist", func(t *testing.T) {
// Create a temporary rbac.yml file
dir := "configs"
err := os.MkdirAll(dir, 0755)
require.NoError(t, err)
filePath := filepath.Join(dir, "rbac.yml")
err = os.WriteFile(filePath, []byte("roles: []"), 0600)
require.NoError(t, err)
defer func() {
os.Remove(filePath)
os.Remove(dir)
}()
result := ResolveRBACConfigPath("")
assert.Equal(t, filePath, result)
})
t.Run("returns empty string when no default files exist", func(t *testing.T) {
// Ensure configs directory doesn't exist or is empty
dir := "configs"
os.RemoveAll(dir)
result := ResolveRBACConfigPath("")
assert.Empty(t, result)
})
}
================================================
FILE: pkg/gofr/rbac/middleware.go
================================================
package rbac
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/http/middleware"
)
type authMethod int
const userRole authMethod = 4
const unknownRouteLabel = ""
// AuditLog represents a structured log entry for RBAC authorization decisions.
// It follows the same pattern as HTTP RequestLog for consistency.
type AuditLog struct {
CorrelationID string `json:"correlation_id,omitempty"`
Method string `json:"method,omitempty"`
Route string `json:"route,omitempty"`
Status string `json:"status,omitempty"`
Role string `json:"role,omitempty"`
}
// PrettyPrint formats the RBAC audit log for terminal output, matching HTTP log format.
func (ral *AuditLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%s %-6s %10s %s %s [%s]\u001B[0m\n",
ral.CorrelationID, ral.Status, "", "RBAC", ral.Route, ral.Role)
}
var (
// ErrAccessDenied is returned when a user doesn't have required role/permission.
ErrAccessDenied = errors.New("forbidden: access denied")
// ErrRoleNotFound is returned when role cannot be extracted from request.
ErrRoleNotFound = errors.New("unauthorized: role not found")
// errJWTClaimsNotFound is returned when JWT claims are not found in request context.
errJWTClaimsNotFound = errors.New("JWT claims not found in request context")
// errEmptyClaimPath is returned when claim path is empty.
errEmptyClaimPath = errors.New("empty claim path")
// errClaimPathNotFound is returned when a claim path is not found in JWT claims.
errClaimPathNotFound = errors.New("claim path not found")
// errInvalidArrayNotation is returned when array notation is invalid.
errInvalidArrayNotation = errors.New("invalid array notation")
// errInvalidArrayIndex is returned when array index is invalid.
errInvalidArrayIndex = errors.New("invalid array index")
// errClaimKeyNotFound is returned when a claim key is not found.
errClaimKeyNotFound = errors.New("claim key not found")
// errClaimValueNotArray is returned when a claim value is not an array.
errClaimValueNotArray = errors.New("claim value is not an array")
// errArrayIndexOutOfBounds is returned when array index is out of bounds.
errArrayIndexOutOfBounds = errors.New("array index out of bounds")
// errInvalidClaimStructure is returned when claim structure is invalid.
errInvalidClaimStructure = errors.New("invalid claim structure")
// errAuthorizationError is returned as a generic error message for unknown errors in traces.
errAuthorizationError = errors.New("authorization error")
)
// Middleware creates an HTTP middleware function that enforces RBAC authorization.
// It extracts the user's role and checks if the role is allowed for the requested route.
//
//nolint:gocognit,gocyclo // Middleware complexity is acceptable due to multiple authorization paths
func Middleware(config *Config) func(handler http.Handler) http.Handler {
return func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// If config is nil, allow all requests (fail open)
if config == nil {
handler.ServeHTTP(w, r)
return
}
// Check if endpoint is public using unified Endpoints config
endpoint, isPublic := getEndpointForRequest(r, config)
routeLabel := unknownRouteLabel
if endpoint != nil && endpoint.Path != "" {
routeLabel = endpoint.Path
}
// Start tracing if tracer is available
if config.Tracer != nil {
ctx, span := config.Tracer.Start(r.Context(), "rbac.authorize")
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.route", routeLabel),
)
r = r.WithContext(ctx)
defer span.End()
}
if isPublic {
handler.ServeHTTP(w, r)
return
}
// If no endpoint match found in RBAC config, allow request to proceed to route matching
// RBAC only enforces authorization for endpoints that are explicitly configured
// Routes not in RBAC config are handled by normal route matching (may return 404 if route doesn't exist)
if endpoint == nil {
handler.ServeHTTP(w, r)
return
}
// Extract role using header-based or JWT-based extraction
role, err := extractRole(r, config)
if err != nil {
if config.Metrics != nil {
config.Metrics.IncrementCounter(r.Context(), "rbac_role_extraction_failures")
}
handleAuthError(w, r, config, "", routeLabel, err)
return
}
// Role not included in traces for privacy (roles are PII)
// Only include authorization status (safe boolean) - set below after authorization check
// Check authorization using unified endpoint-based authorization
authorized, _ := checkEndpointAuthorization(role, endpoint, config)
if !authorized {
handleAuthError(w, r, config, role, routeLabel, ErrAccessDenied)
return
}
if config.Tracer != nil {
trace.SpanFromContext(r.Context()).SetAttributes(attribute.Bool("rbac.authorized", true))
}
if config.Logger != nil {
logAuditEvent(config.Logger, r, role, routeLabel, true)
}
// Store role in context and continue
ctx := context.WithValue(r.Context(), userRole, role)
handler.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// handleAuthError handles authorization errors with custom error handler or default response.
func handleAuthError(w http.ResponseWriter, r *http.Request, config *Config, role, route string, err error) {
// Record error in span if tracing is enabled
// Sanitize error message to prevent information leakage
if config.Tracer != nil {
span := trace.SpanFromContext(r.Context())
safeErr := sanitizeErrorForTrace(err)
span.RecordError(safeErr)
span.SetStatus(codes.Error, safeErr.Error())
}
// Log audit event (always enabled when Logger is available)
// Audit logging is automatically performed using GoFr's logger
if config.Logger != nil {
logAuditEvent(config.Logger, r, role, route, false)
}
// Use custom error handler if provided
if config.ErrorHandler != nil {
config.ErrorHandler(w, r, role, route, err)
return
}
// Default error handling
if errors.Is(err, ErrRoleNotFound) {
http.Error(w, "Unauthorized: Missing or invalid role", http.StatusUnauthorized)
return
}
http.Error(w, "Forbidden: Access denied", http.StatusForbidden)
}
// extractRole extracts the user's role from the request.
// Supports header-based extraction (via RoleHeader) or JWT-based extraction (via JWTClaimPath).
// Precedence: JWT takes precedence over header (JWT is more secure).
// No default role is supported - role must be explicitly provided.
func extractRole(r *http.Request, config *Config) (string, error) {
// Try JWT-based extraction first (takes precedence - more secure)
if config.JWTClaimPath != "" {
role, err := extractRoleFromJWT(r, config.JWTClaimPath)
if err == nil && role != "" {
return role, nil
}
// If JWT extraction fails but JWTClaimPath is set, don't fall back to header
// This ensures JWT is the only method when configured
return "", ErrRoleNotFound
}
// Try header-based extraction (only if JWT is not configured)
if config.RoleHeader != "" {
role := r.Header.Get(config.RoleHeader)
if role != "" {
return role, nil
}
}
// No role found - no default role supported
return "", ErrRoleNotFound
}
// extractRoleFromJWT extracts the role from JWT claims in the request context.
// It uses the JWTClaimPath from config to navigate the claim structure.
func extractRoleFromJWT(r *http.Request, claimPath string) (string, error) {
// Get JWT claims from context (set by OAuth middleware)
claims, ok := r.Context().Value(middleware.JWTClaim).(jwt.MapClaims)
if !ok || claims == nil {
return "", fmt.Errorf("%w", errJWTClaimsNotFound)
}
// Extract role using the configured claim path
role, err := extractClaimValue(claims, claimPath)
if err != nil {
return "", fmt.Errorf("failed to extract role from JWT: %w", err)
}
// Convert to string
roleStr, ok := role.(string)
if !ok {
// Try to convert if it's not a string
return fmt.Sprintf("%v", role), nil
}
return roleStr, nil
}
// extractClaimValue extracts a value from JWT claims using a dot-notation or array notation path.
// Examples:
// - "role" -> claims["role"]
// - "roles[0]" -> claims["roles"].([]any)[0]
// - "permissions.role" -> claims["permissions"].(map[string]any)["role"]
func extractClaimValue(claims jwt.MapClaims, path string) (any, error) {
if path == "" {
return nil, fmt.Errorf("%w", errEmptyClaimPath)
}
// Handle array notation: "roles[0]"
if idx := strings.Index(path, "["); idx != -1 {
return extractArrayClaim(claims, path, idx)
}
// Handle dot notation: "permissions.role"
if strings.Contains(path, ".") {
return extractNestedClaim(claims, path)
}
// Simple key lookup
value, ok := claims[path]
if !ok {
return nil, fmt.Errorf("%w: %s", errClaimPathNotFound, path)
}
return value, nil
}
// extractArrayClaim extracts a value from an array in JWT claims.
func extractArrayClaim(claims jwt.MapClaims, path string, idx int) (any, error) {
key := path[:idx]
arrayPath := path[idx:]
// Extract array index
if !strings.HasPrefix(arrayPath, "[") || !strings.HasSuffix(arrayPath, "]") {
return nil, fmt.Errorf("%w: %s", errInvalidArrayNotation, path)
}
indexStr := strings.Trim(arrayPath, "[]")
var index int
if _, err := fmt.Sscanf(indexStr, "%d", &index); err != nil {
return nil, fmt.Errorf("%w: %s", errInvalidArrayIndex, indexStr)
}
value, ok := claims[key]
if !ok {
return nil, fmt.Errorf("%w: %s", errClaimKeyNotFound, key)
}
arr, ok := value.([]any)
if !ok {
return nil, fmt.Errorf("%w: %s", errClaimValueNotArray, key)
}
if index < 0 || index >= len(arr) {
return nil, fmt.Errorf("%w: %d (length: %d)", errArrayIndexOutOfBounds, index, len(arr))
}
return arr[index], nil
}
// extractNestedClaim extracts a value from nested structure in JWT claims.
func extractNestedClaim(claims jwt.MapClaims, path string) (any, error) {
parts := strings.Split(path, ".")
var current any = claims
for i, part := range parts {
isLast := i == len(parts)-1
// Navigate through nested structure
var next any
var exists bool
switch v := current.(type) {
case map[string]any:
next, exists = v[part]
case jwt.MapClaims:
next, exists = v[part]
default:
if isLast {
return nil, fmt.Errorf("%w: %s", errInvalidClaimStructure, strings.Join(parts[:i+1], "."))
}
return nil, fmt.Errorf("%w: %s", errClaimPathNotFound, strings.Join(parts[:i+1], "."))
}
if !exists {
return nil, fmt.Errorf("%w: %s", errClaimPathNotFound, strings.Join(parts[:i+1], "."))
}
if isLast {
return next, nil // Return nil value if key exists but value is nil
}
// For intermediate paths, nil means invalid structure
if next == nil {
return nil, fmt.Errorf("%w: %s", errClaimPathNotFound, strings.Join(parts[:i+1], "."))
}
current = next
}
return nil, fmt.Errorf("%w: %s", errClaimPathNotFound, path)
}
// logAuditEvent logs authorization decisions for audit purposes.
// This is called automatically by the middleware when Logger is set.
// Users don't need to configure this - it uses the provided logger automatically.
func logAuditEvent(logger datasource.Logger, r *http.Request, role, route string, allowed bool) {
if logger == nil {
return // Skip logging if no logger provided
}
status := "REJ"
if allowed {
status = "ACC"
}
// Extract correlation ID from trace context
correlationID := trace.SpanFromContext(r.Context()).SpanContext().TraceID().String()
if correlationID == "" || correlationID == "00000000000000000000000000000000" {
correlationID = ""
}
// Create structured audit log entry
auditLog := &AuditLog{
CorrelationID: correlationID,
Method: r.Method,
Route: route,
Status: status,
Role: role,
}
// Use structured logging at debug level (logger will handle JSON encoding or PrettyPrint)
logger.Debug(auditLog)
}
// sanitizeErrorForTrace sanitizes error messages for traces to prevent information leakage.
// Returns generic error messages that don't expose internal system details.
func sanitizeErrorForTrace(err error) error {
if errors.Is(err, ErrRoleNotFound) {
return ErrRoleNotFound // Safe: generic error message
}
if errors.Is(err, ErrAccessDenied) {
return ErrAccessDenied // Safe: generic error message
}
// For unknown errors, return generic message to prevent information leakage
return errAuthorizationError
}
================================================
FILE: pkg/gofr/rbac/middleware_test.go
================================================
package rbac
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"gofr.dev/pkg/gofr/datasource"
"gofr.dev/pkg/gofr/http/middleware"
)
func TestMiddleware_NilConfig(t *testing.T) {
middlewareFunc := Middleware(nil)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "OK")
}
func TestMiddleware_PublicEndpoint(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/health", Methods: []string{"GET"}, Public: true},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/health", http.NoBody)
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "OK")
}
func TestMiddleware_NoEndpointMatch(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api/users", Methods: []string{"GET"}, RequiredPermissions: []string{"users:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/posts", http.NoBody)
wrapped.ServeHTTP(w, req)
// Routes not in RBAC config are handled by normal route matching
// So unmatched endpoints should be allowed through
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "OK")
}
func TestMiddleware_RoleNotFound(t *testing.T) {
config := &Config{
RoleHeader: "X-User-Role",
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"admin:read", "admin:write"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Contains(t, w.Body.String(), "Unauthorized: Missing or invalid role")
}
func TestMiddleware_ValidRoleAndPermission(t *testing.T) {
config := &Config{
RoleHeader: "X-User-Role",
Roles: []RoleDefinition{
{Name: "admin", Permissions: []string{"admin:read", "admin:write"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:read"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "admin")
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "OK")
}
func TestMiddleware_InvalidPermission(t *testing.T) {
config := &Config{
RoleHeader: "X-User-Role",
Roles: []RoleDefinition{
{Name: "viewer", Permissions: []string{"users:read"}},
},
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"users:write"}},
},
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
require.NotNil(t, middlewareFunc)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK"))
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "viewer")
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
assert.Contains(t, w.Body.String(), "Forbidden: Access denied")
}
func TestExtractRole(t *testing.T) {
testCases := []struct {
desc string
config *Config
request *http.Request
expectedRole string
expectError bool
}{
{
desc: "extracts role from header",
config: &Config{
RoleHeader: "X-User-Role",
},
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "admin")
return req
}(),
expectedRole: "admin",
expectError: false,
},
{
desc: "extracts role from JWT when both configured",
config: &Config{
RoleHeader: "X-User-Role",
JWTClaimPath: "role",
},
request: func() *http.Request {
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "viewer")
claims := jwt.MapClaims{"role": "admin"}
ctx := context.WithValue(req.Context(), middleware.JWTClaim, claims)
return req.WithContext(ctx)
}(),
expectedRole: "admin",
expectError: false,
},
{
desc: "returns error when JWT configured but claims not found",
config: &Config{
JWTClaimPath: "role",
},
request: httptest.NewRequest(http.MethodGet, "/api", http.NoBody),
expectedRole: "",
expectError: true,
},
{
desc: "returns error when header configured but not present",
config: &Config{
RoleHeader: "X-User-Role",
},
request: httptest.NewRequest(http.MethodGet, "/api", http.NoBody),
expectedRole: "",
expectError: true,
},
{
desc: "returns error when no role extraction configured",
config: &Config{},
request: httptest.NewRequest(http.MethodGet, "/api", http.NoBody),
expectedRole: "",
expectError: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
role, err := extractRole(tc.request, tc.config)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Empty(t, role, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expectedRole, role, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractRoleFromJWT(t *testing.T) {
testCases := []struct {
desc string
claimPath string
claims jwt.MapClaims
expectedRole string
expectError bool
}{
{
desc: "extracts role from simple claim",
claimPath: "role",
claims: jwt.MapClaims{
"role": "admin",
},
expectedRole: "admin",
expectError: false,
},
{
desc: "extracts role from array claim",
claimPath: "roles[0]",
claims: jwt.MapClaims{
"roles": []any{"admin", "user"},
},
expectedRole: "admin",
expectError: false,
},
{
desc: "extracts role from nested claim",
claimPath: "permissions.role",
claims: jwt.MapClaims{
"permissions": map[string]any{
"role": "admin",
},
},
expectedRole: "admin",
expectError: false,
},
{
desc: "returns error when claims not in context",
claimPath: "role",
claims: nil,
expectedRole: "",
expectError: true,
},
{
desc: "returns error when claim path not found",
claimPath: "nonexistent",
claims: jwt.MapClaims{
"role": "admin",
},
expectedRole: "",
expectError: true,
},
{
desc: "converts non-string role to string",
claimPath: "role",
claims: jwt.MapClaims{
"role": 123,
},
expectedRole: "123",
expectError: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
if tc.claims != nil {
ctx := context.WithValue(req.Context(), middleware.JWTClaim, tc.claims)
req = req.WithContext(ctx)
}
role, err := extractRoleFromJWT(req, tc.claimPath)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Empty(t, role, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expectedRole, role, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractClaimValue(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "extracts simple claim",
claims: jwt.MapClaims{
"role": "admin",
},
path: "role",
expected: "admin",
expectError: false,
},
{
desc: "extracts array claim",
claims: jwt.MapClaims{
"roles": []any{"admin", "user"},
},
path: "roles[0]",
expected: "admin",
expectError: false,
},
{
desc: "extracts nested claim",
claims: jwt.MapClaims{
"permissions": map[string]any{
"role": "admin",
},
},
path: "permissions.role",
expected: "admin",
expectError: false,
},
{
desc: "returns error for empty path",
claims: jwt.MapClaims{
"role": "admin",
},
path: "",
expected: nil,
expectError: true,
},
{
desc: "returns error for non-existent claim",
claims: jwt.MapClaims{
"role": "admin",
},
path: "nonexistent",
expected: nil,
expectError: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result, err := extractClaimValue(tc.claims, tc.path)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Nil(t, result, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractArrayClaim_Basic(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "extracts first element from array",
claims: jwt.MapClaims{
"roles": []any{"admin", "user"},
},
path: "roles[0]",
expected: "admin",
expectError: false,
},
{
desc: "extracts second element from array",
claims: jwt.MapClaims{
"roles": []any{"admin", "user"},
},
path: "roles[1]",
expected: "user",
expectError: false,
},
{
desc: "handles array with mixed types",
claims: jwt.MapClaims{
"roles": []any{123, "admin", true},
},
path: "roles[0]",
expected: 123,
expectError: false,
},
}
runExtractArrayClaimTests(t, testCases)
}
func TestExtractArrayClaim_Errors(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "returns error for invalid array notation",
claims: jwt.MapClaims{
"roles": []any{"admin"},
},
path: "roles[",
expected: nil,
expectError: true,
},
{
desc: "returns error for non-existent key",
claims: jwt.MapClaims{
"other": []any{"value"},
},
path: "roles[0]",
expected: nil,
expectError: true,
},
{
desc: "returns error when value is not array",
claims: jwt.MapClaims{
"roles": "not an array",
},
path: "roles[0]",
expected: nil,
expectError: true,
},
{
desc: "returns error for out of bounds index",
claims: jwt.MapClaims{
"roles": []any{"admin"},
},
path: "roles[5]",
expected: nil,
expectError: true,
},
{
desc: "returns error for negative index",
claims: jwt.MapClaims{
"roles": []any{"admin"},
},
path: "roles[-1]",
expected: nil,
expectError: true,
},
{
desc: "handles empty array",
claims: jwt.MapClaims{
"roles": []any{},
},
path: "roles[0]",
expected: nil,
expectError: true,
},
{
desc: "handles nil value in claims",
claims: jwt.MapClaims{
"roles": nil,
},
path: "roles[0]",
expected: nil,
expectError: true,
},
}
runExtractArrayClaimTests(t, testCases)
}
func TestExtractArrayClaim_EdgeCases(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "handles array with nil elements",
claims: jwt.MapClaims{
"roles": []any{nil, "admin"},
},
path: "roles[0]",
expected: nil,
expectError: false,
},
}
runExtractArrayClaimTests(t, testCases)
}
func runExtractArrayClaimTests(t *testing.T, testCases []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}) {
t.Helper()
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
idx := 0
for j, c := range tc.path {
if c == '[' {
idx = j
break
}
}
result, err := extractArrayClaim(tc.claims, tc.path, idx)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Nil(t, result, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestExtractNestedClaim_Basic(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "extracts nested claim",
claims: jwt.MapClaims{
"permissions": map[string]any{
"role": "admin",
},
},
path: "permissions.role",
expected: "admin",
expectError: false,
},
{
desc: "extracts deeply nested claim",
claims: jwt.MapClaims{
"user": map[string]any{
"profile": map[string]any{
"role": "admin",
},
},
},
path: "user.profile.role",
expected: "admin",
expectError: false,
},
{
desc: "handles array in nested structure",
claims: jwt.MapClaims{
"data": map[string]any{
"roles": []any{"admin", "user"},
},
},
path: "data.roles",
expected: []any{"admin", "user"},
expectError: false,
},
}
runExtractNestedClaimTests(t, testCases)
}
func TestExtractNestedClaim_Errors(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "returns error for non-existent path",
claims: jwt.MapClaims{
"permissions": map[string]any{
"role": "admin",
},
},
path: "permissions.nonexistent",
expected: nil,
expectError: true,
},
{
desc: "returns error for invalid structure",
claims: jwt.MapClaims{
"permissions": "not a map",
},
path: "permissions.role",
expected: nil,
expectError: true,
},
{
desc: "returns error when intermediate path is nil",
claims: jwt.MapClaims{
"permissions": nil,
},
path: "permissions.role",
expected: nil,
expectError: true,
},
{
desc: "handles empty nested map",
claims: jwt.MapClaims{
"permissions": map[string]any{},
},
path: "permissions.role",
expected: nil,
expectError: true,
},
{
desc: "handles deeply nested nil intermediate value",
claims: jwt.MapClaims{
"user": map[string]any{
"profile": nil,
},
},
path: "user.profile.role",
expected: nil,
expectError: true,
},
}
runExtractNestedClaimTests(t, testCases)
}
func TestExtractNestedClaim_EdgeCases(t *testing.T) {
testCases := []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}{
{
desc: "handles nil value in nested map",
claims: jwt.MapClaims{
"permissions": map[string]any{
"role": nil,
},
},
path: "permissions.role",
expected: nil,
expectError: false,
},
}
runExtractNestedClaimTests(t, testCases)
}
func runExtractNestedClaimTests(t *testing.T, testCases []struct {
desc string
claims jwt.MapClaims
path string
expected any
expectError bool
}) {
t.Helper()
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
result, err := extractNestedClaim(tc.claims, tc.path)
if tc.expectError {
require.Error(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Nil(t, result, "TEST[%d], Failed.\n%s", i, tc.desc)
return
}
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Equal(t, tc.expected, result, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestLogAuditEvent(t *testing.T) {
testCases := []struct {
desc string
logger datasource.Logger
allowed bool
expected int
}{
{
desc: "logs allowed event",
logger: &mockLogger{logs: []string{}},
allowed: true,
expected: 1,
},
{
desc: "logs denied event",
logger: &mockLogger{logs: []string{}},
allowed: false,
expected: 1,
},
{
desc: "does not log when logger is nil",
logger: nil,
allowed: true,
expected: 0,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
logAuditEvent(tc.logger, req, "admin", "/api", tc.allowed)
if tc.logger != nil {
mockLog := tc.logger.(*mockLogger)
assert.GreaterOrEqual(t, len(mockLog.logs), tc.expected, "TEST[%d], Failed.\n%s", i, tc.desc)
}
})
}
}
func TestHandleAuthError(t *testing.T) {
testCases := []struct {
desc string
config *Config
err error
expectedStatus int
expectedBody string
customHandler bool
}{
{
desc: "handles ErrRoleNotFound with default handler",
config: &Config{
Logger: &mockLogger{logs: []string{}},
},
err: ErrRoleNotFound,
expectedStatus: http.StatusUnauthorized,
expectedBody: "Unauthorized: Missing or invalid role",
customHandler: false,
},
{
desc: "handles ErrAccessDenied with default handler",
config: &Config{
Logger: &mockLogger{logs: []string{}},
},
err: ErrAccessDenied,
expectedStatus: http.StatusForbidden,
expectedBody: "Forbidden: Access denied",
customHandler: false,
},
{
desc: "uses custom error handler when provided",
config: &Config{
Logger: &mockLogger{logs: []string{}},
ErrorHandler: func(w http.ResponseWriter, _ *http.Request, _, _ string, _ error) {
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte("Custom error"))
},
},
err: ErrAccessDenied,
expectedStatus: http.StatusTeapot,
expectedBody: "Custom error",
customHandler: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
handleAuthError(w, req, tc.config, "admin", "/api", tc.err)
assert.Equal(t, tc.expectedStatus, w.Code, "TEST[%d], Failed.\n%s", i, tc.desc)
assert.Contains(t, w.Body.String(), tc.expectedBody, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
// mockLogger implements the datasource.Logger interface for testing.
type mockLogger struct {
errorLogs []string
infoLogs []string // Capture actual log messages
logs []string
infoArgs []any // Capture structured log arguments (used for both Info and Debug)
}
func (m *mockLogger) Debug(args ...any) {
m.logs = append(m.logs, "DEBUG")
if len(args) > 0 {
m.infoArgs = append(m.infoArgs, args...)
}
}
func (m *mockLogger) Debugf(_ string, _ ...any) { m.logs = append(m.logs, "DEBUGF") }
func (m *mockLogger) Info(args ...any) {
m.logs = append(m.logs, "INFO")
if len(args) > 0 {
m.infoArgs = append(m.infoArgs, args...)
}
}
func (m *mockLogger) Infof(format string, args ...any) {
m.logs = append(m.logs, "INFOF")
m.infoLogs = append(m.infoLogs, fmt.Sprintf(format, args...))
}
func (m *mockLogger) Error(_ ...any) { m.logs = append(m.logs, "ERROR") }
func (m *mockLogger) Errorf(format string, args ...any) {
m.logs = append(m.logs, "ERRORF")
m.errorLogs = append(m.errorLogs, fmt.Sprintf(format, args...))
}
func (m *mockLogger) Warn(_ ...any) { m.logs = append(m.logs, "WARN") }
func (m *mockLogger) Warnf(_ string, _ ...any) { m.logs = append(m.logs, "WARNF") }
func TestMiddleware_WithTracing(t *testing.T) {
t.Run("starts tracing when tracer is available", func(t *testing.T) {
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:read"}},
},
RoleHeader: "X-User-Role",
Tracer: noop.NewTracerProvider().Tracer("test"),
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "admin")
// Setup role permissions
config.rolePermissionsMap = map[string][]string{
"admin": {"admin:read"},
}
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
})
}
func TestMiddleware_RoleInAuditLogs(t *testing.T) {
t.Run("role is included in audit logs", func(t *testing.T) {
mockLog := &mockLogger{
logs: []string{},
infoLogs: []string{},
infoArgs: []any{},
}
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:read"}},
},
RoleHeader: "X-User-Role",
Logger: mockLog,
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "admin")
// Setup role permissions
config.rolePermissionsMap = map[string][]string{
"admin": {"admin:read"},
}
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify audit log contains role
assert.NotEmpty(t, mockLog.logs, "audit log should be written")
// Verify that Debug was called (structured logging)
assert.Contains(t, mockLog.logs, "DEBUG", "audit log should be written via Debug")
// Verify structured log contains AuditLog
assert.NotEmpty(t, mockLog.infoArgs, "audit log struct should be captured")
auditLog, ok := mockLog.infoArgs[0].(*AuditLog)
require.True(t, ok, "audit log should be AuditLog struct")
assert.Equal(t, "admin", auditLog.Role, "audit log should contain role")
assert.Equal(t, "ACC", auditLog.Status, "audit log should have ACC status")
assert.Equal(t, "GET", auditLog.Method, "audit log should contain method")
assert.Equal(t, "/api", auditLog.Route, "audit log should contain route")
assert.NotEmpty(t, auditLog.CorrelationID, "audit log should contain correlation ID")
})
t.Run("role is included in audit logs for denied requests", func(t *testing.T) {
mockLog := &mockLogger{
logs: []string{},
infoLogs: []string{},
infoArgs: []any{},
}
config := &Config{
Endpoints: []EndpointMapping{
{Path: "/api", Methods: []string{"GET"}, RequiredPermissions: []string{"admin:read"}},
},
RoleHeader: "X-User-Role",
Logger: mockLog,
}
err := config.processUnifiedConfig()
require.NoError(t, err)
middlewareFunc := Middleware(config)
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
wrapped := middlewareFunc(handler)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api", http.NoBody)
req.Header.Set("X-User-Role", "viewer") // Role without permission
// Setup role permissions
config.rolePermissionsMap = map[string][]string{
"viewer": {"viewer:read"}, // Different permission
}
wrapped.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
// Verify audit log contains role
assert.NotEmpty(t, mockLog.logs, "audit log should be written")
// Verify that Debug was called (structured logging)
assert.Contains(t, mockLog.logs, "DEBUG", "audit log should be written via Debug")
// Verify structured log contains AuditLog
assert.NotEmpty(t, mockLog.infoArgs, "audit log struct should be captured")
auditLog, ok := mockLog.infoArgs[0].(*AuditLog)
require.True(t, ok, "audit log should be AuditLog struct")
assert.Equal(t, "viewer", auditLog.Role, "audit log should contain role")
assert.Equal(t, "REJ", auditLog.Status, "audit log should have REJ status")
assert.Equal(t, "GET", auditLog.Method, "audit log should contain method")
assert.Equal(t, "/api", auditLog.Route, "audit log should contain route")
})
}
func TestSanitizeErrorForTrace(t *testing.T) {
t.Run("sanitizes known errors", func(t *testing.T) {
err := ErrRoleNotFound
sanitized := sanitizeErrorForTrace(err)
assert.Equal(t, ErrRoleNotFound, sanitized, "known errors should be returned as-is")
})
t.Run("sanitizes access denied errors", func(t *testing.T) {
err := ErrAccessDenied
sanitized := sanitizeErrorForTrace(err)
assert.Equal(t, ErrAccessDenied, sanitized, "known errors should be returned as-is")
})
t.Run("sanitizes unknown errors", func(t *testing.T) {
//nolint:err113 // Test intentionally uses dynamic errors to verify sanitization
testErr := fmt.Errorf("internal system error: database connection failed at 192.168.1.1")
sanitized := sanitizeErrorForTrace(testErr)
// Unknown errors should be sanitized to generic message
assert.Equal(t, "authorization error", sanitized.Error(), "unknown errors should be sanitized")
assert.NotContains(t, sanitized.Error(), "192.168.1.1", "sensitive information should be removed")
assert.NotContains(t, sanitized.Error(), "database connection", "internal details should be removed")
})
t.Run("sanitizes wrapped errors", func(t *testing.T) {
//nolint:err113 // Test intentionally uses dynamic errors to verify sanitization
testErr := fmt.Errorf("internal system error: secret key exposed")
err := fmt.Errorf("wrapped error: %w", testErr)
sanitized := sanitizeErrorForTrace(err)
// Wrapped unknown errors should be sanitized
assert.Equal(t, "authorization error", sanitized.Error(), "wrapped unknown errors should be sanitized")
assert.NotContains(t, sanitized.Error(), "secret key", "sensitive information should be removed")
})
}
================================================
FILE: pkg/gofr/rbac.go
================================================
package gofr
import (
"go.opentelemetry.io/otel"
"gofr.dev/pkg/gofr/rbac"
)
// EnableRBAC enables RBAC by loading configuration from a JSON or YAML file.
// It loads the config directly and sets up the middleware.
//
// Pure config-based: All authorization rules are defined in the config file using:
// - Roles: role → permission mapping (format: "resource:action")
// - Endpoints: route & method → permission mapping
//
// Usage:
//
// // Use default paths (configs/rbac.json, configs/rbac.yaml, configs/rbac.yml)
// // Uses rbac.DefaultConfigPath internally
// app.EnableRBAC()
//
// // Or with custom config path
// app.EnableRBAC("configs/custom-rbac.json")
//
// Role extraction is configured in the config file:
// - Set "roleHeader" for header-based extraction (e.g., "X-User-Role")
// - Set "jwtClaimPath" for JWT-based extraction (e.g., "role", "roles[0]").
func (a *App) EnableRBAC(configPath ...string) {
var path string
if len(configPath) > 0 {
path = configPath[0]
} else {
// Use rbac.DefaultConfigPath (empty string) to trigger default path resolution
path = rbac.ResolveRBACConfigPath(rbac.DefaultConfigPath)
}
// Get dependencies
logger := a.Logger()
metrics := a.Metrics()
tracer := otel.GetTracerProvider().Tracer("gofr-rbac")
// Load configuration directly with dependencies
config, err := rbac.LoadPermissions(path, logger, metrics, tracer)
if err != nil {
a.Logger().Errorf("Failed to load RBAC config: %v", err)
return
}
a.Logger().Infof("Loaded RBAC config successfully")
// Apply middleware using the config
middlewareFunc := rbac.Middleware(config)
a.UseMiddleware(middlewareFunc)
}
================================================
FILE: pkg/gofr/rbac_test.go
================================================
package gofr
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/testutil"
)
func TestEnableRBAC(t *testing.T) {
testCases := []struct {
desc string
configPath string
setupFiles func() (string, error)
cleanupFiles func(string)
expectedLogs []string
expectedError bool
middlewareSet bool
}{
{
desc: "valid config with custom config file",
configPath: "test_rbac.json",
setupFiles: func() (string, error) {
content := `{"roles":[{"name":"admin","permissions":["admin:read"]}],` +
`"endpoints":[{"path":"/api","methods":["GET"],"requiredPermissions":["admin:read"]}]}`
return createTestConfigFile("test_rbac.json", content)
},
cleanupFiles: func(path string) {
os.Remove(path)
},
expectedLogs: []string{"Loaded RBAC config"},
expectedError: false,
middlewareSet: true,
},
{
desc: "config file not found",
configPath: "nonexistent.json",
setupFiles: func() (string, error) {
return "", nil
},
cleanupFiles: func(string) {},
expectedLogs: []string{"Failed to load RBAC config"},
expectedError: false,
middlewareSet: false,
},
{
desc: "invalid config file format",
configPath: "invalid.json",
setupFiles: func() (string, error) {
content := `invalid json content{`
return createTestConfigFile("invalid.json", content)
},
cleanupFiles: func(path string) {
os.Remove(path)
},
expectedLogs: []string{"Failed to load RBAC config"},
expectedError: false,
middlewareSet: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// Set up free ports for HTTP, Metrics, and gRPC
_ = testutil.NewServerConfigs(t)
filePath, err := tc.setupFiles()
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
defer tc.cleanupFiles(filePath)
app := New()
app.EnableRBAC(tc.configPath)
// Check that httpServer and router exist
require.NotNil(t, app.httpServer, "TEST[%d], Failed.\n%s", i, tc.desc)
require.NotNil(t, app.httpServer.router, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func TestEnableRBAC_WithDefaultConfigPath(t *testing.T) {
testCases := []struct {
desc string
setupFiles func() (string, error)
cleanupFiles func(string)
expectedLogs []string
expectedError bool
middlewareSet bool
}{
{
desc: "valid config with default config path (no args)",
setupFiles: func() (string, error) {
content := `{"roles":[{"name":"viewer","permissions":["users:read"]}],"endpoints":[{"path":"/health","methods":["GET"],"public":true}]}`
return createTestConfigFile("configs/rbac.json", content)
},
cleanupFiles: func(path string) {
os.Remove(path)
os.Remove("configs")
},
expectedLogs: []string{"Loaded RBAC config"},
expectedError: false,
middlewareSet: true,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// Set up free ports for HTTP, Metrics, and gRPC
_ = testutil.NewServerConfigs(t)
filePath, err := tc.setupFiles()
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
defer tc.cleanupFiles(filePath)
app := New()
app.EnableRBAC()
// Check that httpServer and router exist
require.NotNil(t, app.httpServer, "TEST[%d], Failed.\n%s", i, tc.desc)
require.NotNil(t, app.httpServer.router, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
func createTestConfigFile(filename, content string) (string, error) {
dir := filepath.Dir(filename)
if dir != "." && dir != "" {
err := os.MkdirAll(dir, 0755)
if err != nil {
return "", err
}
}
err := os.WriteFile(filename, []byte(content), 0600)
return filename, err
}
func TestApp_EnableRBAC_Integration(t *testing.T) {
testCases := []struct {
desc string
configContent string
configFile string
expectError bool
}{
{
desc: "valid config with roles and endpoints",
configContent: `{
"roles": [
{"name": "admin", "permissions": ["*:*"]},
{"name": "viewer", "permissions": ["users:read"]}
],
"endpoints": [
{"path": "/health", "methods": ["GET"], "public": true},
{"path": "/api/users", "methods": ["GET"], "requiredPermissions": ["users:read"]}
]
}`,
configFile: "test_integration.json",
expectError: false,
},
{
desc: "config with role inheritance",
configContent: `{
"roles": [
{"name": "viewer", "permissions": ["users:read"]},
{"name": "editor", "permissions": ["users:write"], "inheritsFrom": ["viewer"]}
],
"endpoints": [
{"path": "/api/users", "methods": ["GET"], "requiredPermissions": ["users:read"]}
]
}`,
configFile: "test_inheritance.json",
expectError: false,
},
}
for i, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
// Set up free ports for HTTP, Metrics, and gRPC
_ = testutil.NewServerConfigs(t)
path, err := createTestConfigFile(tc.configFile, tc.configContent)
require.NoError(t, err, "TEST[%d], Failed.\n%s", i, tc.desc)
defer os.Remove(path)
app := New()
app.EnableRBAC(tc.configFile)
require.NotNil(t, app.httpServer, "TEST[%d], Failed.\n%s", i, tc.desc)
require.NotNil(t, app.httpServer.router, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
}
================================================
FILE: pkg/gofr/request.go
================================================
package gofr
import (
"context"
)
// Request is an interface which is written because it allows us
// to create applications without being aware of the transport.
// In both cmd or server application, this abstraction can be used.
type Request interface {
Context() context.Context
Param(string) string
PathParam(string) string
Bind(any) error
HostName() string
Params(string) []string
}
================================================
FILE: pkg/gofr/responder.go
================================================
package gofr
// Responder is used by the application to provide output. This is implemented for both
// cmd and HTTP server application.
type Responder interface {
Respond(data any, err error)
}
// noopResponder is used by GraphQL resolvers. GraphQL reuses *gofr.Context (which
// requires a Responder) but handles its own response serialization — the resolver
// result is collected by the GraphQL engine and written as part of the unified
// GraphQL JSON response, not via the standard HTTP responder.
type noopResponder struct{}
func (noopResponder) Respond(_ any, _ error) {}
================================================
FILE: pkg/gofr/rest.go
================================================
package gofr
import (
"strconv"
"time"
)
// GET adds a Handler for HTTP GET method for a route pattern.
func (a *App) GET(pattern string, handler Handler) {
a.add("GET", pattern, handler)
}
// PUT adds a Handler for HTTP PUT method for a route pattern.
func (a *App) PUT(pattern string, handler Handler) {
a.add("PUT", pattern, handler)
}
// POST adds a Handler for HTTP POST method for a route pattern.
func (a *App) POST(pattern string, handler Handler) {
a.add("POST", pattern, handler)
}
// DELETE adds a Handler for HTTP DELETE method for a route pattern.
func (a *App) DELETE(pattern string, handler Handler) {
a.add("DELETE", pattern, handler)
}
// PATCH adds a Handler for HTTP PATCH method for a route pattern.
func (a *App) PATCH(pattern string, handler Handler) {
a.add("PATCH", pattern, handler)
}
func (a *App) add(method, pattern string, h Handler) {
if !a.httpRegistered && !isPortAvailable(a.httpServer.port) {
a.container.Logger.Fatalf("http port %d is blocked or unreachable", a.httpServer.port)
}
a.httpRegistered = true
reqTimeout, err := strconv.Atoi(a.Config.Get("REQUEST_TIMEOUT"))
if (err != nil && a.Config.Get("REQUEST_TIMEOUT") != "") || reqTimeout < 0 {
reqTimeout = 0
}
a.httpServer.router.Add(method, pattern, handler{
function: h,
container: a.container,
requestTimeout: time.Duration(reqTimeout) * time.Second,
})
}
// AddRESTHandlers creates and registers CRUD routes for the given struct, the struct should always be passed by reference.
func (a *App) AddRESTHandlers(object any) error {
cfg, err := scanEntity(object)
if err != nil {
a.container.Logger.Errorf(err.Error())
return err
}
a.registerCRUDHandlers(cfg, object)
return nil
}
================================================
FILE: pkg/gofr/run.go
================================================
package gofr
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
// Run starts the application. If it is an HTTP server, it will start the server.
func (a *App) Run() {
if a.cmd != nil {
a.cmd.Run(a.container)
}
// Create a context that is canceled on receiving termination signals
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()
if !a.handleStartupHooks(ctx) {
return
}
timeout, err := getShutdownTimeoutFromConfig(a.Config)
if err != nil {
a.Logger().Errorf("error parsing value of shutdown timeout from config: %v. Setting default timeout of 30 sec.", err)
}
a.startShutdownHandler(ctx, timeout)
a.startTelemetryIfEnabled()
a.startAllServers(ctx)
}
// handleStartupHooks runs the startup hooks and returns false if the application should exit.
func (a *App) handleStartupHooks(ctx context.Context) bool {
if err := a.runOnStartHooks(ctx); err != nil {
if !errors.Is(err, context.Canceled) {
a.Logger().Errorf("Startup failed: %v", err)
return false
}
// If the error is context.Canceled, do not exit; allow graceful shutdown.
a.Logger().Info("Startup canceled by context, shutting down gracefully.")
return false
}
return true
}
// startShutdownHandler starts a goroutine to handle graceful shutdown.
func (a *App) startShutdownHandler(ctx context.Context, timeout time.Duration) {
// Goroutine to handle shutdown when context is canceled
go func() {
<-ctx.Done()
// Create a shutdown context with a timeout
shutdownCtx, done := context.WithTimeout(context.WithoutCancel(ctx), timeout)
defer done()
if a.hasTelemetry() {
a.sendTelemetry(http.DefaultClient, false)
}
a.Logger().Infof("Shutting down server with a timeout of %v", timeout)
shutdownErr := a.Shutdown(shutdownCtx)
if shutdownErr != nil {
a.Logger().Debugf("Server shutdown failed: %v", shutdownErr)
}
}()
}
// startTelemetryIfEnabled starts telemetry if it's enabled.
func (a *App) startTelemetryIfEnabled() {
if a.hasTelemetry() {
go a.sendTelemetry(http.DefaultClient, true)
}
}
// startAllServers starts all registered servers concurrently.
func (a *App) startAllServers(ctx context.Context) {
wg := sync.WaitGroup{}
a.startMetricsServer(&wg)
a.startHTTPServer(&wg)
a.startGRPCServer(&wg)
a.startSubscriptionManager(ctx, &wg)
wg.Wait()
}
// startMetricsServer starts the metrics server if configured.
func (a *App) startMetricsServer(wg *sync.WaitGroup) {
// Start Metrics Server
// running metrics server before HTTP and gRPC
if a.metricServer != nil {
wg.Add(1)
go func(m *metricServer) {
defer wg.Done()
m.Run(a.container)
}(a.metricServer)
}
}
// startHTTPServer starts the HTTP server if registered.
func (a *App) startHTTPServer(wg *sync.WaitGroup) {
if a.httpRegistered {
wg.Add(1)
a.httpServerSetup()
go func(s *httpServer) {
defer wg.Done()
s.run(a.container)
}(a.httpServer)
}
}
// startGRPCServer starts the gRPC server if registered.
func (a *App) startGRPCServer(wg *sync.WaitGroup) {
if a.grpcRegistered {
wg.Add(1)
go func(s *grpcServer) {
defer wg.Done()
s.Run(a.container)
}(a.grpcServer)
}
}
// startSubscriptionManager starts the subscription manager.
func (a *App) startSubscriptionManager(ctx context.Context, wg *sync.WaitGroup) {
wg.Add(1)
go func() {
defer wg.Done()
err := a.startSubscriptions(ctx)
if err != nil {
a.Logger().Errorf("Subscription Error : %v", err)
}
}()
}
================================================
FILE: pkg/gofr/service/apikey_auth.go
================================================
// Package service provides an HTTP client with features for logging, metrics, and resilience.It supports various
// functionalities like health checks, circuit-breaker and various authentication.
package service
import (
"context"
"fmt"
"strings"
)
// #nosec G101
const xAPIKeyHeader = "X-Api-Key"
type APIKeyConfig struct {
APIKey string
}
func (a *APIKeyConfig) AddOption(h HTTP) HTTP {
return &authProvider{auth: a.addAuthorizationHeader, HTTP: h}
}
func NewAPIKeyConfig(apiKey string) (Options, error) {
apiKey = strings.TrimSpace(apiKey)
if apiKey == "" {
return nil, AuthErr{Message: "non empty api key is required"}
}
return &APIKeyConfig{APIKey: apiKey}, nil
}
func (a *APIKeyConfig) addAuthorizationHeader(_ context.Context, headers map[string]string) (map[string]string, error) {
if headers == nil {
headers = make(map[string]string)
}
if value, exists := headers[xAPIKeyHeader]; exists {
return headers, AuthErr{Message: fmt.Sprintf("value %v already exists for header %v", value, xAPIKeyHeader)}
}
headers[xAPIKeyHeader] = a.APIKey
return headers, nil
}
================================================
FILE: pkg/gofr/service/apikey_auth_test.go
================================================
package service
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
os.Setenv("GOFR_TELEMETRY", "false")
m.Run()
}
func TestNewAPIKeyConfig(t *testing.T) {
testCases := []struct {
apiKey string
apiKeyOption Options
err error
}{
{apiKey: "valid", apiKeyOption: &APIKeyConfig{APIKey: "valid"}},
{apiKey: " valid ", apiKeyOption: &APIKeyConfig{APIKey: "valid"}},
{apiKey: "", err: AuthErr{Message: "non empty api key is required"}},
{apiKey: " ", err: AuthErr{Message: "non empty api key is required"}},
}
for i, tc := range testCases {
options, err := NewAPIKeyConfig(tc.apiKey)
assert.Equal(t, tc.apiKeyOption, options, "failed test case #%d", i)
assert.Equal(t, tc.err, err, "failed test case #%d", i)
}
}
func TestAddAuthorizationHeader_APIKey(t *testing.T) {
testCases := []struct {
apiKey string
headers map[string]string
response map[string]string
err error
}{
{
apiKey: "valid",
response: map[string]string{xAPIKeyHeader: "valid"},
},
{
apiKey: "valid",
headers: map[string]string{xAPIKeyHeader: "existing-value"},
response: map[string]string{xAPIKeyHeader: "existing-value"},
err: AuthErr{Message: `value existing-value already exists for header X-Api-Key`},
},
{
apiKey: "valid",
headers: map[string]string{"header-key": "existing-value"},
response: map[string]string{"header-key": "existing-value", xAPIKeyHeader: "valid"},
},
}
for i, tc := range testCases {
config := APIKeyConfig{APIKey: tc.apiKey}
response, err := config.addAuthorizationHeader(t.Context(), tc.headers)
assert.Equal(t, tc.response, response, "failed test case #%d", i)
assert.Equal(t, tc.err, err, "failed test case #%d", i)
}
}
func setupAPIKeyAuthHTTPServer(t *testing.T, config *APIKeyConfig) *httptest.Server {
t.Helper()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode := http.StatusOK
if r.Header.Get(xAPIKeyHeader) != config.APIKey {
statusCode = http.StatusUnauthorized
}
w.WriteHeader(statusCode)
}))
t.Cleanup(func() {
server.Close()
})
return server
}
================================================
FILE: pkg/gofr/service/auth.go
================================================
package service
import (
"context"
"net/http"
)
const AuthHeader = "Authorization"
type authProvider struct {
auth func(context.Context, map[string]string) (map[string]string, error)
HTTP
}
func (a *authProvider) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
return a.GetWithHeaders(ctx, path, queryParams, nil)
}
func (a *authProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
headers, err := a.auth(ctx, headers)
if err != nil {
return nil, err
}
return a.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
}
func (a *authProvider) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return a.PostWithHeaders(ctx, path, queryParams, body, nil)
}
func (a *authProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
headers, err := a.auth(ctx, headers)
if err != nil {
return nil, err
}
return a.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *authProvider) Patch(ctx context.Context, path string, queryParams map[string]any, body []byte) (*http.Response, error) {
return a.PatchWithHeaders(ctx, path, queryParams, body, nil)
}
func (a *authProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
headers, err := a.auth(ctx, headers)
if err != nil {
return nil, err
}
return a.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *authProvider) Put(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return a.PutWithHeaders(ctx, path, queryParams, body, nil)
}
func (a *authProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
headers, err := a.auth(ctx, headers)
if err != nil {
return nil, err
}
return a.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *authProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
return a.DeleteWithHeaders(ctx, path, body, nil)
}
func (a *authProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (*http.Response, error) {
headers, err := a.auth(ctx, headers)
if err != nil {
return nil, err
}
return a.HTTP.DeleteWithHeaders(ctx, path, body, headers)
}
================================================
FILE: pkg/gofr/service/auth_test.go
================================================
package service
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"golang.org/x/oauth2"
"gofr.dev/pkg/gofr/logging"
)
func TestAuthProvider(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
validBasicAuthConfig := &BasicAuthConfig{UserName: "username", Password: "password"}
basicAuthServer := setupBasicAuthHTTPServer(t, validBasicAuthConfig)
invalidBasicAuthConfig := &BasicAuthConfig{UserName: "username", Password: "wrong-password"}
validOAuthConfig := oAuthConfigForTests(t, "")
oAuthServer := setupOAuthHTTPServer(t, validOAuthConfig)
validOAuthConfig.TokenURL = oAuthServer.URL + "/token"
invalidOAuthConfig := oAuthConfigForTests(t, oAuthServer.URL+"/token")
invalidOAuthConfig2 := oAuthConfigForTests(t, "")
invalidOAuthConfig3 := oAuthConfigForTests(t, invalidURL)
validAPIKeyConfig := &APIKeyConfig{"valid-value"}
apiKeyAuthServer := setupAPIKeyAuthHTTPServer(t, validAPIKeyConfig)
invalidAPIKeyConfig := &APIKeyConfig{"invalid-value"}
authHeaderExistsErr := AuthErr{Message: "value auth-string already exists for header " + AuthHeader}
apiHeaderExistsErr := AuthErr{Message: "value auth-string already exists for header " + xAPIKeyHeader}
testCases := []struct {
authOption Options
headers map[string]string
statusCode int
err error
}{
{authOption: validBasicAuthConfig, statusCode: http.StatusOK},
{authOption: invalidBasicAuthConfig, statusCode: http.StatusUnauthorized},
{authOption: validOAuthConfig, headers: map[string]string{AuthHeader: "auth-string"}, err: authHeaderExistsErr},
{authOption: validAPIKeyConfig, statusCode: http.StatusOK},
{authOption: invalidAPIKeyConfig, statusCode: http.StatusUnauthorized},
{authOption: validAPIKeyConfig, headers: map[string]string{xAPIKeyHeader: "auth-string"}, err: apiHeaderExistsErr},
{authOption: validOAuthConfig, statusCode: http.StatusOK},
{authOption: invalidOAuthConfig, statusCode: http.StatusUnauthorized, err: errInvalidCredentials},
{authOption: invalidOAuthConfig2, statusCode: http.StatusUnauthorized, err: errMissingTokenURL},
{authOption: invalidOAuthConfig3, statusCode: http.StatusUnauthorized, err: errIncorrectProtocol},
{authOption: validOAuthConfig, headers: map[string]string{AuthHeader: "auth-string"}, err: authHeaderExistsErr},
}
httpMethods := []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test Case #%d", i), func(t *testing.T) {
var server *httptest.Server
switch tc.authOption.(type) {
case *OAuthConfig:
server = oAuthServer
case *BasicAuthConfig:
server = basicAuthServer
case *APIKeyConfig:
server = apiKeyAuthServer
}
httpService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil, tc.authOption)
for _, method := range httpMethods {
resp, err := callHTTPService(t.Context(), httpService, method, tc.headers)
validateOAuthError(t, err, tc.err, tc.statusCode)
if err != nil {
return
}
assert.Equal(t, tc.statusCode, resp.StatusCode)
err = resp.Body.Close()
if err != nil {
t.Errorf("error closing response body %v", err)
}
}
})
}
}
func validateOAuthError(t *testing.T, err, expectedError error, statusCode int) {
t.Helper()
retrieveError := &oauth2.RetrieveError{}
URLError := &url.Error{}
authErr := AuthErr{}
if errors.As(err, &retrieveError) {
assert.Equal(t, statusCode, retrieveError.Response.StatusCode)
} else if errors.As(err, &URLError) {
assert.Equal(t, expectedError, URLError.Err)
} else if errors.As(err, &authErr) {
assert.Equal(t, expectedError, err)
} else if err != nil {
t.Errorf("Unknown error type encountered %v", err)
}
}
func callHTTPService(ctx context.Context, service HTTP, method string,
headers map[string]string) (resp *http.Response, err error) {
if headers != nil {
resp, err = callHTTPServiceWithHeaders(ctx, service, method, headers)
} else {
resp, err = callHTTPServiceWithoutHeaders(ctx, service, method)
}
return resp, err
}
func callHTTPServiceWithHeaders(ctx context.Context, service HTTP, method string,
headers map[string]string) (resp *http.Response, err error) {
path := "test"
queryParams := map[string]any{"key": "value"}
body := []byte("body")
switch method {
case http.MethodGet:
return service.GetWithHeaders(ctx, path, queryParams, headers)
case http.MethodPost:
return service.PostWithHeaders(ctx, path, queryParams, body, headers)
case http.MethodPut:
return service.PutWithHeaders(ctx, path, queryParams, body, headers)
case http.MethodPatch:
return service.PatchWithHeaders(ctx, path, queryParams, body, headers)
case http.MethodDelete:
return service.DeleteWithHeaders(ctx, path, body, headers)
default:
return nil, AuthErr{Message: "unknown method"}
}
}
func callHTTPServiceWithoutHeaders(ctx context.Context, service HTTP, method string) (resp *http.Response, err error) {
path := "test"
queryParams := map[string]any{"key": "value"}
body := []byte("body")
switch method {
case http.MethodGet:
return service.Get(ctx, path, queryParams)
case http.MethodPost:
return service.Post(ctx, path, queryParams, body)
case http.MethodPut:
return service.Put(ctx, path, queryParams, body)
case http.MethodPatch:
return service.Patch(ctx, path, queryParams, body)
case http.MethodDelete:
return service.Delete(ctx, path, body)
default:
return nil, AuthErr{Message: "unknown method"}
}
}
================================================
FILE: pkg/gofr/service/basic_auth.go
================================================
package service
import (
"context"
"encoding/base64"
"fmt"
"strings"
)
type BasicAuthConfig struct {
UserName string
Password string
}
func (c *BasicAuthConfig) AddOption(h HTTP) HTTP {
return &authProvider{auth: c.addAuthorizationHeader, HTTP: h}
}
func NewBasicAuthConfig(username, password string) (Options, error) {
username = strings.TrimSpace(username)
password = strings.TrimSpace(password)
if username == "" {
return nil, AuthErr{Message: "username is required"}
}
if password == "" {
return nil, AuthErr{Message: "password is required"}
}
decodedPassword, err := base64.StdEncoding.DecodeString(password)
if err != nil || string(decodedPassword) == password {
return nil, AuthErr{Err: err, Message: "password should be base64 encoded"}
}
return &BasicAuthConfig{username, string(decodedPassword)}, nil
}
func (c *BasicAuthConfig) addAuthorizationHeader(_ context.Context, headers map[string]string) (map[string]string, error) {
if headers == nil {
headers = make(map[string]string)
}
if value, exists := headers[AuthHeader]; exists {
return headers, AuthErr{Message: fmt.Sprintf("value %v already exists for header %v", value, AuthHeader)}
}
encodedAuth := base64.StdEncoding.EncodeToString([]byte(c.UserName + ":" + c.Password))
headers[AuthHeader] = "Basic " + encodedAuth
return headers, nil
}
================================================
FILE: pkg/gofr/service/basic_auth_test.go
================================================
package service
import (
"encoding/base64"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNewBasicAuthConfig(t *testing.T) {
badPasswordErr := AuthErr{Err: base64.CorruptInputError(12), Message: "password should be base64 encoded"}
testCases := []struct {
username string
password string
option Options
err error
}{
{username: "value", password: "", option: nil, err: AuthErr{Message: "password is required"}},
{username: "", password: "", option: nil, err: AuthErr{Message: "username is required"}},
{username: " ", password: "", option: nil, err: AuthErr{Message: "username is required"}},
{username: "value", password: "cGFzc3dvcmQ===", option: nil, err: badPasswordErr},
{username: "value", password: "cGFzc3dvcmQ=", option: &BasicAuthConfig{"value", "password"}, err: nil},
{username: " value ", password: "cGFzc3dvcmQ=", option: &BasicAuthConfig{"value", "password"}, err: nil},
{username: " value ", password: " cGFzc3dvcmQ=", option: &BasicAuthConfig{"value", "password"}, err: nil},
}
for i, tc := range testCases {
result, err := NewBasicAuthConfig(tc.username, tc.password)
assert.Equal(t, tc.option, result, "failed test case #%d", i)
assert.Equal(t, tc.err, err, "failed test case #%d", i)
}
}
func TestAddAuthorizationHeader_BasicAuth(t *testing.T) {
testCases := []struct {
username string
password string
headers map[string]string
response map[string]string
err error
}{
{
username: "username",
password: "cGFzc3dvcmQ=",
headers: nil,
response: map[string]string{AuthHeader: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
},
{
username: "username",
password: "cGFzc3dvcmQ=",
headers: map[string]string{AuthHeader: "existing value"},
response: map[string]string{AuthHeader: "existing value"},
err: AuthErr{Message: "value existing value already exists for header Authorization"},
},
{
username: "username",
password: "cGFzc3dvcmQ=",
headers: map[string]string{"header-key": "existing-value"},
response: map[string]string{"header-key": "existing-value", AuthHeader: "Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
err: nil,
},
}
for i, tc := range testCases {
config, err := NewBasicAuthConfig(tc.username, tc.password)
if err != nil {
t.Fatalf("unable to get basicAuthConfig for test case #%d", i)
}
basicAuthConfig, ok := config.(*BasicAuthConfig)
if !ok {
t.Fatalf("unable to get basicAuthConfig for test case #%d", i)
}
response, err := basicAuthConfig.addAuthorizationHeader(t.Context(), tc.headers)
assert.Equal(t, tc.response, response, "failed test case #%d", i)
assert.Equal(t, tc.err, err, "failed test case #%d", i)
}
}
func setupBasicAuthHTTPServer(t *testing.T, config *BasicAuthConfig) *httptest.Server {
t.Helper()
validHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(config.UserName+":"+config.Password))
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
statusCode := http.StatusOK
if r.Header.Get(AuthHeader) != validHeader {
statusCode = http.StatusUnauthorized
}
w.WriteHeader(statusCode)
}))
t.Cleanup(func() {
server.Close()
})
return server
}
func checkAuthHeaders(t *testing.T, r *http.Request) {
t.Helper()
authHeader := r.Header.Get(AuthHeader)
if authHeader == "" {
return
}
authParts := strings.Split(authHeader, " ")
payload, _ := base64.StdEncoding.DecodeString(authParts[1])
credentials := strings.Split(string(payload), ":")
assert.Equal(t, "user", credentials[0])
assert.Equal(t, "password", credentials[1])
}
================================================
FILE: pkg/gofr/service/circuit_breaker.go
================================================
package service
import (
"context"
"errors"
"net/http"
"sync"
"time"
)
// circuitBreaker states.
const (
ClosedState = iota
OpenState
)
var (
// ErrCircuitOpen indicates that the circuit breaker is open.
ErrCircuitOpen = errors.New("unable to connect to server at host")
ErrUnexpectedCircuitBreakerResultType = errors.New("unexpected result type from circuit breaker")
)
// CircuitBreakerConfig holds the configuration for the circuitBreaker.
type CircuitBreakerConfig struct {
Threshold int // Threshold represents the max no of retry before switching the circuit breaker state.
Interval time.Duration // Interval represents the time interval duration between hitting the HealthURL
}
// circuitBreaker represents a circuit breaker implementation.
type circuitBreaker struct {
mu sync.RWMutex
state int // ClosedState or OpenState
failureCount int
threshold int
interval time.Duration
lastChecked time.Time
metrics Metrics
serviceName string
HTTP
}
// NewCircuitBreaker creates a new circuitBreaker instance based on the provided config.
//
//nolint:revive // Allow returning unexported types as intended.
func NewCircuitBreaker(config CircuitBreakerConfig, h HTTP) *circuitBreaker {
cb := &circuitBreaker{
state: ClosedState,
threshold: config.Threshold,
interval: config.Interval,
HTTP: h,
}
// Perform asynchronous health checks
go cb.startHealthChecks()
return cb
}
// executeWithCircuitBreaker executes the given function with circuit breaker protection.
func (cb *circuitBreaker) executeWithCircuitBreaker(ctx context.Context, f func(ctx context.Context) (*http.Response,
error)) (*http.Response, error) {
cb.mu.RLock()
isOpen := cb.state == OpenState
cb.mu.RUnlock()
if isOpen {
// Circuit is open - try recovery without holding lock
if !cb.tryCircuitRecovery() {
return nil, ErrCircuitOpen
}
// Circuit recovered, proceed with request
}
result, err := f(ctx)
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil || (result != nil && result.StatusCode > 500) {
cb.handleFailure()
if cb.state == OpenState {
return nil, ErrCircuitOpen
}
} else {
cb.resetFailureCount()
}
return result, err
}
// isOpen returns true if the circuit breaker is in the open state.
func (cb *circuitBreaker) isOpen() bool {
cb.mu.Lock()
defer cb.mu.Unlock()
return cb.state == OpenState
}
func (cb *circuitBreaker) healthCheck(ctx context.Context) bool {
if httpSvc := extractHTTPService(cb.HTTP); httpSvc != nil && httpSvc.healthEndpoint != "" {
resp := cb.HTTP.getHealthResponseForEndpoint(ctx, httpSvc.healthEndpoint, httpSvc.healthTimeout)
return resp.Status == serviceUp
}
resp := cb.HTTP.HealthCheck(ctx)
return resp.Status == serviceUp
}
// startHealthChecks initiates periodic health checks.
func (cb *circuitBreaker) startHealthChecks() {
ticker := time.NewTicker(cb.interval)
for range ticker.C {
if cb.isOpen() {
go func() {
if cb.healthCheck(context.TODO()) {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.resetCircuit()
}
}()
}
}
}
// openCircuit transitions the circuit breaker to the open state.
func (cb *circuitBreaker) openCircuit() {
cb.state = OpenState
cb.lastChecked = time.Now()
if cb.metrics != nil {
cb.metrics.SetGauge("app_http_circuit_breaker_state", 1, "service", cb.serviceName)
}
}
// resetCircuit transitions the circuit breaker to the closed state.
func (cb *circuitBreaker) resetCircuit() {
cb.state = ClosedState
cb.failureCount = 0
if cb.metrics != nil {
cb.metrics.SetGauge("app_http_circuit_breaker_state", 0, "service", cb.serviceName)
}
}
// handleFailure increments the failure count and opens the circuit if the threshold is reached.
func (cb *circuitBreaker) handleFailure() {
cb.failureCount++
if cb.failureCount > cb.threshold {
cb.openCircuit()
}
}
// resetFailureCount resets the failure count to zero.
func (cb *circuitBreaker) resetFailureCount() {
cb.failureCount = 0
}
func (cb *CircuitBreakerConfig) AddOption(h HTTP) HTTP {
circuitBreaker := NewCircuitBreaker(*cb, h)
if httpSvc := extractHTTPService(h); httpSvc != nil {
circuitBreaker.metrics = httpSvc.Metrics
circuitBreaker.serviceName = httpSvc.name
if circuitBreaker.metrics != nil {
// Initialize the gauge to 0 (Closed) - gauge is already registered in container.go
circuitBreaker.metrics.SetGauge("app_http_circuit_breaker_state", 0, "service", circuitBreaker.serviceName)
}
}
return circuitBreaker
}
func (cb *circuitBreaker) tryCircuitRecovery() bool {
cb.mu.Lock()
defer cb.mu.Unlock()
if cb.state == ClosedState {
return true
}
if time.Since(cb.lastChecked) > cb.interval {
// Update lastChecked to prevent busy loop of health checks from other requests
cb.lastChecked = time.Now()
if cb.healthCheck(context.TODO()) {
cb.resetCircuit()
return true
}
}
return false
}
func (*circuitBreaker) handleCircuitBreakerResult(result any, err error) (*http.Response, error) {
if err != nil {
return nil, err
}
response, ok := result.(*http.Response)
if !ok {
return nil, ErrUnexpectedCircuitBreakerResultType
}
return response, nil
}
func (cb *circuitBreaker) doRequest(ctx context.Context, method, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
if cb.isOpen() {
if !cb.tryCircuitRecovery() {
return nil, ErrCircuitOpen
}
}
var result any
var err error
switch method {
case http.MethodGet:
result, err = cb.executeWithCircuitBreaker(ctx, func(ctx context.Context) (*http.Response, error) {
return cb.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
})
case http.MethodPost:
result, err = cb.executeWithCircuitBreaker(ctx, func(ctx context.Context) (*http.Response, error) {
return cb.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
})
case http.MethodPatch:
result, err = cb.executeWithCircuitBreaker(ctx, func(ctx context.Context) (*http.Response, error) {
return cb.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
})
case http.MethodPut:
result, err = cb.executeWithCircuitBreaker(ctx, func(ctx context.Context) (*http.Response, error) {
return cb.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
})
case http.MethodDelete:
result, err = cb.executeWithCircuitBreaker(ctx, func(ctx context.Context) (*http.Response, error) {
return cb.HTTP.DeleteWithHeaders(ctx, path, body, headers)
})
}
resp, err := cb.handleCircuitBreakerResult(result, err)
if err != nil {
return nil, err
}
return resp, err
}
func (cb *circuitBreaker) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodGet, path, queryParams, nil, headers)
}
// PostWithHeaders is a wrapper for doRequest with the POST method and headers.
func (cb *circuitBreaker) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPost, path, queryParams, body, headers)
}
// PatchWithHeaders is a wrapper for doRequest with the PATCH method and headers.
func (cb *circuitBreaker) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPatch, path, queryParams, body, headers)
}
// PutWithHeaders is a wrapper for doRequest with the PUT method and headers.
func (cb *circuitBreaker) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPut, path, queryParams, body, headers)
}
// DeleteWithHeaders is a wrapper for doRequest with the DELETE method and headers.
func (cb *circuitBreaker) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (
*http.Response, error) {
return cb.doRequest(ctx, http.MethodDelete, path, nil, body, headers)
}
func (cb *circuitBreaker) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodGet, path, queryParams, nil, nil)
}
// Post is a wrapper for doRequest with the POST method and headers.
func (cb *circuitBreaker) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPost, path, queryParams, body, nil)
}
// Patch is a wrapper for doRequest with the PATCH method and headers.
func (cb *circuitBreaker) Patch(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPatch, path, queryParams, body, nil)
}
// Put is a wrapper for doRequest with the PUT method and headers.
func (cb *circuitBreaker) Put(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return cb.doRequest(ctx, http.MethodPut, path, queryParams, body, nil)
}
// Delete is a wrapper for doRequest with the DELETE method and headers.
func (cb *circuitBreaker) Delete(ctx context.Context, path string, body []byte) (
*http.Response, error) {
return cb.doRequest(ctx, http.MethodDelete, path, nil, body, nil)
}
================================================
FILE: pkg/gofr/service/circuit_breaker_test.go
================================================
package service
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func testServer() *httptest.Server {
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
return httptest.NewServer(h)
}
func setupHTTPServiceTestServerForCircuitBreaker(t *testing.T) (*httptest.Server, HTTP) {
t.Helper()
// Start a test HTTP server
server := testServer()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// Initialize HTTP service with custom transport, URL, tracer, logger, and metrics
service := httpService{
Client: &http.Client{Transport: &customTransport{}},
url: server.URL,
name: "test-service",
Tracer: otel.Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.DEBUG),
Metrics: mockMetric,
}
// Circuit breaker configuration
cbConfig := CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
}
// Apply circuit breaker option to the HTTP service
httpservice := cbConfig.AddOption(&service)
return server, httpservice
}
func TestHttpService_GetSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.Get(t.Context(), "test", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_GetWithHeaderSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.GetWithHeaders(t.Context(), "test", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_GetCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.Get(t.Context(), tc.path, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_GetWithHeaderCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.GetWithHeaders(t.Context(), tc.path, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PutSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.Put(t.Context(), "test", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PutWithHeaderSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.PutWithHeaders(t.Context(), "test", nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PutCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.Put(t.Context(), tc.path, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PutWithHeaderCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.PutWithHeaders(t.Context(), tc.path, nil, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PatchSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.Get(t.Context(), "test", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PatchWithHeaderSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.GetWithHeaders(t.Context(), "test", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PatchCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.Patch(t.Context(), tc.path, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PatchWithHeaderCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.PatchWithHeaders(t.Context(), tc.path, nil, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PostSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.Post(t.Context(), "test", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PostWithHeaderSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.PostWithHeaders(t.Context(), "test", nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_PostCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.Post(t.Context(), tc.path, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_PostWithHeaderCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.PostWithHeaders(t.Context(), tc.path, nil, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_DeleteSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.Delete(t.Context(), "test", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_DeleteWithHeaderSuccessRequests(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric, &CircuitBreakerConfig{
Threshold: 1,
Interval: 1,
})
resp, err := service.DeleteWithHeaders(t.Context(), "test", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHttpService_DeleteCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.Delete(t.Context(), tc.path, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestHttpService_DeleteWithHeaderCBOpenRequests(t *testing.T) {
server, service := setupHTTPServiceTestServerForCircuitBreaker(t)
defer server.Close()
// Test cases
testCases := []struct {
name string
path string
expectErr bool
expectResp *http.Response
}{
{"Request will Fail", "invalid", true, nil},
{"Request will Fail", "invalid", true, nil},
{"Request will pass", "success", false, &http.Response{}},
}
// Perform test cases
for _, tc := range testCases {
resp, err := service.DeleteWithHeaders(t.Context(), tc.path, nil, nil)
if tc.expectErr {
require.Error(t, err)
assert.Nil(t, resp)
} else {
require.NoError(t, err)
assert.NotNil(t, resp)
_ = resp.Body.Close()
}
}
}
func TestCircuitBreaker_Metrics(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge("app_http_circuit_breaker_state", 1.0, "service", "test-service").MinTimes(1)
mockMetric.EXPECT().SetGauge("app_http_circuit_breaker_state", 0.0, "service", "test-service").AnyTimes()
service := httpService{
Client: &http.Client{Transport: &customTransport{}},
url: server.URL,
name: "test-service",
Tracer: otel.Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.DEBUG),
Metrics: mockMetric,
}
cbConfig := CircuitBreakerConfig{
Threshold: 1,
Interval: 1 * time.Second,
}
httpServiceWithCB := cbConfig.AddOption(&service)
// Trigger failures to open circuit
for i := 0; i < 3; i++ {
resp, _ := httpServiceWithCB.Get(t.Context(), "invalid", nil)
if resp != nil && resp.Body != nil {
_ = resp.Body.Close()
}
}
}
func TestCircuitBreaker_HTTP500_TripsCircuit(t *testing.T) {
server := testServer()
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
// Expect metrics to be recorded
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
service := httpService{
Client: &http.Client{Transport: &customTransport{}},
url: server.URL,
name: "test-service",
Tracer: otel.Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.DEBUG),
Metrics: mockMetric,
}
// Threshold 1, Long Interval
cbConfig := CircuitBreakerConfig{
Threshold: 1,
Interval: 1 * time.Minute,
}
httpServiceWithCB := cbConfig.AddOption(&service)
// 1. First call returns 500. Failure count becomes 1.
resp, err := httpServiceWithCB.Get(t.Context(), "error-500", nil)
require.NoError(t, err) // 500 is not an error returned by Get
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
resp.Body.Close()
// 2. Second call returns 500. Failure count becomes 2. Threshold (1) exceeded. Circuit Opens immediately.
// The request is executed, but the CB sees the failure count > threshold and returns ErrCircuitOpen.
resp, err = httpServiceWithCB.Get(t.Context(), "error-500", nil)
if resp != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
assert.Nil(t, resp)
// 3. Third call should also fail with ErrCircuitOpen (Circuit is Open)
resp, err = httpServiceWithCB.Get(t.Context(), "error-500", nil)
if resp != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
assert.Nil(t, resp)
}
type customTransport struct {
}
func (*customTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if r.URL.Path == "/.well-known/alive" || r.URL.Path == "/success" {
return &http.Response{
Body: io.NopCloser(bytes.NewBufferString("Hello World")),
StatusCode: http.StatusOK,
Request: r,
}, nil
}
if r.URL.Path == "/error-500" {
return &http.Response{
Body: io.NopCloser(bytes.NewBufferString("Internal Server Error")),
StatusCode: http.StatusServiceUnavailable,
Request: r,
}, nil
}
return nil, testutil.CustomError{ErrorMessage: "cb error"}
}
func TestCircuitBreaker_CustomHealthEndpoint_Recovery(t *testing.T) {
// Server that returns 502 for /fail (triggers circuit breaker), 200 for /health and /success
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health":
w.WriteHeader(http.StatusOK)
case "/fail":
w.WriteHeader(http.StatusBadGateway)
default:
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
httpSvc := NewHTTPService(server.URL,
logging.NewMockLogger(logging.DEBUG),
mockMetric,
&CircuitBreakerConfig{Threshold: 1, Interval: 200 * time.Millisecond},
&HealthConfig{HealthEndpoint: "health", Timeout: 5},
)
// First request returns 502 - failure count becomes 1, threshold not exceeded (1 > 1 is false)
resp, err := httpSvc.Get(t.Context(), "fail", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
resp.Body.Close()
// Second request returns 502 - failure count becomes 2, exceeds threshold (2 > 1)
// Circuit opens and returns ErrCircuitOpen
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Third request - circuit is still open, returns ErrCircuitOpen
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Wait for interval to pass so circuit can attempt recovery via /health endpoint
time.Sleep(1 * time.Second)
// Fourth request - circuit should recover via /health endpoint and succeed
resp, err = httpSvc.Get(t.Context(), "success", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
}
func TestCircuitBreaker_DefaultHealthEndpoint_NoRecoveryWhenMissing(t *testing.T) {
// Server that returns 502 for /fail (triggers circuit breaker),
// 404 for /.well-known/alive (default health endpoint missing)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/alive":
w.WriteHeader(http.StatusNotFound) // Default health endpoint not available
case "/fail":
w.WriteHeader(http.StatusBadGateway)
default:
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
// No HealthConfig - will use default /.well-known/alive which returns 404
httpSvc := NewHTTPService(server.URL,
logging.NewMockLogger(logging.DEBUG),
mockMetric,
&CircuitBreakerConfig{Threshold: 1, Interval: 200 * time.Millisecond},
)
// First request returns 502 - failure count becomes 1, threshold not exceeded
resp, err := httpSvc.Get(t.Context(), "fail", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
resp.Body.Close()
// Second request returns 502 - failure count becomes 2, exceeds threshold
// Circuit opens and returns ErrCircuitOpen
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Third request - circuit is still open
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Wait for interval to pass
time.Sleep(500 * time.Millisecond)
// Fourth request should also fail - circuit cannot recover because /.well-known/alive returns 404
resp, err = httpSvc.Get(t.Context(), "success", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
}
func TestCircuitBreaker_HealthEndpointWithTimeout(t *testing.T) {
// Server that returns 502 for /fail, 200 for /health and other paths
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health":
w.WriteHeader(http.StatusOK)
case "/fail":
w.WriteHeader(http.StatusBadGateway)
default:
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
httpSvc := NewHTTPService(server.URL,
logging.NewMockLogger(logging.DEBUG),
mockMetric,
&CircuitBreakerConfig{Threshold: 1, Interval: 200 * time.Millisecond},
&HealthConfig{HealthEndpoint: "health", Timeout: 10},
)
// First request returns 502 - failure count becomes 1, threshold not exceeded
resp, err := httpSvc.Get(t.Context(), "fail", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
resp.Body.Close()
// Second request returns 502 - failure count becomes 2, exceeds threshold
// Circuit opens and returns ErrCircuitOpen
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Third request - circuit is still open
resp, err = httpSvc.Get(t.Context(), "fail", nil)
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
require.ErrorIs(t, err, ErrCircuitOpen)
// Wait for interval to pass so circuit can attempt recovery
time.Sleep(500 * time.Millisecond)
// Fourth request - circuit should recover using custom health endpoint
resp, err = httpSvc.Get(t.Context(), "success", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
}
// TestCircuitBreaker_ParallelExecution tests that requests execute in parallel.
func TestCircuitBreaker_ParallelExecution(t *testing.T) {
requestCount := 0
mu := sync.Mutex{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
mu.Lock()
requestCount++
mu.Unlock()
time.Sleep(1 * time.Second) // Simulate slow endpoint
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status": "ok"}`))
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
httpSvc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{
Threshold: 10,
Interval: 5 * time.Second,
})
startTime := time.Now()
var wg sync.WaitGroup
numRequests := 5
errors := make([]error, numRequests)
// Launch 5 concurrent requests
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
resp, err := httpSvc.Get(t.Context(), "test", nil)
errors[index] = err
if err == nil && resp != nil {
_, _ = io.ReadAll(resp.Body)
_ = resp.Body.Close()
}
}(i)
}
wg.Wait()
totalTime := time.Since(startTime)
// Verify all requests completed successfully
for i := 0; i < numRequests; i++ {
require.NoError(t, errors[i], "Request %d should not error", i)
}
// All 5 requests should complete in ~2s (parallel)
assert.Less(t, totalTime, 4*time.Second, "Requests should execute in parallel")
assert.Equal(t, numRequests, requestCount, "All requests should have been processed")
}
// TestCircuitBreaker_ConcurrentFailures tests thread safety during concurrent failures.
func TestCircuitBreaker_ConcurrentFailures(t *testing.T) {
failCount := 0
mu := sync.Mutex{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
mu.Lock()
failCount++
current := failCount
mu.Unlock()
// First 3 requests fail, rest succeed
if current <= 3 {
w.WriteHeader(http.StatusServiceUnavailable)
} else {
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
httpSvc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{
Threshold: 2,
Interval: 1 * time.Second,
})
var wg sync.WaitGroup
numRequests := 10
for i := 0; i < numRequests; i++ {
wg.Add(1)
go func() {
defer wg.Done()
resp, _ := httpSvc.Get(t.Context(), "test", nil)
if resp != nil {
_ = resp.Body.Close()
}
}()
}
wg.Wait()
}
// TestCircuitBreaker_MixedHTTPMethods tests parallel requests with different HTTP methods.
func TestCircuitBreaker_MixedHTTPMethods(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
time.Sleep(1 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
httpSvc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{
Threshold: 5,
Interval: 2 * time.Second,
})
startTime := time.Now()
var wg sync.WaitGroup
// Test all HTTP methods in parallel
methods := []func() (*http.Response, error){
func() (*http.Response, error) { return httpSvc.Get(t.Context(), "test", nil) },
func() (*http.Response, error) { return httpSvc.Post(t.Context(), "test", nil, []byte(`{}`)) },
func() (*http.Response, error) { return httpSvc.Put(t.Context(), "test", nil, []byte(`{}`)) },
func() (*http.Response, error) { return httpSvc.Patch(t.Context(), "test", nil, []byte(`{}`)) },
func() (*http.Response, error) { return httpSvc.Delete(t.Context(), "test", []byte(`{}`)) },
}
for _, method := range methods {
wg.Add(1)
go func(fn func() (*http.Response, error)) {
defer wg.Done()
resp, err := fn()
if err == nil && resp != nil {
_ = resp.Body.Close()
}
}(method)
}
wg.Wait()
totalTime := time.Since(startTime)
// All 5 methods should complete in ~1s (parallel)
assert.Less(t, totalTime, 2*time.Second, "Different HTTP methods should execute in parallel")
}
================================================
FILE: pkg/gofr/service/connection_pool.go
================================================
package service
import (
"errors"
"fmt"
"net/http"
"time"
)
var (
errNegativeMaxIdleConns = errors.New("MaxIdleConns cannot be negative")
errNegativeMaxIdleConnsPerHost = errors.New("MaxIdleConnsPerHost cannot be negative")
errNegativeIdleConnTimeout = errors.New("IdleConnTimeout cannot be negative")
)
// ConnectionPoolConfig holds the configuration for HTTP connection pool settings.
// It customizes the HTTP transport layer to optimize connection reuse for high-frequency requests.
//
// Note: This configuration must be applied first when using multiple options with AddHTTPService,
// as it needs to access the underlying HTTP client transport. If applied after wrapper options
// (CircuitBreaker, Retry, OAuth), it will be silently ignored.
//
// Example:
//
// app.AddHTTPService("api-service", "https://api.example.com",
// &service.ConnectionPoolConfig{
// MaxIdleConns: 100,
// MaxIdleConnsPerHost: 20,
// IdleConnTimeout: 90 * time.Second,
// },
// &service.CircuitBreakerConfig{...}, // Other options after ConnectionPoolConfig
// )
type ConnectionPoolConfig struct {
// MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts.
// If not explicitly set (0), a default of 100 will be used.
// Negative values will cause validation error.
MaxIdleConns int
// MaxIdleConnsPerHost controls the maximum idle (keep-alive) connections to keep per-host.
// This is the critical setting for microservices making frequent requests to the same host.
// If set to 0, Go's DefaultMaxIdleConnsPerHost (2) will be used.
// Negative values will cause validation error.
// Default Go value: 2 (which is often insufficient for microservices)
// Recommended: 10-20 for typical microservices, higher for high-traffic services
MaxIdleConnsPerHost int
// IdleConnTimeout is the maximum amount of time an idle (keep-alive) connection will remain
// idle before closing itself.
// If not explicitly set (0), a default of 90 seconds will be used.
// Negative values will cause validation error.
IdleConnTimeout time.Duration
}
// Validate checks if the connection pool configuration values are valid.
func (c *ConnectionPoolConfig) Validate() error {
if c.MaxIdleConns < 0 {
return fmt.Errorf("%w, got: %d", errNegativeMaxIdleConns, c.MaxIdleConns)
}
if c.MaxIdleConnsPerHost < 0 {
return fmt.Errorf("%w, got: %d", errNegativeMaxIdleConnsPerHost, c.MaxIdleConnsPerHost)
}
if c.IdleConnTimeout < 0 {
return fmt.Errorf("%w, got: %v", errNegativeIdleConnTimeout, c.IdleConnTimeout)
}
return nil
}
// AddOption implements the Options interface to apply connection pool configuration to HTTP service.
// It modifies the underlying HTTP client's transport to use optimized connection pool settings.
func (c *ConnectionPoolConfig) AddOption(h HTTP) HTTP {
// Extract the base httpService from any wrapped service
httpSvc := extractHTTPService(h)
if httpSvc == nil {
// If we can't find the base service, return unchanged
// This maintains backward compatibility
return h
}
// Validate configuration before applying
if err := c.Validate(); err != nil {
return h
}
// Clone the default transport to preserve important settings like TLS timeouts and proxy configuration
transport := http.DefaultTransport.(*http.Transport).Clone()
// Apply connection pool settings with defaults
if c.MaxIdleConns > 0 {
transport.MaxIdleConns = c.MaxIdleConns
} else {
// Set a reasonable default if not specified
transport.MaxIdleConns = 100
}
if c.MaxIdleConnsPerHost > 0 {
transport.MaxIdleConnsPerHost = c.MaxIdleConnsPerHost
}
// Note: If MaxIdleConnsPerHost is 0, Go uses DefaultMaxIdleConnsPerHost (2)
if c.IdleConnTimeout > 0 {
transport.IdleConnTimeout = c.IdleConnTimeout
} else {
// Set a reasonable default if not specified
transport.IdleConnTimeout = 90 * time.Second
}
// Apply the custom transport to the HTTP client
httpSvc.Client.Transport = transport
return h
}
// extractHTTPService attempts to extract the base *httpService from a potentially wrapped HTTP service.
// It handles the common wrapper types used in the service package.
func extractHTTPService(h HTTP) *httpService {
switch v := h.(type) {
case *httpService:
return v
case *circuitBreaker:
return extractHTTPService(v.HTTP)
case *retryProvider:
return extractHTTPService(v.HTTP)
case *authProvider:
return extractHTTPService(v.HTTP)
case *customHealthService:
return extractHTTPService(v.HTTP)
case *rateLimiter:
return extractHTTPService(v.HTTP)
case *customHeader:
return extractHTTPService(v.HTTP)
default:
return nil
}
}
================================================
FILE: pkg/gofr/service/connection_pool_test.go
================================================
package service
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
)
func TestConnectionPoolConfig_AddOption_CustomSettings(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
mockHTTPService := &httpService{
Client: &http.Client{},
}
result := config.AddOption(mockHTTPService)
assert.Equal(t, mockHTTPService, result)
transport, ok := mockHTTPService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 100, transport.MaxIdleConns)
assert.Equal(t, 10, transport.MaxIdleConnsPerHost)
assert.Equal(t, 30*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_ZeroValuesUseDefaults(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 0,
MaxIdleConnsPerHost: 0,
IdleConnTimeout: 0,
}
mockHTTPService := &httpService{
Client: &http.Client{},
}
result := config.AddOption(mockHTTPService)
assert.Equal(t, mockHTTPService, result)
transport, _ := mockHTTPService.Client.Transport.(*http.Transport)
assert.Equal(t, 100, transport.MaxIdleConns)
assert.Equal(t, 90*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_PartialConfiguration(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConnsPerHost: 20,
}
mockHTTPService := &httpService{
Client: &http.Client{},
}
result := config.AddOption(mockHTTPService)
assert.Equal(t, mockHTTPService, result)
transport, _ := mockHTTPService.Client.Transport.(*http.Transport)
assert.Equal(t, 100, transport.MaxIdleConns)
assert.Equal(t, 20, transport.MaxIdleConnsPerHost)
assert.Equal(t, 90*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_NonHTTPService(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
mockService := NewMockHTTP(ctrl)
result := config.AddOption(mockService)
assert.Equal(t, mockService, result)
}
func TestConnectionPoolConfig_AddOption_WithCircuitBreakerWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
wrappedService := NewCircuitBreaker(CircuitBreakerConfig{
Threshold: 3,
Interval: 1 * time.Second,
}, baseService)
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_WithRetryWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
wrappedService := (&RetryConfig{MaxRetries: 3}).AddOption(baseService)
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, _ := baseService.Client.Transport.(*http.Transport)
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_WithAuthWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
wrappedService := &authProvider{
auth: func(_ context.Context, headers map[string]string) (map[string]string, error) {
return headers, nil
},
HTTP: baseService,
}
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_WithCustomHealthWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
wrappedService := (&HealthConfig{
HealthEndpoint: "/health",
Timeout: 5,
}).AddOption(baseService)
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_Validate_ValidConfiguration(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
err := config.Validate()
assert.NoError(t, err)
}
func TestConnectionPoolConfig_Validate_NegativeMaxIdleConns(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: -1,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
err := config.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "MaxIdleConns cannot be negative")
}
func TestConnectionPoolConfig_Validate_NegativeMaxIdleConnsPerHost(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: -1,
IdleConnTimeout: 30 * time.Second,
}
err := config.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "MaxIdleConnsPerHost cannot be negative")
}
func TestConnectionPoolConfig_Validate_NegativeIdleConnTimeout(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: -1 * time.Second,
}
err := config.Validate()
require.Error(t, err)
assert.Contains(t, err.Error(), "IdleConnTimeout cannot be negative")
}
func TestConnectionPoolConfig_Validate_ZeroValuesAreValid(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 0,
MaxIdleConnsPerHost: 0,
IdleConnTimeout: 0,
}
err := config.Validate()
assert.NoError(t, err)
}
func TestConnectionPoolConfig_ClonesDefaultTransport(t *testing.T) {
config := &ConnectionPoolConfig{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second,
}
baseService := &httpService{
Client: &http.Client{},
}
config.AddOption(baseService)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
defaultTransport := http.DefaultTransport.(*http.Transport)
assert.Equal(t, defaultTransport.TLSHandshakeTimeout, transport.TLSHandshakeTimeout)
assert.NotNil(t, transport.Proxy, "Proxy function should be set from default transport")
}
func TestConnectionPoolConfig_AddOption_WithRateLimiterWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
rlConfig := &RateLimiterConfig{
Requests: 10,
Window: time.Minute,
Burst: 20,
}
wrappedService := rlConfig.AddOption(baseService)
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
func TestConnectionPoolConfig_AddOption_WithCustomHeaderWrapper(t *testing.T) {
baseService := &httpService{
Client: &http.Client{},
}
wrappedService := (&DefaultHeaders{
Headers: map[string]string{"X-Custom": "value"},
}).AddOption(baseService)
config := &ConnectionPoolConfig{
MaxIdleConns: 50,
MaxIdleConnsPerHost: 15,
IdleConnTimeout: 60 * time.Second,
}
result := config.AddOption(wrappedService)
assert.Equal(t, wrappedService, result)
transport, ok := baseService.Client.Transport.(*http.Transport)
assert.True(t, ok, "Transport should be of type *http.Transport")
assert.Equal(t, 50, transport.MaxIdleConns)
assert.Equal(t, 15, transport.MaxIdleConnsPerHost)
assert.Equal(t, 60*time.Second, transport.IdleConnTimeout)
}
================================================
FILE: pkg/gofr/service/custom_header.go
================================================
package service
import (
"context"
"net/http"
)
type DefaultHeaders struct {
Headers map[string]string
}
func (a *DefaultHeaders) AddOption(h HTTP) HTTP {
return &customHeader{
Headers: a.Headers,
HTTP: h,
}
}
type customHeader struct {
Headers map[string]string
HTTP
}
func (a *customHeader) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
return a.GetWithHeaders(ctx, path, queryParams, nil)
}
func (a *customHeader) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
headers = setCustomHeader(headers, a.Headers)
return a.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
}
func (a *customHeader) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return a.PostWithHeaders(ctx, path, queryParams, body, nil)
}
func (a *customHeader) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
headers = setCustomHeader(headers, a.Headers)
return a.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *customHeader) Put(ctx context.Context, api string, queryParams map[string]any, body []byte) (
*http.Response, error) {
return a.PutWithHeaders(ctx, api, queryParams, body, nil)
}
func (a *customHeader) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
headers = setCustomHeader(headers, a.Headers)
return a.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *customHeader) Patch(ctx context.Context, path string, queryParams map[string]any, body []byte) (
*http.Response, error) {
return a.PatchWithHeaders(ctx, path, queryParams, body, nil)
}
func (a *customHeader) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
headers = setCustomHeader(headers, a.Headers)
return a.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
}
func (a *customHeader) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
return a.DeleteWithHeaders(ctx, path, body, nil)
}
func (a *customHeader) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (
*http.Response, error) {
headers = setCustomHeader(headers, a.Headers)
return a.HTTP.DeleteWithHeaders(ctx, path, body, headers)
}
func setCustomHeader(headers, customHeader map[string]string) map[string]string {
if headers == nil {
headers = make(map[string]string)
}
for key, value := range customHeader {
headers[key] = value
}
return headers
}
================================================
FILE: pkg/gofr/service/custom_header_test.go
================================================
package service
import (
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
func Test_CustomDomainProvider_Get(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
queryParams := map[string]any{"key": "value"}
body := []byte("body")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
w.WriteHeader(http.StatusOK)
_, err := w.Write(body)
if err != nil {
return
}
}))
defer server.Close()
customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&DefaultHeaders{
Headers: map[string]string{
"TEST_KEY": "test_value",
},
})
resp, err := customHeaderService.Get(t.Context(), "/path", queryParams)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, err)
bodyBytes, _ := io.ReadAll(resp.Body)
assert.Equal(t, string(body), string(bodyBytes))
}
func Test_CustomDomainProvider_Post(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
queryParams := map[string]any{"key": "value"}
body := []byte("body")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
w.WriteHeader(http.StatusCreated)
}))
defer server.Close()
customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&DefaultHeaders{
Headers: map[string]string{
"TEST_KEY": "test_value",
}})
resp, err := customHeaderService.Post(t.Context(), "/path", queryParams, body)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
require.NoError(t, err)
}
func TestCustomDomainProvider_Put(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
queryParams := map[string]any{"key": "value"}
body := []byte("body")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPut, r.Method)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&DefaultHeaders{
Headers: map[string]string{
"TEST_KEY": "test_value",
}})
resp, err := customHeaderService.Put(t.Context(), "/path", queryParams, body)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, err)
}
func TestCustomDomainProvider_Patch(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
queryParams := map[string]any{"key": "value"}
body := []byte("body")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPatch, r.Method)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&DefaultHeaders{
Headers: map[string]string{
"TEST_KEY": "test_value",
}})
resp, err := customHeaderService.Patch(t.Context(), "/path", queryParams, body)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
require.NoError(t, err)
}
func TestCustomDomainProvider_Delete(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
body := []byte("body")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodDelete, r.Method)
w.WriteHeader(http.StatusNoContent)
}))
defer server.Close()
customHeaderService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&DefaultHeaders{
Headers: map[string]string{
"TEST_KEY": "test_value",
}})
resp, err := customHeaderService.Delete(t.Context(), "/path", body)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
require.NoError(t, err)
}
================================================
FILE: pkg/gofr/service/errors.go
================================================
package service
import (
"fmt"
)
type AuthErr struct {
Err error
Message string
}
func (o AuthErr) Error() string {
switch {
case o.Message == "" && o.Err == nil:
return "unknown error"
case o.Message == "":
return o.Err.Error()
case o.Err == nil:
return o.Message
default:
return fmt.Sprintf("%v: %v", o.Message, o.Err)
}
}
================================================
FILE: pkg/gofr/service/errors_test.go
================================================
package service
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
var errTest = errors.New(`message inside error`)
func TestHttpService_OAuthError(t *testing.T) {
testCases := []struct {
err error
message string
response string
}{
{nil, "", "unknown error"},
{nil, "error message", "error message"},
{errTest, "", "message inside error"},
{errTest, "error message", fmt.Sprintf("%v: %v", "error message", errTest.Error())},
}
for i, tc := range testCases {
oAuthError := AuthErr{tc.err, tc.message}
assert.Equal(t, tc.response, oAuthError.Error(), "failed test case #%d", i)
}
}
================================================
FILE: pkg/gofr/service/health.go
================================================
package service
import (
"context"
"net/http"
"strings"
"time"
)
const (
serviceUp = "UP"
serviceDown = "DOWN"
defaultTimeout = 5
AlivePath = "/.well-known/alive"
HealthPath = "/.well-known/health"
)
type Health struct {
Status string `json:"status"`
Details map[string]any `json:"details"`
}
func (h *httpService) HealthCheck(ctx context.Context) *Health {
return h.getHealthResponseForEndpoint(ctx, strings.TrimPrefix(AlivePath, "/"), defaultTimeout)
}
func (h *httpService) getHealthResponseForEndpoint(ctx context.Context, endpoint string, timeout int) *Health {
var healthResponse = Health{
Details: make(map[string]any),
}
// create a new context with timeout for healthCheck call.
ctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Duration(timeout)*time.Second)
defer cancel()
// send a new context as we can have downstream services taking too long
// which may cancel the original health check http request
resp, err := h.Get(ctx, endpoint, nil)
if err != nil || resp == nil {
healthResponse.Status = serviceDown
healthResponse.Details["error"] = err.Error()
return &healthResponse
}
defer resp.Body.Close()
healthResponse.Details["host"] = resp.Request.URL.Host
if resp.StatusCode == http.StatusOK {
healthResponse.Status = serviceUp
return &healthResponse
}
healthResponse.Status = serviceDown
healthResponse.Details["error"] = "service down"
return &healthResponse
}
================================================
FILE: pkg/gofr/service/health_config.go
================================================
package service
import "context"
type HealthConfig struct {
HealthEndpoint string
Timeout int
}
func (h *HealthConfig) AddOption(svc HTTP) HTTP {
// if timeout is not provided we set a convenient default timeout.
if h.Timeout == 0 {
h.Timeout = defaultTimeout
}
// Set health config on the parent httpService so other options can access it
if httpSvc := extractHTTPService(svc); httpSvc != nil {
httpSvc.healthEndpoint = h.HealthEndpoint
httpSvc.healthTimeout = h.Timeout
}
return &customHealthService{
healthEndpoint: h.HealthEndpoint,
timeout: h.Timeout,
HTTP: svc,
}
}
type customHealthService struct {
healthEndpoint string
timeout int
HTTP
}
func (c *customHealthService) HealthCheck(ctx context.Context) *Health {
return c.HTTP.getHealthResponseForEndpoint(ctx, c.healthEndpoint, c.timeout)
}
================================================
FILE: pkg/gofr/service/health_config_test.go
================================================
package service
import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
func setupMockMetrics(t *testing.T) *MockMetrics {
t.Helper()
ctrl := gomock.NewController(t)
mockMetric := NewMockMetrics(ctrl)
mockMetric.EXPECT().RecordHistogram(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewCounter(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().NewGauge(gomock.Any(), gomock.Any()).AnyTimes()
mockMetric.EXPECT().SetGauge(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()
return mockMetric
}
func TestHealthConfig_AddOption_SetsParentHealthEndpoint(t *testing.T) {
server := httptest.NewServer(nil)
defer server.Close()
mockMetric := setupMockMetrics(t)
svc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{Threshold: 3, Interval: time.Second},
)
healthConfig := HealthConfig{
HealthEndpoint: "breeds",
Timeout: 10,
}
result := healthConfig.AddOption(svc)
// Verify httpService parent has health config set
httpSvc := extractHTTPService(svc)
require.NotNil(t, httpSvc)
assert.Equal(t, "breeds", httpSvc.healthEndpoint)
assert.Equal(t, 10, httpSvc.healthTimeout)
// Verify customHealthService is returned
customHealth, ok := result.(*customHealthService)
assert.True(t, ok)
assert.Equal(t, "breeds", customHealth.healthEndpoint)
assert.Equal(t, 10, customHealth.timeout)
}
func TestHealthConfig_AddOption_DefaultTimeout(t *testing.T) {
server := httptest.NewServer(nil)
defer server.Close()
mockMetric := setupMockMetrics(t)
svc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric)
healthConfig := HealthConfig{
HealthEndpoint: "health",
// Timeout not set - should use default
}
result := healthConfig.AddOption(svc)
// Verify default timeout is used
httpSvc := extractHTTPService(svc)
require.NotNil(t, httpSvc)
assert.Equal(t, "health", httpSvc.healthEndpoint)
assert.Equal(t, defaultTimeout, httpSvc.healthTimeout)
customHealth, ok := result.(*customHealthService)
assert.True(t, ok)
assert.Equal(t, defaultTimeout, customHealth.timeout)
}
func TestHealthConfig_AddOption_WithRetryAndCircuitBreaker(t *testing.T) {
server := httptest.NewServer(nil)
defer server.Close()
mockMetric := setupMockMetrics(t)
// Create service with circuit breaker and retry
svc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{Threshold: 3, Interval: time.Second},
&RetryConfig{MaxRetries: 3},
)
healthConfig := HealthConfig{
HealthEndpoint: "status",
Timeout: 15,
}
result := healthConfig.AddOption(svc)
// Verify httpService parent has health config set
httpSvc := extractHTTPService(svc)
require.NotNil(t, httpSvc)
assert.Equal(t, "status", httpSvc.healthEndpoint)
assert.Equal(t, 15, httpSvc.healthTimeout)
// Verify customHealthService wraps the chain
customHealth, ok := result.(*customHealthService)
assert.True(t, ok)
assert.Equal(t, "status", customHealth.healthEndpoint)
}
func TestCircuitBreaker_UsesParentHealthEndpoint(t *testing.T) {
// Server that returns 502 for /fail, 200 for /custom-health
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/custom-health":
w.WriteHeader(http.StatusOK)
case "/fail":
w.WriteHeader(http.StatusBadGateway)
case "/.well-known/alive":
w.WriteHeader(http.StatusNotFound) // Default endpoint not available
default:
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
mockMetric := setupMockMetrics(t)
// Create service with circuit breaker AND health config
svc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{Threshold: 1, Interval: 200 * time.Millisecond},
&HealthConfig{HealthEndpoint: "custom-health", Timeout: 5},
)
// First request returns 502 - failure count becomes 1
resp, err := svc.Get(t.Context(), "fail", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
resp.Body.Close()
// Second request - circuit opens and returns ErrCircuitOpen
resp, err = svc.Get(t.Context(), "fail", nil)
if err != nil {
require.ErrorIs(t, err, ErrCircuitOpen)
return
}
defer resp.Body.Close()
require.ErrorIs(t, err, ErrCircuitOpen)
// Wait for interval to pass
time.Sleep(500 * time.Millisecond)
// Circuit should recover using /custom-health (from parent httpService)
resp, err = svc.Get(t.Context(), "success", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
resp.Body.Close()
}
func TestCircuitBreaker_UsesDefaultHealthEndpoint_WhenNoHealthConfig(t *testing.T) {
// Server where default health endpoint returns 404
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/.well-known/alive":
w.WriteHeader(http.StatusNotFound) // Default endpoint not available
case "/fail":
w.WriteHeader(http.StatusBadGateway)
default:
w.WriteHeader(http.StatusOK)
}
}))
defer server.Close()
mockMetric := setupMockMetrics(t)
// Create service with circuit breaker but NO health config
svc := NewHTTPService(server.URL, logging.NewMockLogger(logging.DEBUG), mockMetric,
&CircuitBreakerConfig{Threshold: 1, Interval: 200 * time.Millisecond},
)
// First request returns 502
resp, err := svc.Get(t.Context(), "fail", nil)
if err != nil {
require.ErrorIs(t, err, ErrCircuitOpen)
return
}
defer resp.Body.Close()
require.NoError(t, err)
assert.Equal(t, http.StatusBadGateway, resp.StatusCode)
// Second request - circuit opens
resp, err = svc.Get(t.Context(), "fail", nil)
if err != nil {
require.ErrorIs(t, err, ErrCircuitOpen)
return
}
defer resp.Body.Close()
require.ErrorIs(t, err, ErrCircuitOpen)
// Wait for interval
time.Sleep(500 * time.Millisecond)
// Circuit should NOT recover because /.well-known/alive returns 404
resp, err = svc.Get(t.Context(), "success", nil)
if err != nil {
require.Error(t, err)
return
}
defer resp.Body.Close()
require.ErrorIs(t, err, ErrCircuitOpen)
}
================================================
FILE: pkg/gofr/service/health_test.go
================================================
package service
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
"gofr.dev/pkg/gofr/testutil"
)
func TestHTTPService_HealthCheck(t *testing.T) {
service, server, metrics := initializeTest(t, "alive", http.StatusOK)
defer server.Close()
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL,
"method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusOK)).Times(1)
// when params value is of type []string then last value is sent in request
resp := service.HealthCheck(t.Context())
assert.Equal(t, &Health{Status: serviceUp, Details: map[string]any{"host": server.URL[7:]}},
resp, "TEST[%d], Failed.\n%s")
}
func TestHTTPService_HealthCheckCustomURL(t *testing.T) {
service, server, metrics := initializeTest(t, "ready", http.StatusOK)
defer server.Close()
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL,
"method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusOK)).Times(1)
// when params value is of type []string then last value is sent in request
resp := service.HealthCheck(t.Context())
assert.Equal(t, &Health{Status: serviceUp, Details: map[string]any{"host": server.URL[7:]}},
resp, "TEST[%d], Failed.\n%s")
}
func TestHTTPService_HealthCheckErrorResponse(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", gomock.Any(),
"method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusServiceUnavailable))
service := NewHTTPService("http://test", logging.NewMockLogger(logging.INFO), metrics)
// when params value is of type []string then last value is sent in request
resp := service.HealthCheck(t.Context())
body, _ := json.Marshal(&resp)
assert.Contains(t, string(body), `{"status":"DOWN","details":{"error":"Get \"http://test/.well-known/alive\"`)
}
func TestHTTPService_HealthCheckDifferentStatusCode(t *testing.T) {
service, server, metrics := initializeTest(t, "bad-request", http.StatusBadRequest)
defer server.Close()
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL,
"method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusBadRequest)).AnyTimes()
// when params value is of type []string then last value is sent in request
resp := service.HealthCheck(t.Context())
assert.Equal(t, &Health{Status: serviceDown,
Details: map[string]any{"host": server.URL[7:], "error": "service down"}},
resp, "TEST[%d], Failed.\n%s")
}
func TestHTTPService_HealthCheckTimeout(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
server := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/.well-known/alive", r.URL.Path)
time.Sleep(1 * time.Second)
}))
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL,
"method", http.MethodGet, "status", fmt.Sprintf("%v", http.StatusServiceUnavailable)).AnyTimes()
log := testutil.StdoutOutputForFunc(func() {
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), metrics,
&HealthConfig{HealthEndpoint: ".well-known/alive", Timeout: 1})
resp := service.HealthCheck(t.Context())
assert.Equal(t, &Health{Status: serviceDown,
Details: map[string]any{"error": "Get \"" + server.URL + "/.well-known/alive\": context deadline exceeded"}},
resp, "TEST[%d], Failed.\n%s")
})
assert.Contains(t, log, "context deadline exceeded")
}
func initializeTest(t *testing.T, urlSuffix string, statusCode int) (HTTP, *httptest.Server, *MockMetrics) {
t.Helper()
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/.well-known/"+urlSuffix, r.URL.Path)
if statusCode == http.StatusOK {
_, _ = w.Write([]byte(`{"data":"UP"}`))
}
w.WriteHeader(statusCode)
}))
service := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), metrics,
&HealthConfig{HealthEndpoint: ".well-known/" + urlSuffix})
return service, server, metrics
}
================================================
FILE: pkg/gofr/service/logger.go
================================================
package service
import (
"fmt"
"io"
"time"
)
type Logger interface {
Log(args ...any)
}
type Log struct {
Timestamp time.Time `json:"timestamp"`
ResponseTime int64 `json:"latency"`
CorrelationID string `json:"correlationId"`
ResponseCode int `json:"responseCode"`
HTTPMethod string `json:"httpMethod"`
URI string `json:"uri"`
}
func (l *Log) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s \n",
l.CorrelationID, colorForStatusCode(l.ResponseCode),
l.ResponseCode, l.ResponseTime, l.HTTPMethod, l.URI)
}
type ErrorLog struct {
*Log
ErrorMessage string `json:"errorMessage"`
}
func (el *ErrorLog) PrettyPrint(writer io.Writer) {
fmt.Fprintf(writer, "\u001B[38;5;8m%s \u001B[38;5;%dm%-6d\u001B[0m %8d\u001B[38;5;8mµs\u001B[0m %s %s \n",
el.CorrelationID, colorForStatusCode(el.ResponseCode),
el.ResponseCode, el.ResponseTime, el.HTTPMethod, el.URI)
}
func colorForStatusCode(status int) int {
const (
blue = 34
red = 202
yellow = 220
)
switch {
case status >= 200 && status < 300:
return blue
case status >= 400 && status < 500:
return yellow
case status >= 500 && status < 600:
return red
}
return 0
}
================================================
FILE: pkg/gofr/service/logger_test.go
================================================
package service
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLog_PrettyPrint(t *testing.T) {
w := new(bytes.Buffer)
l := &Log{
ResponseTime: 100,
CorrelationID: "abc-test-correlation-id",
ResponseCode: 200,
HTTPMethod: "GET",
URI: "/api/test",
}
l.PrettyPrint(w)
assert.Equal(t, "\u001B[38;5;8mabc-test-correlation-id \u001B[38;5;34m200 \u001B[0m 100\u001B[38;5;8mµs\u001B[0m GET /api/test \n",
w.String())
}
func TestErrorLog_PrettyPrint(t *testing.T) {
w := new(bytes.Buffer)
l := &ErrorLog{
Log: &Log{
ResponseTime: 100,
CorrelationID: "abc-test-correlation-id",
ResponseCode: 200,
HTTPMethod: "GET",
URI: "/api/test",
},
ErrorMessage: "some error occurred",
}
l.PrettyPrint(w)
assert.Equal(t, "\u001B[38;5;8mabc-test-correlation-id \u001B[38;5;34m200 \u001B[0m 100\u001B[38;5;8mµs\u001B[0m GET /api/test \n",
w.String())
}
func Test_ColorForStatusCode(t *testing.T) {
testCases := []struct {
desc string
code int
expOut int
}{
{desc: "200 OK", code: 200, expOut: 34},
{desc: "201 Created", code: 201, expOut: 34},
{desc: "400 Bad Request", code: 400, expOut: 220},
{desc: "409 Conflict", code: 409, expOut: 220},
{desc: "500 Internal Srv Error", code: 500, expOut: 202},
}
for _, tc := range testCases {
out := colorForStatusCode(tc.code)
assert.Equal(t, tc.expOut, out)
}
}
================================================
FILE: pkg/gofr/service/metrics.go
================================================
package service
import "context"
type Metrics interface {
NewCounter(name, desc string)
NewGauge(name, desc string)
IncrementCounter(ctx context.Context, name string, labels ...string)
RecordHistogram(ctx context.Context, name string, value float64, labels ...string)
SetGauge(name string, value float64, labels ...string)
}
================================================
FILE: pkg/gofr/service/mock_http_service.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: ../service/new.go
//
// Generated by this command:
//
// mockgen -source=../service/new.go -destination=../service/mock_http_service.go -package=service HTTP
//
// Package service is a generated GoMock package.
package service
import (
context "context"
http "net/http"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockHTTP is a mock of HTTP interface.
type MockHTTP struct {
ctrl *gomock.Controller
recorder *MockHTTPMockRecorder
}
// MockHTTPMockRecorder is the mock recorder for MockHTTP.
type MockHTTPMockRecorder struct {
mock *MockHTTP
}
// NewMockHTTP creates a new mock instance.
func NewMockHTTP(ctrl *gomock.Controller) *MockHTTP {
mock := &MockHTTP{ctrl: ctrl}
mock.recorder = &MockHTTPMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHTTP) EXPECT() *MockHTTPMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockHTTP) Delete(ctx context.Context, api string, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, api, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockHTTPMockRecorder) Delete(ctx, api, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockHTTP)(nil).Delete), ctx, api, body)
}
// DeleteWithHeaders mocks base method.
func (m *MockHTTP) DeleteWithHeaders(ctx context.Context, api string, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWithHeaders", ctx, api, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteWithHeaders indicates an expected call of DeleteWithHeaders.
func (mr *MockHTTPMockRecorder) DeleteWithHeaders(ctx, api, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWithHeaders", reflect.TypeOf((*MockHTTP)(nil).DeleteWithHeaders), ctx, api, body, headers)
}
// Get mocks base method.
func (m *MockHTTP) Get(ctx context.Context, api string, queryParams map[string]any) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, api, queryParams)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockHTTPMockRecorder) Get(ctx, api, queryParams any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockHTTP)(nil).Get), ctx, api, queryParams)
}
// GetWithHeaders mocks base method.
func (m *MockHTTP) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWithHeaders", ctx, path, queryParams, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWithHeaders indicates an expected call of GetWithHeaders.
func (mr *MockHTTPMockRecorder) GetWithHeaders(ctx, path, queryParams, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWithHeaders", reflect.TypeOf((*MockHTTP)(nil).GetWithHeaders), ctx, path, queryParams, headers)
}
// HealthCheck mocks base method.
func (m *MockHTTP) HealthCheck(ctx context.Context) *Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "HealthCheck", ctx)
ret0, _ := ret[0].(*Health)
return ret0
}
// HealthCheck indicates an expected call of HealthCheck.
func (mr *MockHTTPMockRecorder) HealthCheck(ctx any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HealthCheck", reflect.TypeOf((*MockHTTP)(nil).HealthCheck), ctx)
}
// Patch mocks base method.
func (m *MockHTTP) Patch(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Patch", ctx, api, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockHTTPMockRecorder) Patch(ctx, api, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockHTTP)(nil).Patch), ctx, api, queryParams, body)
}
// PatchWithHeaders mocks base method.
func (m *MockHTTP) PatchWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PatchWithHeaders", ctx, api, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PatchWithHeaders indicates an expected call of PatchWithHeaders.
func (mr *MockHTTPMockRecorder) PatchWithHeaders(ctx, api, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchWithHeaders", reflect.TypeOf((*MockHTTP)(nil).PatchWithHeaders), ctx, api, queryParams, body, headers)
}
// Post mocks base method.
func (m *MockHTTP) Post(ctx context.Context, path string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Post", ctx, path, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Post indicates an expected call of Post.
func (mr *MockHTTPMockRecorder) Post(ctx, path, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockHTTP)(nil).Post), ctx, path, queryParams, body)
}
// PostWithHeaders mocks base method.
func (m *MockHTTP) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostWithHeaders", ctx, path, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PostWithHeaders indicates an expected call of PostWithHeaders.
func (mr *MockHTTPMockRecorder) PostWithHeaders(ctx, path, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostWithHeaders", reflect.TypeOf((*MockHTTP)(nil).PostWithHeaders), ctx, path, queryParams, body, headers)
}
// Put mocks base method.
func (m *MockHTTP) Put(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Put", ctx, api, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Put indicates an expected call of Put.
func (mr *MockHTTPMockRecorder) Put(ctx, api, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockHTTP)(nil).Put), ctx, api, queryParams, body)
}
// PutWithHeaders mocks base method.
func (m *MockHTTP) PutWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutWithHeaders", ctx, api, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutWithHeaders indicates an expected call of PutWithHeaders.
func (mr *MockHTTPMockRecorder) PutWithHeaders(ctx, api, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutWithHeaders", reflect.TypeOf((*MockHTTP)(nil).PutWithHeaders), ctx, api, queryParams, body, headers)
}
// getHealthResponseForEndpoint mocks base method.
func (m *MockHTTP) getHealthResponseForEndpoint(ctx context.Context, endpoint string, timeout int) *Health {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "getHealthResponseForEndpoint", ctx, endpoint, timeout)
ret0, _ := ret[0].(*Health)
return ret0
}
// getHealthResponseForEndpoint indicates an expected call of getHealthResponseForEndpoint.
func (mr *MockHTTPMockRecorder) getHealthResponseForEndpoint(ctx, endpoint, timeout any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "getHealthResponseForEndpoint", reflect.TypeOf((*MockHTTP)(nil).getHealthResponseForEndpoint), ctx, endpoint, timeout)
}
// MockhttpClient is a mock of httpClient interface.
type MockhttpClient struct {
ctrl *gomock.Controller
recorder *MockhttpClientMockRecorder
}
// MockhttpClientMockRecorder is the mock recorder for MockhttpClient.
type MockhttpClientMockRecorder struct {
mock *MockhttpClient
}
// NewMockhttpClient creates a new mock instance.
func NewMockhttpClient(ctrl *gomock.Controller) *MockhttpClient {
mock := &MockhttpClient{ctrl: ctrl}
mock.recorder = &MockhttpClientMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockhttpClient) EXPECT() *MockhttpClientMockRecorder {
return m.recorder
}
// Delete mocks base method.
func (m *MockhttpClient) Delete(ctx context.Context, api string, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Delete", ctx, api, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Delete indicates an expected call of Delete.
func (mr *MockhttpClientMockRecorder) Delete(ctx, api, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockhttpClient)(nil).Delete), ctx, api, body)
}
// DeleteWithHeaders mocks base method.
func (m *MockhttpClient) DeleteWithHeaders(ctx context.Context, api string, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteWithHeaders", ctx, api, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DeleteWithHeaders indicates an expected call of DeleteWithHeaders.
func (mr *MockhttpClientMockRecorder) DeleteWithHeaders(ctx, api, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteWithHeaders", reflect.TypeOf((*MockhttpClient)(nil).DeleteWithHeaders), ctx, api, body, headers)
}
// Get mocks base method.
func (m *MockhttpClient) Get(ctx context.Context, api string, queryParams map[string]any) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", ctx, api, queryParams)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Get indicates an expected call of Get.
func (mr *MockhttpClientMockRecorder) Get(ctx, api, queryParams any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockhttpClient)(nil).Get), ctx, api, queryParams)
}
// GetWithHeaders mocks base method.
func (m *MockhttpClient) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetWithHeaders", ctx, path, queryParams, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetWithHeaders indicates an expected call of GetWithHeaders.
func (mr *MockhttpClientMockRecorder) GetWithHeaders(ctx, path, queryParams, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetWithHeaders", reflect.TypeOf((*MockhttpClient)(nil).GetWithHeaders), ctx, path, queryParams, headers)
}
// Patch mocks base method.
func (m *MockhttpClient) Patch(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Patch", ctx, api, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Patch indicates an expected call of Patch.
func (mr *MockhttpClientMockRecorder) Patch(ctx, api, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Patch", reflect.TypeOf((*MockhttpClient)(nil).Patch), ctx, api, queryParams, body)
}
// PatchWithHeaders mocks base method.
func (m *MockhttpClient) PatchWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PatchWithHeaders", ctx, api, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PatchWithHeaders indicates an expected call of PatchWithHeaders.
func (mr *MockhttpClientMockRecorder) PatchWithHeaders(ctx, api, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PatchWithHeaders", reflect.TypeOf((*MockhttpClient)(nil).PatchWithHeaders), ctx, api, queryParams, body, headers)
}
// Post mocks base method.
func (m *MockhttpClient) Post(ctx context.Context, path string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Post", ctx, path, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Post indicates an expected call of Post.
func (mr *MockhttpClientMockRecorder) Post(ctx, path, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Post", reflect.TypeOf((*MockhttpClient)(nil).Post), ctx, path, queryParams, body)
}
// PostWithHeaders mocks base method.
func (m *MockhttpClient) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PostWithHeaders", ctx, path, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PostWithHeaders indicates an expected call of PostWithHeaders.
func (mr *MockhttpClientMockRecorder) PostWithHeaders(ctx, path, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostWithHeaders", reflect.TypeOf((*MockhttpClient)(nil).PostWithHeaders), ctx, path, queryParams, body, headers)
}
// Put mocks base method.
func (m *MockhttpClient) Put(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Put", ctx, api, queryParams, body)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Put indicates an expected call of Put.
func (mr *MockhttpClientMockRecorder) Put(ctx, api, queryParams, body any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Put", reflect.TypeOf((*MockhttpClient)(nil).Put), ctx, api, queryParams, body)
}
// PutWithHeaders mocks base method.
func (m *MockhttpClient) PutWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutWithHeaders", ctx, api, queryParams, body, headers)
ret0, _ := ret[0].(*http.Response)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutWithHeaders indicates an expected call of PutWithHeaders.
func (mr *MockhttpClientMockRecorder) PutWithHeaders(ctx, api, queryParams, body, headers any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutWithHeaders", reflect.TypeOf((*MockhttpClient)(nil).PutWithHeaders), ctx, api, queryParams, body, headers)
}
================================================
FILE: pkg/gofr/service/mock_metrics.go
================================================
// Code generated by MockGen. DO NOT EDIT.
// Source: metrics.go
//
// Generated by this command:
//
// mockgen -source=metrics.go -destination=mock_metrics.go -package=service
//
// Package service is a generated GoMock package.
package service
import (
context "context"
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// MockMetrics is a mock of Metrics interface.
type MockMetrics struct {
ctrl *gomock.Controller
recorder *MockMetricsMockRecorder
isgomock struct{}
}
// MockMetricsMockRecorder is the mock recorder for MockMetrics.
type MockMetricsMockRecorder struct {
mock *MockMetrics
}
// NewMockMetrics creates a new mock instance.
func NewMockMetrics(ctrl *gomock.Controller) *MockMetrics {
mock := &MockMetrics{ctrl: ctrl}
mock.recorder = &MockMetricsMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockMetrics) EXPECT() *MockMetricsMockRecorder {
return m.recorder
}
// IncrementCounter mocks base method.
func (m *MockMetrics) IncrementCounter(ctx context.Context, name string, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "IncrementCounter", varargs...)
}
// IncrementCounter indicates an expected call of IncrementCounter.
func (mr *MockMetricsMockRecorder) IncrementCounter(ctx, name any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IncrementCounter", reflect.TypeOf((*MockMetrics)(nil).IncrementCounter), varargs...)
}
// NewCounter mocks base method.
func (m *MockMetrics) NewCounter(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewCounter", name, desc)
}
// NewCounter indicates an expected call of NewCounter.
func (mr *MockMetricsMockRecorder) NewCounter(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCounter", reflect.TypeOf((*MockMetrics)(nil).NewCounter), name, desc)
}
// NewGauge mocks base method.
func (m *MockMetrics) NewGauge(name, desc string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "NewGauge", name, desc)
}
// NewGauge indicates an expected call of NewGauge.
func (mr *MockMetricsMockRecorder) NewGauge(name, desc any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewGauge", reflect.TypeOf((*MockMetrics)(nil).NewGauge), name, desc)
}
// RecordHistogram mocks base method.
func (m *MockMetrics) RecordHistogram(ctx context.Context, name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{ctx, name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "RecordHistogram", varargs...)
}
// RecordHistogram indicates an expected call of RecordHistogram.
func (mr *MockMetricsMockRecorder) RecordHistogram(ctx, name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{ctx, name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RecordHistogram", reflect.TypeOf((*MockMetrics)(nil).RecordHistogram), varargs...)
}
// SetGauge mocks base method.
func (m *MockMetrics) SetGauge(name string, value float64, labels ...string) {
m.ctrl.T.Helper()
varargs := []any{name, value}
for _, a := range labels {
varargs = append(varargs, a)
}
m.ctrl.Call(m, "SetGauge", varargs...)
}
// SetGauge indicates an expected call of SetGauge.
func (mr *MockMetricsMockRecorder) SetGauge(name, value any, labels ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{name, value}, labels...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetGauge", reflect.TypeOf((*MockMetrics)(nil).SetGauge), varargs...)
}
================================================
FILE: pkg/gofr/service/mock_oauth_server.go
================================================
package service
import (
"crypto/rand"
"crypto/rsa"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const clientIDLength = 10
const clientSecretLength = 24
const privateKeyBits = 2048
type oAuthTestSever struct {
tokenURL string
clientID string
clientSecret string
testURL string
audienceClaim string
privateKey *rsa.PrivateKey
httpServer *httptest.Server
}
func setupOAuthHTTPServer(t *testing.T, config *OAuthConfig) *httptest.Server {
t.Helper()
server := oAuthTestSever{
tokenURL: "/token",
testURL: "/test",
audienceClaim: config.EndpointParams.Get("aud"),
}
server.clientID = config.ClientID
server.clientSecret = config.ClientSecret
privateKey, err := rsa.GenerateKey(rand.Reader, privateKeyBits)
require.NoError(t, err, "failed to generate private key, aborting")
server.privateKey = privateKey
mux := http.NewServeMux()
mux.HandleFunc(server.tokenURL, func(w http.ResponseWriter, r *http.Request) {
errMessage, statusCode := server.validateCredentials(r)
if statusCode != http.StatusOK {
http.Error(w, errMessage, statusCode)
return
}
accessToken, err := server.generateToken(getClaims(r))
if err != nil {
http.Error(w, "Unable to generate token", http.StatusInternalServerError)
return
}
// Prepare the JSON response
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
tokenResponse := map[string]any{
"access_token": accessToken,
"token_type": "Bearer",
"expires_in": 3600, // Expires in 1 hour
"scope": "read write", // Mock scope
}
_ = json.NewEncoder(w).Encode(tokenResponse)
})
mux.HandleFunc(server.testURL, func(w http.ResponseWriter, r *http.Request) {
header := r.Header.Get(AuthHeader)
token := strings.Split(header, " ")
if len(token) <= 1 {
w.WriteHeader(http.StatusUnauthorized)
}
parsedToken, _ := jwt.Parse(token[1], func(*jwt.Token) (any, error) {
return []byte("my-secret-key"), nil
})
claims, err := parsedToken.Claims.GetAudience()
assert.NoError(t, err, "error while getting audience from claims")
assert.NotEmptyf(t, claims, "no value in claims")
assert.Equal(t, server.audienceClaim, claims[0])
w.WriteHeader(http.StatusOK)
})
server.httpServer = httptest.NewServer(mux)
t.Cleanup(func() {
server.httpServer.Close()
})
return server.httpServer
}
func (s *oAuthTestSever) validateCredentials(r *http.Request) (errMessage string, statusCode int) {
err := r.ParseForm()
if err != nil {
return "Failed to parse form", http.StatusBadRequest
}
grantType := r.Form.Get("grant_type")
clientID := r.Form.Get("client_id")
clientSecret := r.Form.Get("client_secret")
// Basic validation
if grantType != "client_credentials" || clientID == "" || clientSecret == "" {
return "Invalid token request", http.StatusBadRequest
}
// Validate the authorization code
if s.clientID != clientID || s.clientSecret != clientSecret {
return "Invalid credentials", http.StatusUnauthorized
}
return "", http.StatusOK
}
func (s *oAuthTestSever) generateToken(claims jwt.MapClaims) (string, error) {
claims["iat"] = time.Now().Unix()
claims["exp"] = time.Now().Add(time.Hour).Unix()
t := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
return t.SignedString(s.privateKey)
}
func getClaims(r *http.Request) map[string]any {
claims := make(map[string]any, 0)
for key, value := range r.Form {
if key == "client_id" || key == "client_secret" || key == "grant_type" {
continue
}
claims[key] = value
}
return claims
}
// Helper function to generate a random string.
func generateRandomString(length int) (token string, err error) {
// Generate random bytes
b := make([]byte, length)
_, err = rand.Read(b) // Use crypto/rand.Read
if err != nil {
return "", fmt.Errorf("failed to read random bytes: %w", err)
}
// Encode to base64 to make it URL-safe and human-readable (for tokens)
return base64.URLEncoding.EncodeToString(b), nil
}
================================================
FILE: pkg/gofr/service/new.go
================================================
package service
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptrace"
"strings"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace"
)
type httpService struct {
*http.Client
trace.Tracer
url string
name string
Logger
Metrics
// healthEndpoint is the custom endpoint for health checks (shared across options)
healthEndpoint string
// healthTimeout is the timeout in seconds for health check requests
healthTimeout int
}
type HTTP interface {
// HTTP is embedded as HTTP would be able to access it's clients method
httpClient
// HealthCheck to get the service health and report it to the current application
HealthCheck(ctx context.Context) *Health
getHealthResponseForEndpoint(ctx context.Context, endpoint string, timeout int) *Health
}
type httpClient interface {
// Get performs an HTTP GET request.
Get(ctx context.Context, api string, queryParams map[string]any) (*http.Response, error)
// GetWithHeaders performs an HTTP GET request with custom headers.
GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error)
// Post performs an HTTP POST request.
Post(ctx context.Context, path string, queryParams map[string]any, body []byte) (*http.Response, error)
// PostWithHeaders performs an HTTP POST request with custom headers.
PostWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error)
// Put performs an HTTP PUT request.
Put(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error)
// PutWithHeaders performs an HTTP PUT request with custom headers.
PutWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error)
// Patch performs an HTTP PATCH request.
Patch(ctx context.Context, api string, queryParams map[string]any, body []byte) (*http.Response, error)
// PatchWithHeaders performs an HTTP PATCH request with custom headers.
PatchWithHeaders(ctx context.Context, api string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error)
// Delete performs an HTTP DELETE request.
Delete(ctx context.Context, api string, body []byte) (*http.Response, error)
// DeleteWithHeaders performs an HTTP DELETE request with custom headers.
DeleteWithHeaders(ctx context.Context, api string, body []byte, headers map[string]string) (*http.Response, error)
}
// NewHTTPService function creates a new instance of the httpService struct, which implements the HTTP interface.
// It initializes the http.Client, url, Tracer, and Logger fields of the httpService struct with the provided values.
func NewHTTPService(serviceAddress string, logger Logger, metrics Metrics, options ...Options) HTTP {
h := &httpService{
// using default HTTP client to do HTTP communication
Client: &http.Client{},
url: serviceAddress,
Tracer: otel.Tracer("gofr-http-client"),
Logger: logger,
Metrics: metrics,
}
var svc HTTP
svc = h
// if options are given, then add them to the httpService struct
for _, o := range options {
svc = o.AddOption(svc)
}
return svc
}
func (h *httpService) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
return h.GetWithHeaders(ctx, path, queryParams, nil)
}
func (h *httpService) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
return h.createAndSendRequest(ctx, http.MethodGet, path, queryParams, nil, headers)
}
func (h *httpService) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return h.PostWithHeaders(ctx, path, queryParams, body, nil)
}
func (h *httpService) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return h.createAndSendRequest(ctx, http.MethodPost, path, queryParams, body, headers)
}
func (h *httpService) Patch(ctx context.Context, path string, queryParams map[string]any, body []byte) (*http.Response, error) {
return h.PatchWithHeaders(ctx, path, queryParams, body, nil)
}
func (h *httpService) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return h.createAndSendRequest(ctx, http.MethodPatch, path, queryParams, body, headers)
}
func (h *httpService) Put(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return h.PutWithHeaders(ctx, path, queryParams, body, nil)
}
func (h *httpService) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
return h.createAndSendRequest(ctx, http.MethodPut, path, queryParams, body, headers)
}
func (h *httpService) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
return h.DeleteWithHeaders(ctx, path, body, nil)
}
func (h *httpService) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (*http.Response, error) {
return h.createAndSendRequest(ctx, http.MethodDelete, path, nil, body, headers)
}
func (h *httpService) createAndSendRequest(ctx context.Context, method string, path string,
queryParams map[string]any, body []byte, headers map[string]string) (*http.Response, error) {
uri := h.url + "/" + path
uri = strings.TrimRight(uri, "/")
ctx, span := h.Tracer.Start(ctx, uri)
defer span.End()
// Attach client-side trace handling for HTTP request.
clientTraceCtx := httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
// Create the HTTP request with the tracing context.
req, err := http.NewRequestWithContext(clientTraceCtx, method, uri, bytes.NewBuffer(body))
if err != nil {
return nil, err
}
var isContentTypeSet bool
for k, v := range headers {
if strings.EqualFold(k, "content-type") {
isContentTypeSet = true
}
req.Header.Set(k, v)
}
if !isContentTypeSet {
req.Header.Set("Content-Type", "application/json")
}
// Inject tracing information into the request headers.
otel.GetTextMapPropagator().Inject(clientTraceCtx, propagation.HeaderCarrier(req.Header))
// encode the query parameters on the request.
encodeQueryParameters(req, queryParams)
log := &Log{
Timestamp: time.Now(),
CorrelationID: trace.SpanFromContext(clientTraceCtx).SpanContext().TraceID().String(),
HTTPMethod: method,
URI: uri,
}
requestStart := time.Now()
resp, err := h.Do(req)
respTime := time.Since(requestStart)
log.ResponseTime = respTime.Microseconds()
if err != nil {
log.ResponseCode = http.StatusServiceUnavailable
h.Log(&ErrorLog{Log: log, ErrorMessage: err.Error()})
h.updateMetrics(clientTraceCtx, method, respTime.Seconds(), http.StatusServiceUnavailable)
return resp, err
}
h.updateMetrics(clientTraceCtx, method, respTime.Seconds(), resp.StatusCode)
log.ResponseCode = resp.StatusCode
h.Log(log)
return resp, nil
}
func (h *httpService) updateMetrics(ctx context.Context, method string, timeTaken float64, statusCode int) {
if h.Metrics != nil {
labels := []string{"path", h.url, "method", method, "status", fmt.Sprintf("%v", statusCode)}
if h.name != "" {
labels = append(labels, "service", h.name)
}
h.RecordHistogram(ctx, "app_http_service_response", timeTaken, labels...)
}
}
func encodeQueryParameters(req *http.Request, queryParams map[string]any) {
q := req.URL.Query()
for k, v := range queryParams {
switch vt := v.(type) {
case []string:
for _, val := range vt {
q.Add(k, val)
}
default:
q.Set(k, fmt.Sprintf("%v", v))
}
}
req.URL.RawQuery = q.Encode()
}
type attributesOption map[string]string
func (a attributesOption) AddOption(h HTTP) HTTP {
if svc := extractHTTPService(h); svc != nil {
if name, ok := a["name"]; ok {
svc.name = name
}
}
return h
}
// WithAttributes returns an Option that sets the attributes of the HTTP service.
func WithAttributes(attributes map[string]string) Options {
return attributesOption(attributes)
}
================================================
FILE: pkg/gofr/service/new_test.go
================================================
package service
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/sdk/trace"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
func TestNewHTTPService(t *testing.T) {
tests := []struct {
desc string
serviceAddress string
}{
{"Valid Address", "http://example.com"},
{"Empty Address", ""},
{"Invalid Address", "not_a_valid_address"},
}
for i, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
service := NewHTTPService(tc.serviceAddress, nil, nil)
assert.NotNil(t, service, "TEST[%d], Failed.\n%s", i, tc.desc)
})
}
t.Run("WithAttributes", func(t *testing.T) {
service := NewHTTPService("http://example.com", nil, nil, WithAttributes(map[string]string{"name": "test-service"}))
httpSvc := service.(*httpService)
assert.Equal(t, "test-service", httpSvc.name)
})
}
func TestHTTPService_createAndSendRequest(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
ctx := t.Context()
tests := []struct {
desc string
queryParams map[string]any
body []byte
headers map[string]string
expQueryParam string
expContentType string
}{
{"with query params, body and header", map[string]any{"key": "value", "name": []string{"gofr", "test"}},
[]byte("{Test Body}"), map[string]string{"header1": "value1"}, "key=value&name=gofr&name=test", "application/json"},
{"with query params, body, header and content type", map[string]any{"key": "value", "name": []string{"gofr", "test"}},
[]byte("{Test Body}"), map[string]string{"header1": "value1", "content-type": "application/json"},
"key=value&name=gofr&name=test", "application/json"},
{"with query params, body, header and content type xml", map[string]any{"key": "value", "name": []string{"gofr", "test"}},
[]byte("{Test Body}"), map[string]string{"header1": "value1", "content-type": "application/xml"},
"key=value&name=gofr&name=test", "application/xml"},
{"without query params, body, header and content type", nil, []byte("{Test Body}"),
map[string]string{"header1": "value1", "content-type": "application/json"},
"", "application/json"},
}
for i, tc := range tests {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
body, err := io.ReadAll(r.Body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, tc.expQueryParam, r.URL.RawQuery)
assert.Contains(t, "value1", r.Header.Get("Header1"))
assert.Contains(t, tc.expContentType, r.Header.Get("Content-Type"))
assert.Equal(t, string(tc.body), string(body))
w.WriteHeader(http.StatusOK)
}))
service := &httpService{
Client: http.DefaultClient,
url: server.URL,
Tracer: trace.NewTracerProvider().Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.INFO),
Metrics: metrics,
healthEndpoint: "",
healthTimeout: 0,
}
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", server.URL,
"method", http.MethodPost, "status", fmt.Sprintf("%v", http.StatusOK)).Times(1)
resp, err := service.createAndSendRequest(ctx,
http.MethodPost, "test-path", tc.queryParams, tc.body, tc.headers)
if err != nil {
if resp != nil {
resp.Body.Close()
}
}
require.NoError(t, err)
assert.NotNil(t, resp, "TEST[%d], Failed.\n%s", i, tc.desc)
server.Close()
}
}
func TestHTTPService_Get(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.Get(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}})
validateResponse(t, resp, err, false)
}
func TestHTTPService_GetWithHeaders(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "value1", r.Header.Get("Header1"))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.GetWithHeaders(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}},
map[string]string{"header1": "value1"})
validateResponse(t, resp, err, false)
}
func TestHTTPService_Put(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPut, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.Put(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"))
validateResponse(t, resp, err, false)
}
func TestHTTPService_PutWithHeaders(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPut, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "value1", r.Header.Get("Header1"))
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.PutWithHeaders(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"),
map[string]string{"header1": "value1"})
validateResponse(t, resp, err, false)
}
func TestHTTPService_Patch(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPatch, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.Patch(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"))
validateResponse(t, resp, err, false)
}
func TestHTTPService_PatchWithHeaders(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPatch, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "value1", r.Header.Get("Header1"))
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.PatchWithHeaders(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"),
map[string]string{"header1": "value1"})
validateResponse(t, resp, err, false)
}
func TestHTTPService_Post(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.Post(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"))
validateResponse(t, resp, err, false)
}
func TestHTTPService_PostWithHeaders(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Equal(t, "key=value&name=gofr&name=test", r.URL.RawQuery)
assert.Contains(t, "value1", r.Header.Get("Header1"))
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.PostWithHeaders(t.Context(), "test-path",
map[string]any{"key": "value", "name": []string{"gofr", "test"}}, []byte("{Test Body}"),
map[string]string{"header1": "value1"})
validateResponse(t, resp, err, false)
}
func TestHTTPService_Delete(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodDelete, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.Delete(t.Context(), "test-path", []byte("{Test Body}"))
validateResponse(t, resp, err, false)
}
func TestHTTPService_DeleteWithHeaders(t *testing.T) {
// Setup a test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// read request body
var body []byte
_, err := r.Body.Read(body)
if err != nil {
t.Fatal("Unable to read request body")
}
assert.Equal(t, http.MethodDelete, r.Method)
assert.Equal(t, "/test-path", r.URL.Path)
assert.Contains(t, "value1", r.Header.Get("Header1"))
assert.Contains(t, "Test Body", string(body))
w.WriteHeader(http.StatusOK)
}))
defer server.Close()
service := newService(t, server)
resp, err := service.DeleteWithHeaders(t.Context(), "test-path", []byte("{Test Body}"),
map[string]string{"header1": "value1"})
validateResponse(t, resp, err, false)
}
func TestHTTPService_createAndSendRequestCreateRequestFailure(t *testing.T) {
service := &httpService{
Client: http.DefaultClient,
Tracer: trace.NewTracerProvider().Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.INFO),
}
ctx := t.Context()
// when params value is of type []string then last value is sent in request
resp, err := service.createAndSendRequest(ctx,
"!@#$", "test-path", map[string]any{"key": "value", "name": []string{"gofr", "test"}},
[]byte("{Test Body}"), map[string]string{"header1": "value1"})
validateResponse(t, resp, err, true)
}
func TestHTTPService_createAndSendRequestServerError(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
service := &httpService{
Client: http.DefaultClient,
Tracer: trace.NewTracerProvider().Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.INFO),
Metrics: metrics,
}
ctx := t.Context()
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(), "path", gomock.Any(),
"method", http.MethodPost, "status", fmt.Sprintf("%v", http.StatusServiceUnavailable))
// when params value is of type []string then last value is sent in request
resp, err := service.createAndSendRequest(ctx,
http.MethodPost, "test-path", map[string]any{"key": "value", "name": []string{"gofr", "test"}},
[]byte("{Test Body}"), map[string]string{"header1": "value1"})
validateResponse(t, resp, err, true)
}
func validateResponse(t *testing.T, resp *http.Response, err error, hasError bool) {
t.Helper()
if resp != nil {
defer resp.Body.Close()
}
if hasError {
require.Error(t, err)
assert.Nil(t, resp, "TEST[%d], Failed.\n%s")
return
}
require.NoError(t, err)
assert.NotNil(t, resp, "TEST[%d], Failed.\n%s")
}
func newService(t *testing.T, server *httptest.Server) *httpService {
t.Helper()
tp := trace.NewTracerProvider()
return &httpService{
Client: http.DefaultClient,
url: server.URL,
Tracer: tp.Tracer("gofr-http-client"),
Logger: logging.NewMockLogger(logging.INFO),
}
}
================================================
FILE: pkg/gofr/service/oauth.go
================================================
package service
import (
"context"
"fmt"
"net/url"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
// OAuthConfig describes a 2-legged OAuth2 flow, with both the
// client application information and the server's endpoint URLs.
type OAuthConfig struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string
// Scope specifies optional requested permissions.
Scopes []string
// EndpointParams specifies additional parameters for requests to the token endpoint.
EndpointParams url.Values
// AuthStyle represents how requests for tokens are authenticated to the server
// Defaults to [oauth2.AuthStyleAutoDetect]
AuthStyle oauth2.AuthStyle
}
func (c *OAuthConfig) AddOption(svc HTTP) HTTP {
return &authProvider{auth: c.addAuthorizationHeader, HTTP: svc}
}
func NewOAuthConfig(clientID, secret, tokenURL string, scopes []string, params url.Values, authStyle oauth2.AuthStyle) (Options, error) {
if clientID == "" {
return nil, AuthErr{nil, "client id is mandatory"}
}
if secret == "" {
return nil, AuthErr{nil, "client secret is mandatory"}
}
if err := validateTokenURL(tokenURL); err != nil {
return nil, err
}
config := OAuthConfig{
ClientID: clientID,
ClientSecret: secret,
TokenURL: tokenURL,
Scopes: scopes,
EndpointParams: params,
AuthStyle: authStyle,
}
return &config, nil
}
func validateTokenURL(tokenURL string) error {
if tokenURL == "" {
return AuthErr{nil, "token url is mandatory"}
}
u, err := url.Parse(tokenURL)
switch {
case err != nil:
return AuthErr{err, "error in token URL"}
case u.Host == "" || u.Scheme == "":
return AuthErr{err, "empty host"}
case strings.Contains(u.Host, ".."):
return AuthErr{nil, "invalid host pattern, contains `..`"}
case strings.HasSuffix(u.Host, "."):
return AuthErr{nil, "invalid host pattern, ends with `.`"}
case u.Scheme != methodHTTP && u.Scheme != methodHTTPS:
return AuthErr{nil, "invalid scheme, allowed http and https only"}
default:
return nil
}
}
func (c *OAuthConfig) addAuthorizationHeader(ctx context.Context, headers map[string]string) (map[string]string, error) {
var err error
if headers == nil {
headers = make(map[string]string)
}
if authHeader, ok := headers[AuthHeader]; ok && authHeader != "" {
return nil, AuthErr{Message: fmt.Sprintf("value %v already exists for header %v", authHeader, AuthHeader)}
}
clientCredentials := clientcredentials.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
TokenURL: c.TokenURL,
Scopes: c.Scopes,
EndpointParams: c.EndpointParams,
AuthStyle: c.AuthStyle,
}
token, err := clientCredentials.TokenSource(ctx).Token()
if err != nil {
return nil, err
}
headers[AuthHeader] = fmt.Sprintf("%v %v", token.Type(), token.AccessToken)
return headers, nil
}
================================================
FILE: pkg/gofr/service/oauth_test.go
================================================
package service
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"golang.org/x/oauth2"
)
const invalidURL = "abc://invalid-url"
var (
errMissingTokenURL = errors.New(`unsupported protocol scheme ""`)
errIncorrectProtocol = errors.New(`unsupported protocol scheme "abc"`)
errInvalidCredentials = &oauth2.RetrieveError{Response: &http.Response{StatusCode: http.StatusUnauthorized}}
)
func TestNewOAuthConfig(t *testing.T) {
config := oAuthConfigForTests(t, "/token")
server := setupOAuthHTTPServer(t, config)
tokenURL := server.URL + config.TokenURL
clientID := config.ClientID
clientSecret := config.ClientSecret
testCases := []struct {
clientID string
clientSecret string
tokenURL string
scopes []string
params url.Values
authStyle oauth2.AuthStyle
err error
}{
{err: AuthErr{nil, "client id is mandatory"}},
{clientID: clientID, err: AuthErr{nil, "client secret is mandatory"}},
{clientID: clientID, tokenURL: tokenURL, err: AuthErr{nil, "client secret is mandatory"}},
{clientID: clientID, clientSecret: clientSecret, err: AuthErr{nil, "token url is mandatory"}},
{clientID: clientID, clientSecret: clientSecret, tokenURL: "invalid_url_format", err: AuthErr{nil, "empty host"}},
{clientID: clientID, clientSecret: clientSecret, tokenURL: tokenURL},
{clientID: clientID, clientSecret: "some_random_client_secret", tokenURL: tokenURL},
{clientID: "some_random_client_id", clientSecret: clientSecret, tokenURL: tokenURL},
{clientID: clientID, clientSecret: clientSecret, tokenURL: tokenURL, authStyle: 1},
{clientID: clientID, clientSecret: "some_random_client_secret", tokenURL: tokenURL, authStyle: 1},
{clientID: "some_random_client_id", clientSecret: clientSecret, tokenURL: tokenURL, authStyle: 2},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test case #%d", i), func(t *testing.T) {
config, err := NewOAuthConfig(tc.clientID, tc.clientSecret, tc.tokenURL, tc.scopes, tc.params, tc.authStyle)
assert.Equal(t, tc.err, err)
if tc.err != nil {
assert.Empty(t, config)
return
}
oAuthConfig, ok := config.(*OAuthConfig)
assert.True(t, ok, "failed to get OAuthConfig")
if oAuthConfig == nil {
t.Errorf("failed to get OAuthConfig")
return
}
assert.Equal(t, tc.clientID, oAuthConfig.ClientID)
assert.Equal(t, tc.clientSecret, oAuthConfig.ClientSecret)
assert.Equal(t, tc.tokenURL, oAuthConfig.TokenURL)
assert.Equal(t, tc.params, oAuthConfig.EndpointParams)
assert.Equal(t, tc.scopes, oAuthConfig.Scopes)
assert.Equal(t, tc.authStyle, oAuthConfig.AuthStyle)
})
}
}
func TestHttpService_validateTokenURL(t *testing.T) {
testCases := []struct {
tokenURL string
errMsg string
}{
{tokenURL: "https://www.example.com"},
{tokenURL: "https://www.example.com.", errMsg: "invalid host pattern, ends with `.`"},
{tokenURL: "https://www.192.168.1.1.com"},
{tokenURL: "https://www.192.168.1.1..com", errMsg: "invalid host pattern, contains `..`"},
{tokenURL: "ftp://www.192.168.1.1..com", errMsg: "invalid host pattern, contains `..`"},
{tokenURL: "ftp://www.192.168.1.1.com", errMsg: "invalid scheme, allowed http and https only"},
{tokenURL: "www.192.168.1.1.com", errMsg: "empty host"},
{tokenURL: "https://www.example.", errMsg: "invalid host pattern, ends with `.`"},
{errMsg: "token url is mandatory"},
{tokenURL: "invalid_url_format", errMsg: "empty host"},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test Case #%d", i), func(t *testing.T) {
err := validateTokenURL(tc.tokenURL)
if tc.errMsg != "" {
assert.ErrorContains(t, err, tc.errMsg)
}
})
}
}
func TestAddAuthorizationHeader_OAuth(t *testing.T) {
config := oAuthConfigForTests(t, "/token")
server := setupOAuthHTTPServer(t, config)
tokenURL := server.URL + config.TokenURL
emptyHeaders := map[string]string{}
headerWithAuth := map[string]string{AuthHeader: "Value"}
headerWithEmptyAuth := map[string]string{AuthHeader: ""}
headerWithoutAuth := map[string]string{"Content Type": "Value"}
headerWithEmptyAuthAndOtherValues := map[string]string{"Content Type": "Value", AuthHeader: ""}
authHeaderExistsError := AuthErr{Message: "value Value already exists for header Authorization"}
testCases := []struct {
tokenURL string
headers map[string]string
response map[string]string
err error
}{
{headers: headerWithAuth, err: authHeaderExistsError},
{err: &url.Error{Op: "Post", URL: "", Err: errMissingTokenURL}},
{tokenURL: tokenURL, headers: headerWithAuth, err: authHeaderExistsError},
{tokenURL: tokenURL, headers: headerWithEmptyAuth, response: emptyHeaders},
{tokenURL: tokenURL, headers: headerWithoutAuth, response: headerWithoutAuth},
{tokenURL: tokenURL, headers: headerWithEmptyAuthAndOtherValues, response: headerWithoutAuth},
}
for i, tc := range testCases {
t.Run(fmt.Sprintf("Test Case #%d", i), func(t *testing.T) {
config.TokenURL = tc.tokenURL
headers, err := config.addAuthorizationHeader(t.Context(), tc.headers)
assert.Equal(t, tc.err, err)
if err != nil {
return
}
authHeader, ok := headers[AuthHeader]
assert.True(t, ok)
assert.NotEmpty(t, authHeader)
assert.True(t, strings.HasPrefix(authHeader, "Bearer"))
delete(headers, AuthHeader)
assert.Equal(t, tc.response, headers)
})
}
}
// Helper method for getting OAuthConfig.
func oAuthConfigForTests(t *testing.T, tokenURL string) *OAuthConfig {
t.Helper()
config := &OAuthConfig{
TokenURL: tokenURL,
EndpointParams: map[string][]string{
"aud": {"some-random-value"},
},
AuthStyle: oauth2.AuthStyleInParams,
}
clientID, err := generateRandomString(clientIDLength)
if err != nil {
t.Fatalf("unable to generate random string for oAuthConfig")
}
config.ClientID = clientID
clientSecret, err := generateRandomString(clientSecretLength)
if err != nil {
t.Fatalf("unable to generate random string for oAuthConfig")
}
config.ClientSecret = clientSecret
return config
}
================================================
FILE: pkg/gofr/service/options.go
================================================
package service
type Options interface {
AddOption(h HTTP) HTTP
}
================================================
FILE: pkg/gofr/service/rate_limiter.go
================================================
package service
import (
"context"
"net/http"
"strings"
"time"
)
const (
defaultRequestsPerMinute = 60
defaultBurstCapacity = 10
defaultWindow = time.Minute
)
// rateLimiter provides unified rate limiting for HTTP clients.
type rateLimiter struct {
config RateLimiterConfig
store RateLimiterStore
HTTP // Embedded HTTP service
}
// NewRateLimiter creates a new unified rate limiter.
func NewRateLimiter(config RateLimiterConfig, h HTTP) HTTP {
rl := &rateLimiter{
config: config,
store: config.Store,
HTTP: h,
}
// Start cleanup routine
ctx := context.Background()
rl.store.StartCleanup(ctx)
return rl
}
// AddOption allows RateLimiterConfig to be used as a service.Options.
func (config *RateLimiterConfig) AddOption(h HTTP) HTTP {
// Validate always succeeds - it auto-corrects invalid values
_ = config.Validate()
// Assume cfg is already validated via constructor
if config.Store == nil {
config.Store = NewLocalRateLimiterStore()
}
return NewRateLimiter(*config, h)
}
// buildFullURL constructs an absolute URL by combining the base service URL with the given path.
func (rl *rateLimiter) buildFullURL(path string) string {
if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
return path
}
// Get base URL from embedded HTTP service
httpSvcImpl, ok := rl.HTTP.(*httpService)
if !ok {
return path
}
base := strings.TrimRight(httpSvcImpl.url, "/")
if base == "" {
return path
}
// Ensure path starts with /
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
return base + path
}
// checkRateLimit performs rate limit check using the configured store.
func (rl *rateLimiter) checkRateLimit(req *http.Request) error {
serviceKey := rl.config.KeyFunc(req)
allowed, retryAfter, err := rl.store.Allow(req.Context(), serviceKey, rl.config)
if err != nil {
return nil // Fail open
}
if !allowed {
return &RateLimitError{ServiceKey: serviceKey, RetryAfter: retryAfter}
}
return nil
}
// Get performs rate-limited HTTP GET request.
func (rl *rateLimiter) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.Get(ctx, path, queryParams)
}
// GetWithHeaders performs rate-limited HTTP GET request with custom headers.
func (rl *rateLimiter) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
}
// Post performs rate-limited HTTP POST request.
func (rl *rateLimiter) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.Post(ctx, path, queryParams, body)
}
// PostWithHeaders performs rate-limited HTTP POST request with custom headers.
func (rl *rateLimiter) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
}
// Put performs rate-limited HTTP PUT request.
func (rl *rateLimiter) Put(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPut, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.Put(ctx, path, queryParams, body)
}
// PutWithHeaders performs rate-limited HTTP PUT request with custom headers.
func (rl *rateLimiter) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPut, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
}
// Patch performs rate-limited HTTP PATCH request.
func (rl *rateLimiter) Patch(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.Patch(ctx, path, queryParams, body)
}
// PatchWithHeaders performs rate-limited HTTP PATCH request with custom headers.
func (rl *rateLimiter) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte, headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodPatch, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
}
// Delete performs rate-limited HTTP DELETE request.
func (rl *rateLimiter) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.Delete(ctx, path, body)
}
// DeleteWithHeaders performs rate-limited HTTP DELETE request with custom headers.
func (rl *rateLimiter) DeleteWithHeaders(ctx context.Context, path string, body []byte,
headers map[string]string) (*http.Response, error) {
fullURL := rl.buildFullURL(path)
req, _ := http.NewRequestWithContext(ctx, http.MethodDelete, fullURL, http.NoBody)
if err := rl.checkRateLimit(req); err != nil {
return nil, err
}
return rl.HTTP.DeleteWithHeaders(ctx, path, body, headers)
}
================================================
FILE: pkg/gofr/service/rate_limiter_config.go
================================================
package service
import (
"errors"
"fmt"
"net/http"
"time"
)
var (
errInvalidRequestRate = errors.New("requests must be greater than 0 per configured time window")
errBurstLessThanRequests = errors.New("burst must be greater than requests per window")
errInvalidRedisResultType = errors.New("unexpected Redis result type")
)
const (
unknownServiceKey = "unknown"
methodHTTP = "http"
methodHTTPS = "https"
)
// RateLimiterConfig with custom keying support.
type RateLimiterConfig struct {
Requests float64 // Number of requests allowed
Window time.Duration // Time window (e.g., time.Minute, time.Hour)
Burst int // Maximum burst capacity (must be > 0)
KeyFunc func(*http.Request) string // Optional custom key extraction
Store RateLimiterStore
}
// defaultKeyFunc extracts a normalized service key from an HTTP request.
func defaultKeyFunc(req *http.Request) string {
if req == nil || req.URL == nil {
return unknownServiceKey
}
scheme := req.URL.Scheme
host := req.URL.Host
if scheme == "" {
if req.TLS != nil {
scheme = methodHTTPS
} else {
scheme = methodHTTP
}
}
if host == "" {
host = req.Host
}
if host == "" {
host = unknownServiceKey
}
return scheme + "://" + host
}
// Validate checks if the configuration is valid.
// Validate checks if the configuration is valid and sets defaults.
func (config *RateLimiterConfig) Validate() error {
var validationError error
if config.Requests <= 0 {
validationError = fmt.Errorf("%w: %f", errInvalidRequestRate, config.Requests)
config.Requests = 60 // Default: 60 requests per minute
}
if config.Window <= 0 {
config.Window = defaultWindow
}
if config.Burst <= 0 {
config.Burst = defaultBurstCapacity // Default: allow burst of 10 requests
}
if float64(config.Burst) < config.Requests {
validationError = fmt.Errorf("%w: burst=%d, requests=%f", errBurstLessThanRequests, config.Burst, config.Requests)
config.Burst = int(config.Requests)
}
// Set default key function if not provided
if config.KeyFunc == nil {
config.KeyFunc = defaultKeyFunc
}
return validationError
}
// RequestsPerSecond converts the configured rate to requests per second.
func (config *RateLimiterConfig) RequestsPerSecond() float64 {
// Convert any time window to "requests per second" for internal math
return float64(config.Requests) / config.Window.Seconds()
}
// RateLimitError represents a rate limiting error.
type RateLimitError struct {
ServiceKey string
RetryAfter time.Duration
}
func (e *RateLimitError) Error() string {
return fmt.Sprintf("rate limit exceeded for service: %s, retry after: %v", e.ServiceKey, e.RetryAfter)
}
// StatusCode Implement StatusCodeResponder so Responder picks correct HTTP code.
func (*RateLimitError) StatusCode() int {
return http.StatusTooManyRequests // 429
}
================================================
FILE: pkg/gofr/service/rate_limiter_config_test.go
================================================
package service
import (
"crypto/tls"
"net/http"
"net/url"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRateLimiterConfig_Validate(t *testing.T) {
t.Run("invalid RPS", func(t *testing.T) {
cfg := RateLimiterConfig{Requests: 0, Burst: 1}
_ = cfg.Validate()
assert.Equal(t, defaultRequestsPerMinute, int(cfg.Requests))
})
t.Run("burst less than requests", func(t *testing.T) {
cfg := RateLimiterConfig{Requests: 5, Burst: 3}
err := cfg.Validate()
require.Error(t, err)
assert.ErrorIs(t, err, errBurstLessThanRequests)
})
t.Run("sets default KeyFunc when nil", func(t *testing.T) {
cfg := RateLimiterConfig{Requests: 1.5, Burst: 2}
require.Nil(t, cfg.KeyFunc)
require.NoError(t, cfg.Validate())
require.NotNil(t, cfg.KeyFunc)
})
}
func TestDefaultKeyFunc(t *testing.T) {
t.Run("nil request", func(t *testing.T) {
assert.Equal(t, "unknown", defaultKeyFunc(nil))
})
t.Run("nil URL", func(t *testing.T) {
req := &http.Request{}
assert.Equal(t, "unknown", defaultKeyFunc(req))
})
t.Run("http derived scheme", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Host: "example.com"},
}
assert.Equal(t, "http://example.com", defaultKeyFunc(req))
})
t.Run("https derived scheme", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Host: "secure.com"},
TLS: &tls.ConnectionState{},
}
assert.Equal(t, "https://secure.com", defaultKeyFunc(req))
})
t.Run("host from req.Host fallback", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{},
Host: "fallback:9090",
}
assert.Equal(t, "http://fallback:9090", defaultKeyFunc(req))
})
t.Run("unknown service key when no host present", func(t *testing.T) {
req := &http.Request{
URL: &url.URL{},
}
assert.Equal(t, "http://unknown", defaultKeyFunc(req))
})
}
func TestRequestsPerSecond(t *testing.T) {
cfg := RateLimiterConfig{Requests: 10, Window: 2 * time.Second}
assert.InEpsilon(t, 5.0, cfg.RequestsPerSecond(), 0.001)
}
func TestRateLimitError_ErrorAndStatusCode(t *testing.T) {
err := &RateLimitError{ServiceKey: "svc", RetryAfter: 2 * time.Second}
assert.Contains(t, err.Error(), "rate limit exceeded for service: svc")
assert.Equal(t, http.StatusTooManyRequests, err.StatusCode())
}
================================================
FILE: pkg/gofr/service/rate_limiter_store.go
================================================
package service
import (
"context"
"fmt"
"strconv"
"sync"
"sync/atomic"
"time"
gofrRedis "gofr.dev/pkg/gofr/datasource/redis"
)
const (
cleanupInterval = 5 * time.Minute // How often to clean up unused buckets
bucketTTL = 10 * time.Minute // How long to keep unused buckets
)
// RateLimiterStore abstracts the storage and cleanup for rate limiter buckets.
type RateLimiterStore interface {
Allow(ctx context.Context, key string, config RateLimiterConfig) (allowed bool, retryAfter time.Duration, err error)
StartCleanup(ctx context.Context)
StopCleanup()
}
// tokenBucket with simplified integer-only token handling.
type tokenBucket struct {
tokens int64 // Current tokens
lastRefillTime int64 // Unix nano timestamp
maxTokens int64 // Maximum tokens
refillRate int64 // Tokens per second (as integer)
}
// bucketEntry holds bucket with last access time for cleanup.
type bucketEntry struct {
bucket *tokenBucket
lastAccess int64 // Unix timestamp
}
// newTokenBucket creates a new token bucket with integer-only math.
func newTokenBucket(config *RateLimiterConfig) *tokenBucket {
maxTokens := int64(config.Burst)
refillRate := int64(config.RequestsPerSecond())
return &tokenBucket{
tokens: maxTokens,
lastRefillTime: time.Now().UnixNano(),
maxTokens: maxTokens,
refillRate: refillRate,
}
}
// allow checks if a token can be consumed.
func (tb *tokenBucket) allow() (allowed bool, waitTime time.Duration) {
now := time.Now().UnixNano()
// Calculate tokens to add based on elapsed time
elapsed := now - atomic.LoadInt64(&tb.lastRefillTime)
tokensToAdd := elapsed * tb.refillRate / int64(time.Second)
// Update tokens atomically
for {
oldTokens := atomic.LoadInt64(&tb.tokens)
newTokens := oldTokens + tokensToAdd
if newTokens > tb.maxTokens {
newTokens = tb.maxTokens
}
// Early return if not enough tokens
if newTokens < 1 {
waitTime := time.Duration((1-newTokens)*int64(time.Second)/tb.refillRate) * time.Nanosecond
if waitTime < time.Millisecond {
waitTime = time.Millisecond
}
return false, waitTime
}
// Try to consume a token
if atomic.CompareAndSwapInt64(&tb.tokens, oldTokens, newTokens-1) {
atomic.StoreInt64(&tb.lastRefillTime, now)
return true, 0
}
}
}
// LocalRateLimiterStore implements RateLimiterStore using in-memory buckets.
type LocalRateLimiterStore struct {
buckets *sync.Map
stopCh chan struct{}
}
func NewLocalRateLimiterStore() *LocalRateLimiterStore {
return &LocalRateLimiterStore{
buckets: &sync.Map{},
}
}
func (l *LocalRateLimiterStore) Allow(_ context.Context, key string, config RateLimiterConfig) (bool, time.Duration, error) {
now := time.Now().Unix()
entry, _ := l.buckets.LoadOrStore(key, &bucketEntry{
bucket: newTokenBucket(&config),
lastAccess: now,
})
bucketEntry := entry.(*bucketEntry)
atomic.StoreInt64(&bucketEntry.lastAccess, now)
allowed, retryAfter := bucketEntry.bucket.allow()
return allowed, retryAfter, nil
}
func (l *LocalRateLimiterStore) StartCleanup(ctx context.Context) {
l.stopCh = make(chan struct{})
go func() {
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
l.cleanupExpiredBuckets()
case <-l.stopCh:
return
case <-ctx.Done():
return
}
}
}()
}
func (l *LocalRateLimiterStore) StopCleanup() {
if l.stopCh != nil {
close(l.stopCh)
}
}
func (l *LocalRateLimiterStore) cleanupExpiredBuckets() {
cutoff := time.Now().Unix() - int64(bucketTTL.Seconds())
cleaned := 0
l.buckets.Range(func(key, value any) bool {
entry := value.(*bucketEntry)
if atomic.LoadInt64(&entry.lastAccess) < cutoff {
l.buckets.Delete(key)
cleaned++
}
return true
})
}
// tokenBucketScript is a Lua script for atomic token bucket rate limiting in Redis.
// Updated to use integer-only token math for simplicity
//
//nolint:gosec // This is a Lua script for Redis, not credentials
const tokenBucketScript = `
local key = KEYS[1]
local burst = tonumber(ARGV[1])
local requests = tonumber(ARGV[2])
local window_seconds = tonumber(ARGV[3])
local now = tonumber(ARGV[4])
-- Calculate refill rate as requests per second
local refill_rate = requests / window_seconds
-- Fetch bucket
local bucket = redis.call("HMGET", key, "tokens", "last_refill")
local tokens = tonumber(bucket[1])
local last_refill = tonumber(bucket[2])
if tokens == nil then
tokens = burst
last_refill = now
end
-- Refill tokens (integer math only)
local delta = math.max(0, (now - last_refill)/1e9)
local tokens_to_add = math.floor(delta * refill_rate)
local new_tokens = math.min(burst, tokens + tokens_to_add)
local allowed = 0
local retryAfter = 0
if new_tokens >= 1 then
allowed = 1
new_tokens = new_tokens - 1
else
retryAfter = math.ceil((1 - new_tokens) / refill_rate * 1000) -- ms
end
redis.call("HSET", key, "tokens", new_tokens, "last_refill", now)
redis.call("EXPIRE", key, 600)
return {allowed, retryAfter}
`
// RedisRateLimiterStore implements RateLimiterStore using Redis.
type RedisRateLimiterStore struct {
client *gofrRedis.Redis
}
func NewRedisRateLimiterStore(client *gofrRedis.Redis) *RedisRateLimiterStore {
return &RedisRateLimiterStore{client: client}
}
func (r *RedisRateLimiterStore) Allow(ctx context.Context, key string, config RateLimiterConfig) (bool, time.Duration, error) {
now := time.Now().UnixNano()
cmd := r.client.Eval(
ctx,
tokenBucketScript,
[]string{"gofr:ratelimit:" + key},
config.Burst, // ARGV[1]: burst
config.Requests, // ARGV[2]: requests
int64(config.Window.Seconds()), // ARGV[3]: window_seconds
now, // ARGV[4]: now (nanoseconds)
)
result, err := cmd.Result()
if err != nil {
return true, 0, err // Fail open
}
resultArray, ok := result.([]any)
if !ok || len(resultArray) != 2 {
return true, 0, errInvalidRedisResultType // Fail open
}
allowed, _ := toInt64(resultArray[0])
retryAfterMs, _ := toInt64(resultArray[1])
return allowed == 1, time.Duration(retryAfterMs) * time.Millisecond, nil
}
func (*RedisRateLimiterStore) StartCleanup(_ context.Context) {
// No-op: Redis handles cleanup automatically via EXPIRE commands in Lua script.
}
func (*RedisRateLimiterStore) StopCleanup() {
// No-op: Redis handles cleanup automatically.
}
// toInt64 safely converts Redis result to int64.
func toInt64(i any) (int64, error) {
switch v := i.(type) {
case int64:
return v, nil
case int:
return int64(v), nil
case float64:
return int64(v), nil
case string:
if v == "" {
return 0, nil
}
return strconv.ParseInt(v, 10, 64)
default:
return 0, fmt.Errorf("%w: %T", errInvalidRedisResultType, i)
}
}
================================================
FILE: pkg/gofr/service/rate_limiter_store_test.go
================================================
package service
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTokenBucket_Allow(t *testing.T) {
cfg := RateLimiterConfig{Requests: 2, Burst: 2, Window: time.Second}
tb := newTokenBucket(&cfg)
// Should allow first two requests
allowed, wait := tb.allow()
assert.True(t, allowed)
assert.Zero(t, wait)
allowed, wait = tb.allow()
assert.True(t, allowed)
assert.Zero(t, wait)
// Third request should be rate limited
allowed, wait = tb.allow()
assert.False(t, allowed)
assert.GreaterOrEqual(t, wait, time.Millisecond)
}
func TestLocalRateLimiterStore_Allow(t *testing.T) {
store := NewLocalRateLimiterStore()
cfg := RateLimiterConfig{Requests: 1, Burst: 1, Window: time.Second}
key := "test-key"
allowed, retry, err := store.Allow(context.Background(), key, cfg)
assert.True(t, allowed)
assert.Zero(t, retry)
require.NoError(t, err)
allowed, retry, err = store.Allow(context.Background(), key, cfg)
assert.False(t, allowed)
assert.GreaterOrEqual(t, retry, time.Millisecond)
assert.NoError(t, err)
}
func TestLocalRateLimiterStore_CleanupExpiredBuckets(t *testing.T) {
store := NewLocalRateLimiterStore()
cfg := RateLimiterConfig{Requests: 1, Burst: 1, Window: time.Second}
key := "cleanup-key"
_, _, err := store.Allow(context.Background(), key, cfg)
require.NoError(t, err)
// Simulate old lastAccess
entry, _ := store.buckets.Load(key)
bucketEntry := entry.(*bucketEntry)
bucketEntry.lastAccess = time.Now().Unix() - int64(bucketTTL.Seconds()) - 1
store.cleanupExpiredBuckets()
_, exists := store.buckets.Load(key)
assert.False(t, exists)
}
func TestLocalRateLimiterStore_StartAndStopCleanup(t *testing.T) {
store := NewLocalRateLimiterStore()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
store.StartCleanup(ctx)
assert.NotNil(t, store.stopCh)
store.StopCleanup()
}
func TestRedisRateLimiterStore_toInt64_ValidCases(t *testing.T) {
tests := []struct {
input any
expected int64
}{
{int64(5), 5},
{int(7), 7},
{float64(3.0), 3},
{"42", 42},
{"", 0},
}
for _, tc := range tests {
val, err := toInt64(tc.input)
require.NoError(t, err)
assert.Equal(t, tc.expected, val)
}
}
func TestRedisRateLimiterStore_toInt64_ErrorCases(t *testing.T) {
_, err := toInt64(struct{}{})
assert.ErrorIs(t, err, errInvalidRedisResultType)
}
================================================
FILE: pkg/gofr/service/rate_limiter_test.go
================================================
package service
import (
"context"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type mockStore struct {
allowed bool
retryAfter time.Duration
err error
}
func (m *mockStore) Allow(_ context.Context, _ string, _ RateLimiterConfig) (bool, time.Duration, error) {
return m.allowed, m.retryAfter, m.err
}
func (*mockStore) StartCleanup(_ context.Context) {}
func (*mockStore) StopCleanup() {}
func TestRateLimiter_buildFullURL(t *testing.T) {
httpSvc := &httpService{url: "http://base.com/api"}
rl := &rateLimiter{HTTP: httpSvc}
assert.Equal(t, "http://foo.com/bar", rl.buildFullURL("http://foo.com/bar"))
assert.Equal(t, "https://foo.com/bar", rl.buildFullURL("https://foo.com/bar"))
assert.Equal(t, "http://base.com/api/foo", rl.buildFullURL("foo"))
assert.Equal(t, "http://base.com/api/foo", rl.buildFullURL("/foo"))
httpSvc.url = ""
assert.Equal(t, "bar", rl.buildFullURL("bar"))
rl.HTTP = &mockHTTP{}
assert.Equal(t, "baz", rl.buildFullURL("baz"))
}
func TestRateLimiter_checkRateLimit_Error(t *testing.T) {
store := &mockStore{allowed: true, err: errTest}
rl := &rateLimiter{
config: RateLimiterConfig{
KeyFunc: func(*http.Request) string { return "svc" },
Store: store,
},
store: store,
}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
err := rl.checkRateLimit(req)
require.NoError(t, err)
}
func TestRateLimiter_checkRateLimit_Denied(t *testing.T) {
store := &mockStore{allowed: false}
rl := &rateLimiter{
config: RateLimiterConfig{
KeyFunc: func(*http.Request) string { return "svc" },
Store: store,
},
store: store,
}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
err := rl.checkRateLimit(req)
assert.IsType(t, &RateLimitError{}, err)
}
func TestRateLimiter_checkRateLimit_Allowed(t *testing.T) {
store := &mockStore{allowed: true}
rl := &rateLimiter{
config: RateLimiterConfig{
KeyFunc: func(*http.Request) string { return "svc" },
Store: store,
},
store: store,
}
req, _ := http.NewRequestWithContext(t.Context(), http.MethodGet, "/", http.NoBody)
err := rl.checkRateLimit(req)
assert.NoError(t, err)
}
func TestRateLimiter_HTTPMethods(t *testing.T) {
mock := &mockHTTP{}
store := &mockStore{allowed: true}
rl := &rateLimiter{
config: RateLimiterConfig{
KeyFunc: func(*http.Request) string { return "svc" },
Store: store,
},
store: store,
HTTP: mock,
}
ctx := context.Background()
resp, err := rl.Get(ctx, "foo", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.GetWithHeaders(ctx, "foo", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.Post(ctx, "foo", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.PostWithHeaders(ctx, "foo", nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.Put(ctx, "foo", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.PutWithHeaders(ctx, "foo", nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.Patch(ctx, "foo", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.PatchWithHeaders(ctx, "foo", nil, nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.Delete(ctx, "foo", nil)
require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
defer resp.Body.Close()
resp, err = rl.DeleteWithHeaders(ctx, "foo", nil, nil)
require.NoError(t, err)
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
_ = resp.Body.Close()
}
================================================
FILE: pkg/gofr/service/response.go
================================================
package service
import "net/http"
type Response struct {
Body []byte
StatusCode int
headers http.Header
}
func (r *Response) GetHeader(key string) string {
if r.headers != nil {
return r.headers.Get(key)
}
return ""
}
================================================
FILE: pkg/gofr/service/response_test.go
================================================
package service
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestResponse_GetHeader(t *testing.T) {
// Arrange
headers := http.Header{}
headers.Set("Content-Type", "application/json")
response := &Response{
headers: headers,
}
result := response.GetHeader("Content-Type")
headerNotFound := response.GetHeader("key")
assert.Equal(t, "application/json", result)
assert.Empty(t, headerNotFound)
}
func TestResponse_GetHeaderNil(t *testing.T) {
// Arrange
response := &Response{}
result := response.GetHeader("Content-Type")
assert.Empty(t, result)
}
================================================
FILE: pkg/gofr/service/retry.go
================================================
package service
import (
"context"
"net/http"
)
type RetryConfig struct {
MaxRetries int
}
func (r *RetryConfig) AddOption(h HTTP) HTTP {
rp := &retryProvider{
maxRetries: r.MaxRetries,
HTTP: h,
}
if httpSvc := extractHTTPService(h); httpSvc != nil {
rp.metrics = httpSvc.Metrics
rp.serviceName = httpSvc.name
}
return rp
}
type retryProvider struct {
maxRetries int
metrics Metrics
serviceName string
HTTP
}
func (rp *retryProvider) Get(ctx context.Context, path string, queryParams map[string]any) (*http.Response,
error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.Get(ctx, path, queryParams)
})
}
func (rp *retryProvider) GetWithHeaders(ctx context.Context, path string, queryParams map[string]any,
headers map[string]string) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.GetWithHeaders(ctx, path, queryParams, headers)
})
}
func (rp *retryProvider) Post(ctx context.Context, path string, queryParams map[string]any,
body []byte) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.Post(ctx, path, queryParams, body)
})
}
func (rp *retryProvider) PostWithHeaders(ctx context.Context, path string, queryParams map[string]any,
body []byte,
headers map[string]string) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.PostWithHeaders(ctx, path, queryParams, body, headers)
})
}
func (rp *retryProvider) Put(ctx context.Context, api string, queryParams map[string]any, body []byte) (
*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.Put(ctx, api, queryParams, body)
})
}
func (rp *retryProvider) PutWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.PutWithHeaders(ctx, path, queryParams, body, headers)
})
}
func (rp *retryProvider) Patch(ctx context.Context, path string, queryParams map[string]any, body []byte) (
*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.Patch(ctx, path, queryParams, body)
})
}
func (rp *retryProvider) PatchWithHeaders(ctx context.Context, path string, queryParams map[string]any, body []byte,
headers map[string]string) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.PatchWithHeaders(ctx, path, queryParams, body, headers)
})
}
func (rp *retryProvider) Delete(ctx context.Context, path string, body []byte) (*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.Delete(ctx, path, body)
})
}
func (rp *retryProvider) DeleteWithHeaders(ctx context.Context, path string, body []byte, headers map[string]string) (
*http.Response, error) {
return rp.doWithRetry(func() (*http.Response, error) {
return rp.HTTP.DeleteWithHeaders(ctx, path, body, headers)
})
}
func (rp *retryProvider) doWithRetry(reqFunc func() (*http.Response, error)) (*http.Response, error) {
var (
resp *http.Response
err error
)
for i := 0; i <= rp.maxRetries; i++ {
resp, err = reqFunc()
if err == nil && resp.StatusCode <= 500 {
return resp, nil
}
if i > 0 && rp.metrics != nil {
rp.metrics.IncrementCounter(context.Background(), "app_http_retry_count", "service", rp.serviceName)
}
}
return resp, err
}
================================================
FILE: pkg/gofr/service/retry_test.go
================================================
package service
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"gofr.dev/pkg/gofr/logging"
)
type mockHTTP struct{}
func (*mockHTTP) HealthCheck(_ context.Context) *Health {
return &Health{
Status: "UP",
Details: map[string]any{"host": "http://test.com"},
}
}
func (*mockHTTP) getHealthResponseForEndpoint(_ context.Context, _ string, _ int) *Health {
return &Health{
Status: "UP",
Details: map[string]any{"host": "http://test.com"},
}
}
func (*mockHTTP) Get(_ context.Context, _ string, _ map[string]any) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) GetWithHeaders(_ context.Context, _ string, _ map[string]any, _ map[string]string) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) Post(_ context.Context, _ string, _ map[string]any, _ []byte) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusCreated, Body: http.NoBody}, nil
}
func (*mockHTTP) PostWithHeaders(_ context.Context, _ string, _ map[string]any, _ []byte,
_ map[string]string) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusCreated, Body: http.NoBody}, nil
}
func (*mockHTTP) Put(_ context.Context, _ string, _ map[string]any, _ []byte) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) PutWithHeaders(_ context.Context, _ string, _ map[string]any, _ []byte,
_ map[string]string) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) Patch(_ context.Context, _ string, _ map[string]any, _ []byte) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) PatchWithHeaders(_ context.Context, _ string, _ map[string]any, _ []byte,
_ map[string]string) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}, nil
}
func (*mockHTTP) Delete(_ context.Context, _ string, _ []byte) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody}, nil
}
func (*mockHTTP) DeleteWithHeaders(_ context.Context, _ string, _ []byte, _ map[string]string) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusNoContent, Body: http.NoBody}, nil
}
// Helper to create a retry HTTP instance.
func newRetryHTTP() HTTP {
mockHTTP := &mockHTTP{}
retryConfig := &RetryConfig{MaxRetries: 3}
return retryConfig.AddOption(mockHTTP)
}
func TestRetryProvider_Get(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the GET request
resp, err := retryHTTP.Get(t.Context(), "/test", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRetryProvider_GetWithHeaders(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the GET request with headers
resp, err := retryHTTP.GetWithHeaders(t.Context(), "/test", nil,
map[string]string{"Content-Type": "application/json"})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRetryProvider_Post(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the POST request
resp, err := retryHTTP.Post(t.Context(), "/test", nil, []byte("body"))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
func TestRetryProvider_PostWithHeaders(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the POST request with headers
resp, err := retryHTTP.PostWithHeaders(t.Context(), "/test", nil, []byte("body"),
map[string]string{"Content-Type": "application/json"})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusCreated, resp.StatusCode)
}
func TestRetryProvider_Put(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the PUT request
resp, err := retryHTTP.Put(t.Context(), "/test", nil, []byte("body"))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRetryProvider_PutWithHeaders(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the PUT request with headers
resp, err := retryHTTP.PutWithHeaders(t.Context(), "/test", nil, []byte("body"),
map[string]string{"Content-Type": "application/json"})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRetryProvider_Patch_WithError(t *testing.T) {
// Create a mock HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checkAuthHeaders(t, r)
assert.Equal(t, http.MethodPatch, r.Method)
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
// Create a new HTTP service instance with basic auth
httpService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), nil,
&RetryConfig{MaxRetries: 5})
// Make the PATCH request
resp, err := httpService.Patch(t.Context(), "/test", nil, []byte("body"))
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
}
func TestRetryProvider_PatchWithHeaders(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the PATCH request with headers
resp, err := retryHTTP.PatchWithHeaders(t.Context(), "/test", nil, []byte("body"),
map[string]string{"Content-Type": "application/json"})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestRetryProvider_Delete(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the DELETE request
resp, err := retryHTTP.Delete(t.Context(), "/test", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}
func TestRetryProvider_DeleteWithHeaders(t *testing.T) {
retryHTTP := newRetryHTTP()
// Make the DELETE request with headers
resp, err := retryHTTP.DeleteWithHeaders(t.Context(), "/test", []byte("body"),
map[string]string{"Content-Type": "application/json"})
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}
func TestRetryProvider_Metrics(t *testing.T) {
ctrl := gomock.NewController(t)
metrics := NewMockMetrics(ctrl)
// Expect NewCounter and IncrementCounter to be called with labels
metrics.EXPECT().NewCounter("app_http_retry_count", gomock.Any()).AnyTimes()
metrics.EXPECT().IncrementCounter(gomock.Any(), "app_http_retry_count", "service", "test-service").MinTimes(1)
metrics.EXPECT().RecordHistogram(gomock.Any(), "app_http_service_response", gomock.Any(),
gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(),
gomock.Any(), gomock.Any()).AnyTimes()
// Create a mock HTTP server that fails
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
defer server.Close()
// Create a new HTTP service instance with retry config, metrics and name
httpService := NewHTTPService(server.URL, logging.NewMockLogger(logging.INFO), metrics,
WithAttributes(map[string]string{"name": "test-service"}), &RetryConfig{MaxRetries: 2})
// Make the request
resp, err := httpService.Get(t.Context(), "/test", nil)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
}
================================================
FILE: pkg/gofr/shutdown.go
================================================
package gofr
import (
"context"
"errors"
"time"
"gofr.dev/pkg/gofr/config"
)
// ShutdownWithContext handles the shutdown process with context timeout.
// It takes a shutdown function and a force close function as parameters.
// If the context times out, the force close function is called.
func ShutdownWithContext(ctx context.Context, shutdownFunc func(ctx context.Context) error, forceCloseFunc func() error) error {
errCh := make(chan error, 1) // Channel to receive shutdown error
go func() {
errCh <- shutdownFunc(ctx) // Run shutdownFunc in a goroutine and send any error to errCh
}()
// Wait for either the context to be done or shutdownFunc to complete
select {
case <-ctx.Done(): // Context timeout reached
err := ctx.Err()
if forceCloseFunc != nil {
err = errors.Join(err, forceCloseFunc()) // Attempt force close if available
}
return err
case err := <-errCh:
return err
}
}
func getShutdownTimeoutFromConfig(cfg config.Config) (time.Duration, error) {
value := cfg.GetOrDefault("SHUTDOWN_GRACE_PERIOD", "30s")
if value == "" {
return shutDownTimeout, nil
}
timeout, err := time.ParseDuration(value)
if err != nil {
return shutDownTimeout, err
}
return timeout, nil
}
================================================
FILE: pkg/gofr/shutdown_test.go
================================================
package gofr
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gofr.dev/pkg/gofr/config"
)
func TestShutdownWithContext_ContextTimeout(t *testing.T) {
// Mock shutdown function that never completes
mockShutdownFunc := func(ctx context.Context) error {
// Simulate a long-running process
<-ctx.Done()
return nil
}
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
err := ShutdownWithContext(ctx, mockShutdownFunc, nil)
require.ErrorIs(t, err, context.DeadlineExceeded, "Expected context deadline exceeded error")
}
func TestShutdownWithContext_SuccessfulShutdown(t *testing.T) {
// Mock shutdown function that completes successfully
mockShutdownFunc := func(_ context.Context) error {
// Simulate a quick shutdown
time.Sleep(50 * time.Millisecond)
return nil
}
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
defer cancel()
err := ShutdownWithContext(ctx, mockShutdownFunc, nil)
require.NoError(t, err, "Expected successful shutdown without error")
}
func Test_getShutdownTimeoutFromConfig_Success(t *testing.T) {
tests := []struct {
name string
configValue string
expectedValue time.Duration
}{
{"Valid timeout", "1s", 1 * time.Second},
{"Empty timeout", "", 30 * time.Second},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"SHUTDOWN_GRACE_PERIOD": tt.configValue,
})
timeout, err := getShutdownTimeoutFromConfig(mockConfig)
require.NoError(t, err)
assert.Equal(t, tt.expectedValue, timeout)
})
}
}
func Test_getShutdownTimeoutFromConfig_Error(t *testing.T) {
mockConfig := config.NewMockConfig(map[string]string{
"SHUTDOWN_GRACE_PERIOD": "invalid",
})
_, err := getShutdownTimeoutFromConfig(mockConfig)
require.Error(t, err)
}
================================================
FILE: pkg/gofr/static/files.go
================================================
package static
import "embed"
//go:embed *
var Files embed.FS
================================================
FILE: pkg/gofr/static/index.css
================================================
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
================================================
FILE: pkg/gofr/static/index.html
================================================
Swagger UI
================================================
FILE: pkg/gofr/static/oauth2-redirect.html
================================================
Swagger UI: OAuth2 Redirect
================================================
FILE: pkg/gofr/static/swagger-ui-bundle.js
================================================
/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(s,i){"object"==typeof exports&&"object"==typeof module?module.exports=i():"function"==typeof define&&define.amd?define([],i):"object"==typeof exports?exports.SwaggerUIBundle=i():s.SwaggerUIBundle=i()}(this,(()=>(()=>{var s,i,u={69119:(s,i)=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.BLANK_URL=i.relativeFirstCharacters=i.urlSchemeRegex=i.ctrlCharactersRegex=i.htmlCtrlEntityRegex=i.htmlEntitiesRegex=i.invalidProtocolRegex=void 0,i.invalidProtocolRegex=/^([^\w]*)(javascript|data|vbscript)/im,i.htmlEntitiesRegex=/(\w+)(^\w|;)?/g,i.htmlCtrlEntityRegex=/&(newline|tab);/gi,i.ctrlCharactersRegex=/[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim,i.urlSchemeRegex=/^.+(:|:)/gim,i.relativeFirstCharacters=[".","/"],i.BLANK_URL="about:blank"},16750:(s,i,u)=>{"use strict";i.J=void 0;var _=u(69119);i.J=function sanitizeUrl(s){if(!s)return _.BLANK_URL;var i,u,w=s;do{i=(w=(u=w,u.replace(_.ctrlCharactersRegex,"").replace(_.htmlEntitiesRegex,(function(s,i){return String.fromCharCode(i)}))).replace(_.htmlCtrlEntityRegex,"").replace(_.ctrlCharactersRegex,"").trim()).match(_.ctrlCharactersRegex)||w.match(_.htmlEntitiesRegex)||w.match(_.htmlCtrlEntityRegex)}while(i&&i.length>0);var x=w;if(!x)return _.BLANK_URL;if(function isRelativeUrlWithoutProtocol(s){return _.relativeFirstCharacters.indexOf(s[0])>-1}(x))return x;var j=x.match(_.urlSchemeRegex);if(!j)return x;var L=j[0];return _.invalidProtocolRegex.test(L)?_.BLANK_URL:x}},67526:(s,i)=>{"use strict";i.byteLength=function byteLength(s){var i=getLens(s),u=i[0],_=i[1];return 3*(u+_)/4-_},i.toByteArray=function toByteArray(s){var i,u,x=getLens(s),j=x[0],L=x[1],B=new w(function _byteLength(s,i,u){return 3*(i+u)/4-u}(0,j,L)),$=0,U=L>0?j-4:j;for(u=0;u>16&255,B[$++]=i>>8&255,B[$++]=255&i;2===L&&(i=_[s.charCodeAt(u)]<<2|_[s.charCodeAt(u+1)]>>4,B[$++]=255&i);1===L&&(i=_[s.charCodeAt(u)]<<10|_[s.charCodeAt(u+1)]<<4|_[s.charCodeAt(u+2)]>>2,B[$++]=i>>8&255,B[$++]=255&i);return B},i.fromByteArray=function fromByteArray(s){for(var i,_=s.length,w=_%3,x=[],j=16383,L=0,B=_-w;LB?B:L+j));1===w?(i=s[_-1],x.push(u[i>>2]+u[i<<4&63]+"==")):2===w&&(i=(s[_-2]<<8)+s[_-1],x.push(u[i>>10]+u[i>>4&63]+u[i<<2&63]+"="));return x.join("")};for(var u=[],_=[],w="undefined"!=typeof Uint8Array?Uint8Array:Array,x="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",j=0;j<64;++j)u[j]=x[j],_[x.charCodeAt(j)]=j;function getLens(s){var i=s.length;if(i%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var u=s.indexOf("=");return-1===u&&(u=i),[u,u===i?0:4-u%4]}function encodeChunk(s,i,_){for(var w,x,j=[],L=i;L<_;L+=3)w=(s[L]<<16&16711680)+(s[L+1]<<8&65280)+(255&s[L+2]),j.push(u[(x=w)>>18&63]+u[x>>12&63]+u[x>>6&63]+u[63&x]);return j.join("")}_["-".charCodeAt(0)]=62,_["_".charCodeAt(0)]=63},48287:(s,i,u)=>{"use strict";const _=u(67526),w=u(251),x="function"==typeof Symbol&&"function"==typeof Symbol.for?Symbol.for("nodejs.util.inspect.custom"):null;i.Buffer=Buffer,i.SlowBuffer=function SlowBuffer(s){+s!=s&&(s=0);return Buffer.alloc(+s)},i.INSPECT_MAX_BYTES=50;const j=2147483647;function createBuffer(s){if(s>j)throw new RangeError('The value "'+s+'" is invalid for option "size"');const i=new Uint8Array(s);return Object.setPrototypeOf(i,Buffer.prototype),i}function Buffer(s,i,u){if("number"==typeof s){if("string"==typeof i)throw new TypeError('The "string" argument must be of type string. Received type number');return allocUnsafe(s)}return from(s,i,u)}function from(s,i,u){if("string"==typeof s)return function fromString(s,i){"string"==typeof i&&""!==i||(i="utf8");if(!Buffer.isEncoding(i))throw new TypeError("Unknown encoding: "+i);const u=0|byteLength(s,i);let _=createBuffer(u);const w=_.write(s,i);w!==u&&(_=_.slice(0,w));return _}(s,i);if(ArrayBuffer.isView(s))return function fromArrayView(s){if(isInstance(s,Uint8Array)){const i=new Uint8Array(s);return fromArrayBuffer(i.buffer,i.byteOffset,i.byteLength)}return fromArrayLike(s)}(s);if(null==s)throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s);if(isInstance(s,ArrayBuffer)||s&&isInstance(s.buffer,ArrayBuffer))return fromArrayBuffer(s,i,u);if("undefined"!=typeof SharedArrayBuffer&&(isInstance(s,SharedArrayBuffer)||s&&isInstance(s.buffer,SharedArrayBuffer)))return fromArrayBuffer(s,i,u);if("number"==typeof s)throw new TypeError('The "value" argument must not be of type number. Received type number');const _=s.valueOf&&s.valueOf();if(null!=_&&_!==s)return Buffer.from(_,i,u);const w=function fromObject(s){if(Buffer.isBuffer(s)){const i=0|checked(s.length),u=createBuffer(i);return 0===u.length||s.copy(u,0,0,i),u}if(void 0!==s.length)return"number"!=typeof s.length||numberIsNaN(s.length)?createBuffer(0):fromArrayLike(s);if("Buffer"===s.type&&Array.isArray(s.data))return fromArrayLike(s.data)}(s);if(w)return w;if("undefined"!=typeof Symbol&&null!=Symbol.toPrimitive&&"function"==typeof s[Symbol.toPrimitive])return Buffer.from(s[Symbol.toPrimitive]("string"),i,u);throw new TypeError("The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type "+typeof s)}function assertSize(s){if("number"!=typeof s)throw new TypeError('"size" argument must be of type number');if(s<0)throw new RangeError('The value "'+s+'" is invalid for option "size"')}function allocUnsafe(s){return assertSize(s),createBuffer(s<0?0:0|checked(s))}function fromArrayLike(s){const i=s.length<0?0:0|checked(s.length),u=createBuffer(i);for(let _=0;_=j)throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+j.toString(16)+" bytes");return 0|s}function byteLength(s,i){if(Buffer.isBuffer(s))return s.length;if(ArrayBuffer.isView(s)||isInstance(s,ArrayBuffer))return s.byteLength;if("string"!=typeof s)throw new TypeError('The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type '+typeof s);const u=s.length,_=arguments.length>2&&!0===arguments[2];if(!_&&0===u)return 0;let w=!1;for(;;)switch(i){case"ascii":case"latin1":case"binary":return u;case"utf8":case"utf-8":return utf8ToBytes(s).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*u;case"hex":return u>>>1;case"base64":return base64ToBytes(s).length;default:if(w)return _?-1:utf8ToBytes(s).length;i=(""+i).toLowerCase(),w=!0}}function slowToString(s,i,u){let _=!1;if((void 0===i||i<0)&&(i=0),i>this.length)return"";if((void 0===u||u>this.length)&&(u=this.length),u<=0)return"";if((u>>>=0)<=(i>>>=0))return"";for(s||(s="utf8");;)switch(s){case"hex":return hexSlice(this,i,u);case"utf8":case"utf-8":return utf8Slice(this,i,u);case"ascii":return asciiSlice(this,i,u);case"latin1":case"binary":return latin1Slice(this,i,u);case"base64":return base64Slice(this,i,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return utf16leSlice(this,i,u);default:if(_)throw new TypeError("Unknown encoding: "+s);s=(s+"").toLowerCase(),_=!0}}function swap(s,i,u){const _=s[i];s[i]=s[u],s[u]=_}function bidirectionalIndexOf(s,i,u,_,w){if(0===s.length)return-1;if("string"==typeof u?(_=u,u=0):u>2147483647?u=2147483647:u<-2147483648&&(u=-2147483648),numberIsNaN(u=+u)&&(u=w?0:s.length-1),u<0&&(u=s.length+u),u>=s.length){if(w)return-1;u=s.length-1}else if(u<0){if(!w)return-1;u=0}if("string"==typeof i&&(i=Buffer.from(i,_)),Buffer.isBuffer(i))return 0===i.length?-1:arrayIndexOf(s,i,u,_,w);if("number"==typeof i)return i&=255,"function"==typeof Uint8Array.prototype.indexOf?w?Uint8Array.prototype.indexOf.call(s,i,u):Uint8Array.prototype.lastIndexOf.call(s,i,u):arrayIndexOf(s,[i],u,_,w);throw new TypeError("val must be string, number or Buffer")}function arrayIndexOf(s,i,u,_,w){let x,j=1,L=s.length,B=i.length;if(void 0!==_&&("ucs2"===(_=String(_).toLowerCase())||"ucs-2"===_||"utf16le"===_||"utf-16le"===_)){if(s.length<2||i.length<2)return-1;j=2,L/=2,B/=2,u/=2}function read(s,i){return 1===j?s[i]:s.readUInt16BE(i*j)}if(w){let _=-1;for(x=u;xL&&(u=L-B),x=u;x>=0;x--){let u=!0;for(let _=0;_w&&(_=w):_=w;const x=i.length;let j;for(_>x/2&&(_=x/2),j=0;j<_;++j){const _=parseInt(i.substr(2*j,2),16);if(numberIsNaN(_))return j;s[u+j]=_}return j}function utf8Write(s,i,u,_){return blitBuffer(utf8ToBytes(i,s.length-u),s,u,_)}function asciiWrite(s,i,u,_){return blitBuffer(function asciiToBytes(s){const i=[];for(let u=0;u>8,w=u%256,x.push(w),x.push(_);return x}(i,s.length-u),s,u,_)}function base64Slice(s,i,u){return 0===i&&u===s.length?_.fromByteArray(s):_.fromByteArray(s.slice(i,u))}function utf8Slice(s,i,u){u=Math.min(s.length,u);const _=[];let w=i;for(;w239?4:i>223?3:i>191?2:1;if(w+j<=u){let u,_,L,B;switch(j){case 1:i<128&&(x=i);break;case 2:u=s[w+1],128==(192&u)&&(B=(31&i)<<6|63&u,B>127&&(x=B));break;case 3:u=s[w+1],_=s[w+2],128==(192&u)&&128==(192&_)&&(B=(15&i)<<12|(63&u)<<6|63&_,B>2047&&(B<55296||B>57343)&&(x=B));break;case 4:u=s[w+1],_=s[w+2],L=s[w+3],128==(192&u)&&128==(192&_)&&128==(192&L)&&(B=(15&i)<<18|(63&u)<<12|(63&_)<<6|63&L,B>65535&&B<1114112&&(x=B))}}null===x?(x=65533,j=1):x>65535&&(x-=65536,_.push(x>>>10&1023|55296),x=56320|1023&x),_.push(x),w+=j}return function decodeCodePointsArray(s){const i=s.length;if(i<=L)return String.fromCharCode.apply(String,s);let u="",_=0;for(;__.length?(Buffer.isBuffer(i)||(i=Buffer.from(i)),i.copy(_,w)):Uint8Array.prototype.set.call(_,i,w);else{if(!Buffer.isBuffer(i))throw new TypeError('"list" argument must be an Array of Buffers');i.copy(_,w)}w+=i.length}return _},Buffer.byteLength=byteLength,Buffer.prototype._isBuffer=!0,Buffer.prototype.swap16=function swap16(){const s=this.length;if(s%2!=0)throw new RangeError("Buffer size must be a multiple of 16-bits");for(let i=0;iu&&(s+=" ... "),""},x&&(Buffer.prototype[x]=Buffer.prototype.inspect),Buffer.prototype.compare=function compare(s,i,u,_,w){if(isInstance(s,Uint8Array)&&(s=Buffer.from(s,s.offset,s.byteLength)),!Buffer.isBuffer(s))throw new TypeError('The "target" argument must be one of type Buffer or Uint8Array. Received type '+typeof s);if(void 0===i&&(i=0),void 0===u&&(u=s?s.length:0),void 0===_&&(_=0),void 0===w&&(w=this.length),i<0||u>s.length||_<0||w>this.length)throw new RangeError("out of range index");if(_>=w&&i>=u)return 0;if(_>=w)return-1;if(i>=u)return 1;if(this===s)return 0;let x=(w>>>=0)-(_>>>=0),j=(u>>>=0)-(i>>>=0);const L=Math.min(x,j),B=this.slice(_,w),$=s.slice(i,u);for(let s=0;s>>=0,isFinite(u)?(u>>>=0,void 0===_&&(_="utf8")):(_=u,u=void 0)}const w=this.length-i;if((void 0===u||u>w)&&(u=w),s.length>0&&(u<0||i<0)||i>this.length)throw new RangeError("Attempt to write outside buffer bounds");_||(_="utf8");let x=!1;for(;;)switch(_){case"hex":return hexWrite(this,s,i,u);case"utf8":case"utf-8":return utf8Write(this,s,i,u);case"ascii":case"latin1":case"binary":return asciiWrite(this,s,i,u);case"base64":return base64Write(this,s,i,u);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return ucs2Write(this,s,i,u);default:if(x)throw new TypeError("Unknown encoding: "+_);_=(""+_).toLowerCase(),x=!0}},Buffer.prototype.toJSON=function toJSON(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};const L=4096;function asciiSlice(s,i,u){let _="";u=Math.min(s.length,u);for(let w=i;w_)&&(u=_);let w="";for(let _=i;_u)throw new RangeError("Trying to access beyond buffer length")}function checkInt(s,i,u,_,w,x){if(!Buffer.isBuffer(s))throw new TypeError('"buffer" argument must be a Buffer instance');if(i>w||is.length)throw new RangeError("Index out of range")}function wrtBigUInt64LE(s,i,u,_,w){checkIntBI(i,_,w,s,u,7);let x=Number(i&BigInt(4294967295));s[u++]=x,x>>=8,s[u++]=x,x>>=8,s[u++]=x,x>>=8,s[u++]=x;let j=Number(i>>BigInt(32)&BigInt(4294967295));return s[u++]=j,j>>=8,s[u++]=j,j>>=8,s[u++]=j,j>>=8,s[u++]=j,u}function wrtBigUInt64BE(s,i,u,_,w){checkIntBI(i,_,w,s,u,7);let x=Number(i&BigInt(4294967295));s[u+7]=x,x>>=8,s[u+6]=x,x>>=8,s[u+5]=x,x>>=8,s[u+4]=x;let j=Number(i>>BigInt(32)&BigInt(4294967295));return s[u+3]=j,j>>=8,s[u+2]=j,j>>=8,s[u+1]=j,j>>=8,s[u]=j,u+8}function checkIEEE754(s,i,u,_,w,x){if(u+_>s.length)throw new RangeError("Index out of range");if(u<0)throw new RangeError("Index out of range")}function writeFloat(s,i,u,_,x){return i=+i,u>>>=0,x||checkIEEE754(s,0,u,4),w.write(s,i,u,_,23,4),u+4}function writeDouble(s,i,u,_,x){return i=+i,u>>>=0,x||checkIEEE754(s,0,u,8),w.write(s,i,u,_,52,8),u+8}Buffer.prototype.slice=function slice(s,i){const u=this.length;(s=~~s)<0?(s+=u)<0&&(s=0):s>u&&(s=u),(i=void 0===i?u:~~i)<0?(i+=u)<0&&(i=0):i>u&&(i=u),i>>=0,i>>>=0,u||checkOffset(s,i,this.length);let _=this[s],w=1,x=0;for(;++x>>=0,i>>>=0,u||checkOffset(s,i,this.length);let _=this[s+--i],w=1;for(;i>0&&(w*=256);)_+=this[s+--i]*w;return _},Buffer.prototype.readUint8=Buffer.prototype.readUInt8=function readUInt8(s,i){return s>>>=0,i||checkOffset(s,1,this.length),this[s]},Buffer.prototype.readUint16LE=Buffer.prototype.readUInt16LE=function readUInt16LE(s,i){return s>>>=0,i||checkOffset(s,2,this.length),this[s]|this[s+1]<<8},Buffer.prototype.readUint16BE=Buffer.prototype.readUInt16BE=function readUInt16BE(s,i){return s>>>=0,i||checkOffset(s,2,this.length),this[s]<<8|this[s+1]},Buffer.prototype.readUint32LE=Buffer.prototype.readUInt32LE=function readUInt32LE(s,i){return s>>>=0,i||checkOffset(s,4,this.length),(this[s]|this[s+1]<<8|this[s+2]<<16)+16777216*this[s+3]},Buffer.prototype.readUint32BE=Buffer.prototype.readUInt32BE=function readUInt32BE(s,i){return s>>>=0,i||checkOffset(s,4,this.length),16777216*this[s]+(this[s+1]<<16|this[s+2]<<8|this[s+3])},Buffer.prototype.readBigUInt64LE=defineBigIntMethod((function readBigUInt64LE(s){validateNumber(s>>>=0,"offset");const i=this[s],u=this[s+7];void 0!==i&&void 0!==u||boundsError(s,this.length-8);const _=i+256*this[++s]+65536*this[++s]+this[++s]*2**24,w=this[++s]+256*this[++s]+65536*this[++s]+u*2**24;return BigInt(_)+(BigInt(w)<>>=0,"offset");const i=this[s],u=this[s+7];void 0!==i&&void 0!==u||boundsError(s,this.length-8);const _=i*2**24+65536*this[++s]+256*this[++s]+this[++s],w=this[++s]*2**24+65536*this[++s]+256*this[++s]+u;return(BigInt(_)<>>=0,i>>>=0,u||checkOffset(s,i,this.length);let _=this[s],w=1,x=0;for(;++x=w&&(_-=Math.pow(2,8*i)),_},Buffer.prototype.readIntBE=function readIntBE(s,i,u){s>>>=0,i>>>=0,u||checkOffset(s,i,this.length);let _=i,w=1,x=this[s+--_];for(;_>0&&(w*=256);)x+=this[s+--_]*w;return w*=128,x>=w&&(x-=Math.pow(2,8*i)),x},Buffer.prototype.readInt8=function readInt8(s,i){return s>>>=0,i||checkOffset(s,1,this.length),128&this[s]?-1*(255-this[s]+1):this[s]},Buffer.prototype.readInt16LE=function readInt16LE(s,i){s>>>=0,i||checkOffset(s,2,this.length);const u=this[s]|this[s+1]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt16BE=function readInt16BE(s,i){s>>>=0,i||checkOffset(s,2,this.length);const u=this[s+1]|this[s]<<8;return 32768&u?4294901760|u:u},Buffer.prototype.readInt32LE=function readInt32LE(s,i){return s>>>=0,i||checkOffset(s,4,this.length),this[s]|this[s+1]<<8|this[s+2]<<16|this[s+3]<<24},Buffer.prototype.readInt32BE=function readInt32BE(s,i){return s>>>=0,i||checkOffset(s,4,this.length),this[s]<<24|this[s+1]<<16|this[s+2]<<8|this[s+3]},Buffer.prototype.readBigInt64LE=defineBigIntMethod((function readBigInt64LE(s){validateNumber(s>>>=0,"offset");const i=this[s],u=this[s+7];void 0!==i&&void 0!==u||boundsError(s,this.length-8);const _=this[s+4]+256*this[s+5]+65536*this[s+6]+(u<<24);return(BigInt(_)<>>=0,"offset");const i=this[s],u=this[s+7];void 0!==i&&void 0!==u||boundsError(s,this.length-8);const _=(i<<24)+65536*this[++s]+256*this[++s]+this[++s];return(BigInt(_)<>>=0,i||checkOffset(s,4,this.length),w.read(this,s,!0,23,4)},Buffer.prototype.readFloatBE=function readFloatBE(s,i){return s>>>=0,i||checkOffset(s,4,this.length),w.read(this,s,!1,23,4)},Buffer.prototype.readDoubleLE=function readDoubleLE(s,i){return s>>>=0,i||checkOffset(s,8,this.length),w.read(this,s,!0,52,8)},Buffer.prototype.readDoubleBE=function readDoubleBE(s,i){return s>>>=0,i||checkOffset(s,8,this.length),w.read(this,s,!1,52,8)},Buffer.prototype.writeUintLE=Buffer.prototype.writeUIntLE=function writeUIntLE(s,i,u,_){if(s=+s,i>>>=0,u>>>=0,!_){checkInt(this,s,i,u,Math.pow(2,8*u)-1,0)}let w=1,x=0;for(this[i]=255&s;++x>>=0,u>>>=0,!_){checkInt(this,s,i,u,Math.pow(2,8*u)-1,0)}let w=u-1,x=1;for(this[i+w]=255&s;--w>=0&&(x*=256);)this[i+w]=s/x&255;return i+u},Buffer.prototype.writeUint8=Buffer.prototype.writeUInt8=function writeUInt8(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,1,255,0),this[i]=255&s,i+1},Buffer.prototype.writeUint16LE=Buffer.prototype.writeUInt16LE=function writeUInt16LE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,2,65535,0),this[i]=255&s,this[i+1]=s>>>8,i+2},Buffer.prototype.writeUint16BE=Buffer.prototype.writeUInt16BE=function writeUInt16BE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,2,65535,0),this[i]=s>>>8,this[i+1]=255&s,i+2},Buffer.prototype.writeUint32LE=Buffer.prototype.writeUInt32LE=function writeUInt32LE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,4,4294967295,0),this[i+3]=s>>>24,this[i+2]=s>>>16,this[i+1]=s>>>8,this[i]=255&s,i+4},Buffer.prototype.writeUint32BE=Buffer.prototype.writeUInt32BE=function writeUInt32BE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,4,4294967295,0),this[i]=s>>>24,this[i+1]=s>>>16,this[i+2]=s>>>8,this[i+3]=255&s,i+4},Buffer.prototype.writeBigUInt64LE=defineBigIntMethod((function writeBigUInt64LE(s,i=0){return wrtBigUInt64LE(this,s,i,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeBigUInt64BE=defineBigIntMethod((function writeBigUInt64BE(s,i=0){return wrtBigUInt64BE(this,s,i,BigInt(0),BigInt("0xffffffffffffffff"))})),Buffer.prototype.writeIntLE=function writeIntLE(s,i,u,_){if(s=+s,i>>>=0,!_){const _=Math.pow(2,8*u-1);checkInt(this,s,i,u,_-1,-_)}let w=0,x=1,j=0;for(this[i]=255&s;++w>0)-j&255;return i+u},Buffer.prototype.writeIntBE=function writeIntBE(s,i,u,_){if(s=+s,i>>>=0,!_){const _=Math.pow(2,8*u-1);checkInt(this,s,i,u,_-1,-_)}let w=u-1,x=1,j=0;for(this[i+w]=255&s;--w>=0&&(x*=256);)s<0&&0===j&&0!==this[i+w+1]&&(j=1),this[i+w]=(s/x>>0)-j&255;return i+u},Buffer.prototype.writeInt8=function writeInt8(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,1,127,-128),s<0&&(s=255+s+1),this[i]=255&s,i+1},Buffer.prototype.writeInt16LE=function writeInt16LE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,2,32767,-32768),this[i]=255&s,this[i+1]=s>>>8,i+2},Buffer.prototype.writeInt16BE=function writeInt16BE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,2,32767,-32768),this[i]=s>>>8,this[i+1]=255&s,i+2},Buffer.prototype.writeInt32LE=function writeInt32LE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,4,2147483647,-2147483648),this[i]=255&s,this[i+1]=s>>>8,this[i+2]=s>>>16,this[i+3]=s>>>24,i+4},Buffer.prototype.writeInt32BE=function writeInt32BE(s,i,u){return s=+s,i>>>=0,u||checkInt(this,s,i,4,2147483647,-2147483648),s<0&&(s=4294967295+s+1),this[i]=s>>>24,this[i+1]=s>>>16,this[i+2]=s>>>8,this[i+3]=255&s,i+4},Buffer.prototype.writeBigInt64LE=defineBigIntMethod((function writeBigInt64LE(s,i=0){return wrtBigUInt64LE(this,s,i,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeBigInt64BE=defineBigIntMethod((function writeBigInt64BE(s,i=0){return wrtBigUInt64BE(this,s,i,-BigInt("0x8000000000000000"),BigInt("0x7fffffffffffffff"))})),Buffer.prototype.writeFloatLE=function writeFloatLE(s,i,u){return writeFloat(this,s,i,!0,u)},Buffer.prototype.writeFloatBE=function writeFloatBE(s,i,u){return writeFloat(this,s,i,!1,u)},Buffer.prototype.writeDoubleLE=function writeDoubleLE(s,i,u){return writeDouble(this,s,i,!0,u)},Buffer.prototype.writeDoubleBE=function writeDoubleBE(s,i,u){return writeDouble(this,s,i,!1,u)},Buffer.prototype.copy=function copy(s,i,u,_){if(!Buffer.isBuffer(s))throw new TypeError("argument should be a Buffer");if(u||(u=0),_||0===_||(_=this.length),i>=s.length&&(i=s.length),i||(i=0),_>0&&_=this.length)throw new RangeError("Index out of range");if(_<0)throw new RangeError("sourceEnd out of bounds");_>this.length&&(_=this.length),s.length-i<_-u&&(_=s.length-i+u);const w=_-u;return this===s&&"function"==typeof Uint8Array.prototype.copyWithin?this.copyWithin(i,u,_):Uint8Array.prototype.set.call(s,this.subarray(u,_),i),w},Buffer.prototype.fill=function fill(s,i,u,_){if("string"==typeof s){if("string"==typeof i?(_=i,i=0,u=this.length):"string"==typeof u&&(_=u,u=this.length),void 0!==_&&"string"!=typeof _)throw new TypeError("encoding must be a string");if("string"==typeof _&&!Buffer.isEncoding(_))throw new TypeError("Unknown encoding: "+_);if(1===s.length){const i=s.charCodeAt(0);("utf8"===_&&i<128||"latin1"===_)&&(s=i)}}else"number"==typeof s?s&=255:"boolean"==typeof s&&(s=Number(s));if(i<0||this.length>>=0,u=void 0===u?this.length:u>>>0,s||(s=0),"number"==typeof s)for(w=i;w=_+4;u-=3)i=`_${s.slice(u-3,u)}${i}`;return`${s.slice(0,u)}${i}`}function checkIntBI(s,i,u,_,w,x){if(s>u||s3?0===i||i===BigInt(0)?`>= 0${_} and < 2${_} ** ${8*(x+1)}${_}`:`>= -(2${_} ** ${8*(x+1)-1}${_}) and < 2 ** ${8*(x+1)-1}${_}`:`>= ${i}${_} and <= ${u}${_}`,new B.ERR_OUT_OF_RANGE("value",w,s)}!function checkBounds(s,i,u){validateNumber(i,"offset"),void 0!==s[i]&&void 0!==s[i+u]||boundsError(i,s.length-(u+1))}(_,w,x)}function validateNumber(s,i){if("number"!=typeof s)throw new B.ERR_INVALID_ARG_TYPE(i,"number",s)}function boundsError(s,i,u){if(Math.floor(s)!==s)throw validateNumber(s,u),new B.ERR_OUT_OF_RANGE(u||"offset","an integer",s);if(i<0)throw new B.ERR_BUFFER_OUT_OF_BOUNDS;throw new B.ERR_OUT_OF_RANGE(u||"offset",`>= ${u?1:0} and <= ${i}`,s)}E("ERR_BUFFER_OUT_OF_BOUNDS",(function(s){return s?`${s} is outside of buffer bounds`:"Attempt to access memory outside buffer bounds"}),RangeError),E("ERR_INVALID_ARG_TYPE",(function(s,i){return`The "${s}" argument must be of type number. Received type ${typeof i}`}),TypeError),E("ERR_OUT_OF_RANGE",(function(s,i,u){let _=`The value of "${s}" is out of range.`,w=u;return Number.isInteger(u)&&Math.abs(u)>2**32?w=addNumericalSeparator(String(u)):"bigint"==typeof u&&(w=String(u),(u>BigInt(2)**BigInt(32)||u<-(BigInt(2)**BigInt(32)))&&(w=addNumericalSeparator(w)),w+="n"),_+=` It must be ${i}. Received ${w}`,_}),RangeError);const $=/[^+/0-9A-Za-z-_]/g;function utf8ToBytes(s,i){let u;i=i||1/0;const _=s.length;let w=null;const x=[];for(let j=0;j<_;++j){if(u=s.charCodeAt(j),u>55295&&u<57344){if(!w){if(u>56319){(i-=3)>-1&&x.push(239,191,189);continue}if(j+1===_){(i-=3)>-1&&x.push(239,191,189);continue}w=u;continue}if(u<56320){(i-=3)>-1&&x.push(239,191,189),w=u;continue}u=65536+(w-55296<<10|u-56320)}else w&&(i-=3)>-1&&x.push(239,191,189);if(w=null,u<128){if((i-=1)<0)break;x.push(u)}else if(u<2048){if((i-=2)<0)break;x.push(u>>6|192,63&u|128)}else if(u<65536){if((i-=3)<0)break;x.push(u>>12|224,u>>6&63|128,63&u|128)}else{if(!(u<1114112))throw new Error("Invalid code point");if((i-=4)<0)break;x.push(u>>18|240,u>>12&63|128,u>>6&63|128,63&u|128)}}return x}function base64ToBytes(s){return _.toByteArray(function base64clean(s){if((s=(s=s.split("=")[0]).trim().replace($,"")).length<2)return"";for(;s.length%4!=0;)s+="=";return s}(s))}function blitBuffer(s,i,u,_){let w;for(w=0;w<_&&!(w+u>=i.length||w>=s.length);++w)i[w+u]=s[w];return w}function isInstance(s,i){return s instanceof i||null!=s&&null!=s.constructor&&null!=s.constructor.name&&s.constructor.name===i.name}function numberIsNaN(s){return s!=s}const U=function(){const s="0123456789abcdef",i=new Array(256);for(let u=0;u<16;++u){const _=16*u;for(let w=0;w<16;++w)i[_+w]=s[u]+s[w]}return i}();function defineBigIntMethod(s){return"undefined"==typeof BigInt?BufferBigIntNotDefined:s}function BufferBigIntNotDefined(){throw new Error("BigInt not supported")}},38075:(s,i,u)=>{"use strict";var _=u(70453),w=u(10487),x=w(_("String.prototype.indexOf"));s.exports=function callBoundIntrinsic(s,i){var u=_(s,!!i);return"function"==typeof u&&x(s,".prototype.")>-1?w(u):u}},10487:(s,i,u)=>{"use strict";var _=u(66743),w=u(70453),x=u(96897),j=u(69675),L=w("%Function.prototype.apply%"),B=w("%Function.prototype.call%"),$=w("%Reflect.apply%",!0)||_.call(B,L),U=u(30655),Y=w("%Math.max%");s.exports=function callBind(s){if("function"!=typeof s)throw new j("a function is required");var i=$(_,B,arguments);return x(i,1+Y(0,s.length-(arguments.length-1)),!0)};var Z=function applyBind(){return $(_,L,arguments)};U?U(s.exports,"apply",{value:Z}):s.exports.apply=Z},57427:(s,i)=>{"use strict";i.parse=function parse(s,i){if("string"!=typeof s)throw new TypeError("argument str must be a string");var u={},_=(i||{}).decode||decode,w=0;for(;w{"use strict";var _=u(16426),w={"text/plain":"Text","text/html":"Url",default:"Text"};s.exports=function copy(s,i){var u,x,j,L,B,$,U=!1;i||(i={}),u=i.debug||!1;try{if(j=_(),L=document.createRange(),B=document.getSelection(),($=document.createElement("span")).textContent=s,$.ariaHidden="true",$.style.all="unset",$.style.position="fixed",$.style.top=0,$.style.clip="rect(0, 0, 0, 0)",$.style.whiteSpace="pre",$.style.webkitUserSelect="text",$.style.MozUserSelect="text",$.style.msUserSelect="text",$.style.userSelect="text",$.addEventListener("copy",(function(_){if(_.stopPropagation(),i.format)if(_.preventDefault(),void 0===_.clipboardData){u&&console.warn("unable to use e.clipboardData"),u&&console.warn("trying IE specific stuff"),window.clipboardData.clearData();var x=w[i.format]||w.default;window.clipboardData.setData(x,s)}else _.clipboardData.clearData(),_.clipboardData.setData(i.format,s);i.onCopy&&(_.preventDefault(),i.onCopy(_.clipboardData))})),document.body.appendChild($),L.selectNodeContents($),B.addRange(L),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");U=!0}catch(_){u&&console.error("unable to copy using execCommand: ",_),u&&console.warn("trying IE specific stuff");try{window.clipboardData.setData(i.format||"text",s),i.onCopy&&i.onCopy(window.clipboardData),U=!0}catch(_){u&&console.error("unable to copy using clipboardData: ",_),u&&console.error("falling back to prompt"),x=function format(s){var i=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return s.replace(/#{\s*key\s*}/g,i)}("message"in i?i.message:"Copy to clipboard: #{key}, Enter"),window.prompt(x,s)}}finally{B&&("function"==typeof B.removeRange?B.removeRange(L):B.removeAllRanges()),$&&document.body.removeChild($),j()}return U}},2205:function(s,i,u){var _;_=void 0!==u.g?u.g:this,s.exports=function(s){if(s.CSS&&s.CSS.escape)return s.CSS.escape;var cssEscape=function(s){if(0==arguments.length)throw new TypeError("`CSS.escape` requires an argument.");for(var i,u=String(s),_=u.length,w=-1,x="",j=u.charCodeAt(0);++w<_;)0!=(i=u.charCodeAt(w))?x+=i>=1&&i<=31||127==i||0==w&&i>=48&&i<=57||1==w&&i>=48&&i<=57&&45==j?"\\"+i.toString(16)+" ":0==w&&1==_&&45==i||!(i>=128||45==i||95==i||i>=48&&i<=57||i>=65&&i<=90||i>=97&&i<=122)?"\\"+u.charAt(w):u.charAt(w):x+="�";return x};return s.CSS||(s.CSS={}),s.CSS.escape=cssEscape,cssEscape}(_)},81919:(s,i,u)=>{"use strict";var _=u(48287).Buffer;function isSpecificValue(s){return s instanceof _||s instanceof Date||s instanceof RegExp}function cloneSpecificValue(s){if(s instanceof _){var i=_.alloc?_.alloc(s.length):new _(s.length);return s.copy(i),i}if(s instanceof Date)return new Date(s.getTime());if(s instanceof RegExp)return new RegExp(s);throw new Error("Unexpected situation")}function deepCloneArray(s){var i=[];return s.forEach((function(s,u){"object"==typeof s&&null!==s?Array.isArray(s)?i[u]=deepCloneArray(s):isSpecificValue(s)?i[u]=cloneSpecificValue(s):i[u]=w({},s):i[u]=s})),i}function safeGetProperty(s,i){return"__proto__"===i?void 0:s[i]}var w=s.exports=function(){if(arguments.length<1||"object"!=typeof arguments[0])return!1;if(arguments.length<2)return arguments[0];var s,i,u=arguments[0];return Array.prototype.slice.call(arguments,1).forEach((function(_){"object"!=typeof _||null===_||Array.isArray(_)||Object.keys(_).forEach((function(x){return i=safeGetProperty(u,x),(s=safeGetProperty(_,x))===u?void 0:"object"!=typeof s||null===s?void(u[x]=s):Array.isArray(s)?void(u[x]=deepCloneArray(s)):isSpecificValue(s)?void(u[x]=cloneSpecificValue(s)):"object"!=typeof i||null===i||Array.isArray(i)?void(u[x]=w({},s)):void(u[x]=w(i,s))}))})),u}},14744:s=>{"use strict";var i=function isMergeableObject(s){return function isNonNullObject(s){return!!s&&"object"==typeof s}(s)&&!function isSpecial(s){var i=Object.prototype.toString.call(s);return"[object RegExp]"===i||"[object Date]"===i||function isReactElement(s){return s.$$typeof===u}(s)}(s)};var u="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function cloneUnlessOtherwiseSpecified(s,i){return!1!==i.clone&&i.isMergeableObject(s)?deepmerge(function emptyTarget(s){return Array.isArray(s)?[]:{}}(s),s,i):s}function defaultArrayMerge(s,i,u){return s.concat(i).map((function(s){return cloneUnlessOtherwiseSpecified(s,u)}))}function getKeys(s){return Object.keys(s).concat(function getEnumerableOwnPropertySymbols(s){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(s).filter((function(i){return Object.propertyIsEnumerable.call(s,i)})):[]}(s))}function propertyIsOnObject(s,i){try{return i in s}catch(s){return!1}}function mergeObject(s,i,u){var _={};return u.isMergeableObject(s)&&getKeys(s).forEach((function(i){_[i]=cloneUnlessOtherwiseSpecified(s[i],u)})),getKeys(i).forEach((function(w){(function propertyIsUnsafe(s,i){return propertyIsOnObject(s,i)&&!(Object.hasOwnProperty.call(s,i)&&Object.propertyIsEnumerable.call(s,i))})(s,w)||(propertyIsOnObject(s,w)&&u.isMergeableObject(i[w])?_[w]=function getMergeFunction(s,i){if(!i.customMerge)return deepmerge;var u=i.customMerge(s);return"function"==typeof u?u:deepmerge}(w,u)(s[w],i[w],u):_[w]=cloneUnlessOtherwiseSpecified(i[w],u))})),_}function deepmerge(s,u,_){(_=_||{}).arrayMerge=_.arrayMerge||defaultArrayMerge,_.isMergeableObject=_.isMergeableObject||i,_.cloneUnlessOtherwiseSpecified=cloneUnlessOtherwiseSpecified;var w=Array.isArray(u);return w===Array.isArray(s)?w?_.arrayMerge(s,u,_):mergeObject(s,u,_):cloneUnlessOtherwiseSpecified(u,_)}deepmerge.all=function deepmergeAll(s,i){if(!Array.isArray(s))throw new Error("first argument should be an array");return s.reduce((function(s,u){return deepmerge(s,u,i)}),{})};var _=deepmerge;s.exports=_},30041:(s,i,u)=>{"use strict";var _=u(30655),w=u(58068),x=u(69675),j=u(75795);s.exports=function defineDataProperty(s,i,u){if(!s||"object"!=typeof s&&"function"!=typeof s)throw new x("`obj` must be an object or a function`");if("string"!=typeof i&&"symbol"!=typeof i)throw new x("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new x("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new x("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new x("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new x("`loose`, if provided, must be a boolean");var L=arguments.length>3?arguments[3]:null,B=arguments.length>4?arguments[4]:null,$=arguments.length>5?arguments[5]:null,U=arguments.length>6&&arguments[6],Y=!!j&&j(s,i);if(_)_(s,i,{configurable:null===$&&Y?Y.configurable:!$,enumerable:null===L&&Y?Y.enumerable:!L,value:u,writable:null===B&&Y?Y.writable:!B});else{if(!U&&(L||B||$))throw new w("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");s[i]=u}}},42838:function(s){s.exports=function(){"use strict";const{entries:s,setPrototypeOf:i,isFrozen:u,getPrototypeOf:_,getOwnPropertyDescriptor:w}=Object;let{freeze:x,seal:j,create:L}=Object,{apply:B,construct:$}="undefined"!=typeof Reflect&&Reflect;x||(x=function freeze(s){return s}),j||(j=function seal(s){return s}),B||(B=function apply(s,i,u){return s.apply(i,u)}),$||($=function construct(s,i){return new s(...i)});const U=unapply(Array.prototype.forEach),Y=unapply(Array.prototype.pop),Z=unapply(Array.prototype.push),ee=unapply(String.prototype.toLowerCase),ie=unapply(String.prototype.toString),ae=unapply(String.prototype.match),le=unapply(String.prototype.replace),ce=unapply(String.prototype.indexOf),pe=unapply(String.prototype.trim),de=unapply(Object.prototype.hasOwnProperty),fe=unapply(RegExp.prototype.test),ye=unconstruct(TypeError);function unapply(s){return function(i){for(var u=arguments.length,_=new Array(u>1?u-1:0),w=1;w2&&void 0!==arguments[2]?arguments[2]:ee;i&&i(s,null);let x=_.length;for(;x--;){let i=_[x];if("string"==typeof i){const s=w(i);s!==i&&(u(_)||(_[x]=s),i=s)}s[i]=!0}return s}function cleanArray(s){for(let i=0;i/gm),Xe=j(/\${[\w\W]*}/gm),Ye=j(/^data-[\-\w.\u00B7-\uFFFF]/),Qe=j(/^aria-[\-\w]+$/),et=j(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),tt=j(/^(?:\w+script|data):/i),rt=j(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),nt=j(/^html$/i),ot=j(/^[a-z][.\w]*(-[.\w]+)+$/i);var st=Object.freeze({__proto__:null,MUSTACHE_EXPR:We,ERB_EXPR:He,TMPLIT_EXPR:Xe,DATA_ATTR:Ye,ARIA_ATTR:Qe,IS_ALLOWED_URI:et,IS_SCRIPT_OR_DATA:tt,ATTR_WHITESPACE:rt,DOCTYPE_NAME:nt,CUSTOM_ELEMENT:ot});const it=function getGlobal(){return"undefined"==typeof window?null:window},at=function _createTrustedTypesPolicy(s,i){if("object"!=typeof s||"function"!=typeof s.createPolicy)return null;let u=null;const _="data-tt-policy-suffix";i&&i.hasAttribute(_)&&(u=i.getAttribute(_));const w="dompurify"+(u?"#"+u:"");try{return s.createPolicy(w,{createHTML:s=>s,createScriptURL:s=>s})}catch(s){return console.warn("TrustedTypes policy "+w+" could not be created."),null}};function createDOMPurify(){let i=arguments.length>0&&void 0!==arguments[0]?arguments[0]:it();const DOMPurify=s=>createDOMPurify(s);if(DOMPurify.version="3.1.2",DOMPurify.removed=[],!i||!i.document||9!==i.document.nodeType)return DOMPurify.isSupported=!1,DOMPurify;let{document:u}=i;const _=u,w=_.currentScript,{DocumentFragment:j,HTMLTemplateElement:B,Node:$,Element:We,NodeFilter:He,NamedNodeMap:Xe=i.NamedNodeMap||i.MozNamedAttrMap,HTMLFormElement:Ye,DOMParser:Qe,trustedTypes:tt}=i,rt=We.prototype,ot=lookupGetter(rt,"cloneNode"),lt=lookupGetter(rt,"nextSibling"),ct=lookupGetter(rt,"childNodes"),ut=lookupGetter(rt,"parentNode");if("function"==typeof B){const s=u.createElement("template");s.content&&s.content.ownerDocument&&(u=s.content.ownerDocument)}let pt,ht="";const{implementation:dt,createNodeIterator:mt,createDocumentFragment:gt,getElementsByTagName:yt}=u,{importNode:vt}=_;let bt={};DOMPurify.isSupported="function"==typeof s&&"function"==typeof ut&&dt&&void 0!==dt.createHTMLDocument;const{MUSTACHE_EXPR:_t,ERB_EXPR:Et,TMPLIT_EXPR:wt,DATA_ATTR:St,ARIA_ATTR:xt,IS_SCRIPT_OR_DATA:kt,ATTR_WHITESPACE:Ot,CUSTOM_ELEMENT:Ct}=st;let{IS_ALLOWED_URI:At}=st,jt=null;const Pt=addToSet({},[...be,..._e,...we,...xe,...Te]);let It=null;const Nt=addToSet({},[...Re,...qe,...$e,...ze]);let Mt=Object.seal(L(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Tt=null,Rt=null,Dt=!0,Lt=!0,Bt=!1,Ft=!0,qt=!1,$t=!0,Ut=!1,zt=!1,Vt=!1,Wt=!1,Kt=!1,Ht=!1,Jt=!0,Gt=!1;const Xt="user-content-";let Yt=!0,Qt=!1,Zt={},er=null;const tr=addToSet({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let rr=null;const nr=addToSet({},["audio","video","img","source","image","track"]);let sr=null;const ir=addToSet({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),ar="http://www.w3.org/1998/Math/MathML",lr="http://www.w3.org/2000/svg",cr="http://www.w3.org/1999/xhtml";let ur=cr,pr=!1,dr=null;const fr=addToSet({},[ar,lr,cr],ie);let mr=null;const gr=["application/xhtml+xml","text/html"],yr="text/html";let vr=null,br=null;const _r=255,Er=u.createElement("form"),wr=function isRegexOrFunction(s){return s instanceof RegExp||s instanceof Function},Sr=function _parseConfig(){let s=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!br||br!==s){if(s&&"object"==typeof s||(s={}),s=clone(s),mr=-1===gr.indexOf(s.PARSER_MEDIA_TYPE)?yr:s.PARSER_MEDIA_TYPE,vr="application/xhtml+xml"===mr?ie:ee,jt=de(s,"ALLOWED_TAGS")?addToSet({},s.ALLOWED_TAGS,vr):Pt,It=de(s,"ALLOWED_ATTR")?addToSet({},s.ALLOWED_ATTR,vr):Nt,dr=de(s,"ALLOWED_NAMESPACES")?addToSet({},s.ALLOWED_NAMESPACES,ie):fr,sr=de(s,"ADD_URI_SAFE_ATTR")?addToSet(clone(ir),s.ADD_URI_SAFE_ATTR,vr):ir,rr=de(s,"ADD_DATA_URI_TAGS")?addToSet(clone(nr),s.ADD_DATA_URI_TAGS,vr):nr,er=de(s,"FORBID_CONTENTS")?addToSet({},s.FORBID_CONTENTS,vr):tr,Tt=de(s,"FORBID_TAGS")?addToSet({},s.FORBID_TAGS,vr):{},Rt=de(s,"FORBID_ATTR")?addToSet({},s.FORBID_ATTR,vr):{},Zt=!!de(s,"USE_PROFILES")&&s.USE_PROFILES,Dt=!1!==s.ALLOW_ARIA_ATTR,Lt=!1!==s.ALLOW_DATA_ATTR,Bt=s.ALLOW_UNKNOWN_PROTOCOLS||!1,Ft=!1!==s.ALLOW_SELF_CLOSE_IN_ATTR,qt=s.SAFE_FOR_TEMPLATES||!1,$t=!1!==s.SAFE_FOR_XML,Ut=s.WHOLE_DOCUMENT||!1,Wt=s.RETURN_DOM||!1,Kt=s.RETURN_DOM_FRAGMENT||!1,Ht=s.RETURN_TRUSTED_TYPE||!1,Vt=s.FORCE_BODY||!1,Jt=!1!==s.SANITIZE_DOM,Gt=s.SANITIZE_NAMED_PROPS||!1,Yt=!1!==s.KEEP_CONTENT,Qt=s.IN_PLACE||!1,At=s.ALLOWED_URI_REGEXP||et,ur=s.NAMESPACE||cr,Mt=s.CUSTOM_ELEMENT_HANDLING||{},s.CUSTOM_ELEMENT_HANDLING&&wr(s.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Mt.tagNameCheck=s.CUSTOM_ELEMENT_HANDLING.tagNameCheck),s.CUSTOM_ELEMENT_HANDLING&&wr(s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Mt.attributeNameCheck=s.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),s.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Mt.allowCustomizedBuiltInElements=s.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),qt&&(Lt=!1),Kt&&(Wt=!0),Zt&&(jt=addToSet({},Te),It=[],!0===Zt.html&&(addToSet(jt,be),addToSet(It,Re)),!0===Zt.svg&&(addToSet(jt,_e),addToSet(It,qe),addToSet(It,ze)),!0===Zt.svgFilters&&(addToSet(jt,we),addToSet(It,qe),addToSet(It,ze)),!0===Zt.mathMl&&(addToSet(jt,xe),addToSet(It,$e),addToSet(It,ze))),s.ADD_TAGS&&(jt===Pt&&(jt=clone(jt)),addToSet(jt,s.ADD_TAGS,vr)),s.ADD_ATTR&&(It===Nt&&(It=clone(It)),addToSet(It,s.ADD_ATTR,vr)),s.ADD_URI_SAFE_ATTR&&addToSet(sr,s.ADD_URI_SAFE_ATTR,vr),s.FORBID_CONTENTS&&(er===tr&&(er=clone(er)),addToSet(er,s.FORBID_CONTENTS,vr)),Yt&&(jt["#text"]=!0),Ut&&addToSet(jt,["html","head","body"]),jt.table&&(addToSet(jt,["tbody"]),delete Tt.tbody),s.TRUSTED_TYPES_POLICY){if("function"!=typeof s.TRUSTED_TYPES_POLICY.createHTML)throw ye('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof s.TRUSTED_TYPES_POLICY.createScriptURL)throw ye('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');pt=s.TRUSTED_TYPES_POLICY,ht=pt.createHTML("")}else void 0===pt&&(pt=at(tt,w)),null!==pt&&"string"==typeof ht&&(ht=pt.createHTML(""));x&&x(s),br=s}},xr=addToSet({},["mi","mo","mn","ms","mtext"]),kr=addToSet({},["foreignobject","annotation-xml"]),Or=addToSet({},["title","style","font","a","script"]),Cr=addToSet({},[..._e,...we,...Se]),Ar=addToSet({},[...xe,...Pe]),jr=function _checkValidNamespace(s){let i=ut(s);i&&i.tagName||(i={namespaceURI:ur,tagName:"template"});const u=ee(s.tagName),_=ee(i.tagName);return!!dr[s.namespaceURI]&&(s.namespaceURI===lr?i.namespaceURI===cr?"svg"===u:i.namespaceURI===ar?"svg"===u&&("annotation-xml"===_||xr[_]):Boolean(Cr[u]):s.namespaceURI===ar?i.namespaceURI===cr?"math"===u:i.namespaceURI===lr?"math"===u&&kr[_]:Boolean(Ar[u]):s.namespaceURI===cr?!(i.namespaceURI===lr&&!kr[_])&&!(i.namespaceURI===ar&&!xr[_])&&!Ar[u]&&(Or[u]||!Cr[u]):!("application/xhtml+xml"!==mr||!dr[s.namespaceURI]))},Pr=function _forceRemove(s){Z(DOMPurify.removed,{element:s});try{s.parentNode.removeChild(s)}catch(i){s.remove()}},Ir=function _removeAttribute(s,i){try{Z(DOMPurify.removed,{attribute:i.getAttributeNode(s),from:i})}catch(s){Z(DOMPurify.removed,{attribute:null,from:i})}if(i.removeAttribute(s),"is"===s&&!It[s])if(Wt||Kt)try{Pr(i)}catch(s){}else try{i.setAttribute(s,"")}catch(s){}},Nr=function _initDocument(s){let i=null,_=null;if(Vt)s=" "+s;else{const i=ae(s,/^[\r\n\t ]+/);_=i&&i[0]}"application/xhtml+xml"===mr&&ur===cr&&(s=''+s+"");const w=pt?pt.createHTML(s):s;if(ur===cr)try{i=(new Qe).parseFromString(w,mr)}catch(s){}if(!i||!i.documentElement){i=dt.createDocument(ur,"template",null);try{i.documentElement.innerHTML=pr?ht:w}catch(s){}}const x=i.body||i.documentElement;return s&&_&&x.insertBefore(u.createTextNode(_),x.childNodes[0]||null),ur===cr?yt.call(i,Ut?"html":"body")[0]:Ut?i.documentElement:x},Mr=function _createNodeIterator(s){return mt.call(s.ownerDocument||s,s,He.SHOW_ELEMENT|He.SHOW_COMMENT|He.SHOW_TEXT|He.SHOW_PROCESSING_INSTRUCTION|He.SHOW_CDATA_SECTION,null)},Tr=function _isClobbered(s){return s instanceof Ye&&(void 0!==s.__depth&&"number"!=typeof s.__depth||void 0!==s.__removalCount&&"number"!=typeof s.__removalCount||"string"!=typeof s.nodeName||"string"!=typeof s.textContent||"function"!=typeof s.removeChild||!(s.attributes instanceof Xe)||"function"!=typeof s.removeAttribute||"function"!=typeof s.setAttribute||"string"!=typeof s.namespaceURI||"function"!=typeof s.insertBefore||"function"!=typeof s.hasChildNodes)},Rr=function _isNode(s){return"function"==typeof $&&s instanceof $},Dr=function _executeHook(s,i,u){bt[s]&&U(bt[s],(s=>{s.call(DOMPurify,i,u,br)}))},Lr=function _sanitizeElements(s){let i=null;if(Dr("beforeSanitizeElements",s,null),Tr(s))return Pr(s),!0;const u=vr(s.nodeName);if(Dr("uponSanitizeElement",s,{tagName:u,allowedTags:jt}),s.hasChildNodes()&&!Rr(s.firstElementChild)&&fe(/<[/\w]/g,s.innerHTML)&&fe(/<[/\w]/g,s.textContent))return Pr(s),!0;if(7===s.nodeType)return Pr(s),!0;if($t&&8===s.nodeType&&fe(/<[/\w]/g,s.data))return Pr(s),!0;if(!jt[u]||Tt[u]){if(!Tt[u]&&Fr(u)){if(Mt.tagNameCheck instanceof RegExp&&fe(Mt.tagNameCheck,u))return!1;if(Mt.tagNameCheck instanceof Function&&Mt.tagNameCheck(u))return!1}if(Yt&&!er[u]){const i=ut(s)||s.parentNode,u=ct(s)||s.childNodes;if(u&&i)for(let _=u.length-1;_>=0;--_){const w=ot(u[_],!0);w.__removalCount=(s.__removalCount||0)+1,i.insertBefore(w,lt(s))}}return Pr(s),!0}return s instanceof We&&!jr(s)?(Pr(s),!0):"noscript"!==u&&"noembed"!==u&&"noframes"!==u||!fe(/<\/no(script|embed|frames)/i,s.innerHTML)?(qt&&3===s.nodeType&&(i=s.textContent,U([_t,Et,wt],(s=>{i=le(i,s," ")})),s.textContent!==i&&(Z(DOMPurify.removed,{element:s.cloneNode()}),s.textContent=i)),Dr("afterSanitizeElements",s,null),!1):(Pr(s),!0)},Br=function _isValidAttribute(s,i,_){if(Jt&&("id"===i||"name"===i)&&(_ in u||_ in Er))return!1;if(Lt&&!Rt[i]&&fe(St,i));else if(Dt&&fe(xt,i));else if(!It[i]||Rt[i]){if(!(Fr(s)&&(Mt.tagNameCheck instanceof RegExp&&fe(Mt.tagNameCheck,s)||Mt.tagNameCheck instanceof Function&&Mt.tagNameCheck(s))&&(Mt.attributeNameCheck instanceof RegExp&&fe(Mt.attributeNameCheck,i)||Mt.attributeNameCheck instanceof Function&&Mt.attributeNameCheck(i))||"is"===i&&Mt.allowCustomizedBuiltInElements&&(Mt.tagNameCheck instanceof RegExp&&fe(Mt.tagNameCheck,_)||Mt.tagNameCheck instanceof Function&&Mt.tagNameCheck(_))))return!1}else if(sr[i]);else if(fe(At,le(_,Ot,"")));else if("src"!==i&&"xlink:href"!==i&&"href"!==i||"script"===s||0!==ce(_,"data:")||!rr[s])if(Bt&&!fe(kt,le(_,Ot,"")));else if(_)return!1;return!0},Fr=function _isBasicCustomElement(s){return"annotation-xml"!==s&&ae(s,Ct)},qr=function _sanitizeAttributes(s){Dr("beforeSanitizeAttributes",s,null);const{attributes:i}=s;if(!i)return;const u={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:It};let _=i.length;for(;_--;){const w=i[_],{name:x,namespaceURI:j,value:L}=w,B=vr(x);let $="value"===x?L:pe(L);if(u.attrName=B,u.attrValue=$,u.keepAttr=!0,u.forceKeepAttr=void 0,Dr("uponSanitizeAttribute",s,u),$=u.attrValue,u.forceKeepAttr)continue;if(Ir(x,s),!u.keepAttr)continue;if(!Ft&&fe(/\/>/i,$)){Ir(x,s);continue}qt&&U([_t,Et,wt],(s=>{$=le($,s," ")}));const Z=vr(s.nodeName);if(Br(Z,B,$)){if(!Gt||"id"!==B&&"name"!==B||(Ir(x,s),$=Xt+$),pt&&"object"==typeof tt&&"function"==typeof tt.getAttributeType)if(j);else switch(tt.getAttributeType(Z,B)){case"TrustedHTML":$=pt.createHTML($);break;case"TrustedScriptURL":$=pt.createScriptURL($)}try{j?s.setAttributeNS(j,x,$):s.setAttribute(x,$),Y(DOMPurify.removed)}catch(s){}}}Dr("afterSanitizeAttributes",s,null)},$r=function _sanitizeShadowDOM(s){let i=null;const u=Mr(s);for(Dr("beforeSanitizeShadowDOM",s,null);i=u.nextNode();){if(Dr("uponSanitizeShadowNode",i,null),Lr(i))continue;const s=ut(i);1===i.nodeType&&(s&&s.__depth?i.__depth=(i.__removalCount||0)+s.__depth+1:i.__depth=1),i.__depth>=_r&&Pr(i),i.content instanceof j&&(i.content.__depth=i.__depth,_sanitizeShadowDOM(i.content)),qr(i)}Dr("afterSanitizeShadowDOM",s,null)};return DOMPurify.sanitize=function(s){let i=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},u=null,w=null,x=null,L=null;if(pr=!s,pr&&(s="\x3c!--\x3e"),"string"!=typeof s&&!Rr(s)){if("function"!=typeof s.toString)throw ye("toString is not a function");if("string"!=typeof(s=s.toString()))throw ye("dirty is not a string, aborting")}if(!DOMPurify.isSupported)return s;if(zt||Sr(i),DOMPurify.removed=[],"string"==typeof s&&(Qt=!1),Qt){if(s.nodeName){const i=vr(s.nodeName);if(!jt[i]||Tt[i])throw ye("root node is forbidden and cannot be sanitized in-place")}}else if(s instanceof $)u=Nr("\x3c!----\x3e"),w=u.ownerDocument.importNode(s,!0),1===w.nodeType&&"BODY"===w.nodeName||"HTML"===w.nodeName?u=w:u.appendChild(w);else{if(!Wt&&!qt&&!Ut&&-1===s.indexOf("<"))return pt&&Ht?pt.createHTML(s):s;if(u=Nr(s),!u)return Wt?null:Ht?ht:""}u&&Vt&&Pr(u.firstChild);const B=Mr(Qt?s:u);for(;x=B.nextNode();){if(Lr(x))continue;const s=ut(x);1===x.nodeType&&(s&&s.__depth?x.__depth=(x.__removalCount||0)+s.__depth+1:x.__depth=1),x.__depth>=_r&&Pr(x),x.content instanceof j&&(x.content.__depth=x.__depth,$r(x.content)),qr(x)}if(Qt)return s;if(Wt){if(Kt)for(L=gt.call(u.ownerDocument);u.firstChild;)L.appendChild(u.firstChild);else L=u;return(It.shadowroot||It.shadowrootmode)&&(L=vt.call(_,L,!0)),L}let Y=Ut?u.outerHTML:u.innerHTML;return Ut&&jt["!doctype"]&&u.ownerDocument&&u.ownerDocument.doctype&&u.ownerDocument.doctype.name&&fe(nt,u.ownerDocument.doctype.name)&&(Y="\n"+Y),qt&&U([_t,Et,wt],(s=>{Y=le(Y,s," ")})),pt&&Ht?pt.createHTML(Y):Y},DOMPurify.setConfig=function(){Sr(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),zt=!0},DOMPurify.clearConfig=function(){br=null,zt=!1},DOMPurify.isValidAttribute=function(s,i,u){br||Sr({});const _=vr(s),w=vr(i);return Br(_,w,u)},DOMPurify.addHook=function(s,i){"function"==typeof i&&(bt[s]=bt[s]||[],Z(bt[s],i))},DOMPurify.removeHook=function(s){if(bt[s])return Y(bt[s])},DOMPurify.removeHooks=function(s){bt[s]&&(bt[s]=[])},DOMPurify.removeAllHooks=function(){bt={}},DOMPurify}return createDOMPurify()}()},78004:s=>{"use strict";class SubRange{constructor(s,i){this.low=s,this.high=i,this.length=1+i-s}overlaps(s){return!(this.highs.high)}touches(s){return!(this.high+1s.high)}add(s){return new SubRange(Math.min(this.low,s.low),Math.max(this.high,s.high))}subtract(s){return s.low<=this.low&&s.high>=this.high?[]:s.low>this.low&&s.highs+i.length),0)}add(s,i){var _add=s=>{for(var i=0;i{for(var i=0;i{for(var i=0;i{for(var u=i.low;u<=i.high;)s.push(u),u++;return s}),[])}subranges(){return this.ranges.map((s=>({low:s.low,high:s.high,length:1+s.high-s.low})))}}s.exports=DRange},30655:(s,i,u)=>{"use strict";var _=u(70453)("%Object.defineProperty%",!0)||!1;if(_)try{_({},"a",{value:1})}catch(s){_=!1}s.exports=_},41237:s=>{"use strict";s.exports=EvalError},69383:s=>{"use strict";s.exports=Error},79290:s=>{"use strict";s.exports=RangeError},79538:s=>{"use strict";s.exports=ReferenceError},58068:s=>{"use strict";s.exports=SyntaxError},69675:s=>{"use strict";s.exports=TypeError},35345:s=>{"use strict";s.exports=URIError},37007:s=>{"use strict";var i,u="object"==typeof Reflect?Reflect:null,_=u&&"function"==typeof u.apply?u.apply:function ReflectApply(s,i,u){return Function.prototype.apply.call(s,i,u)};i=u&&"function"==typeof u.ownKeys?u.ownKeys:Object.getOwnPropertySymbols?function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s).concat(Object.getOwnPropertySymbols(s))}:function ReflectOwnKeys(s){return Object.getOwnPropertyNames(s)};var w=Number.isNaN||function NumberIsNaN(s){return s!=s};function EventEmitter(){EventEmitter.init.call(this)}s.exports=EventEmitter,s.exports.once=function once(s,i){return new Promise((function(u,_){function errorListener(u){s.removeListener(i,resolver),_(u)}function resolver(){"function"==typeof s.removeListener&&s.removeListener("error",errorListener),u([].slice.call(arguments))}eventTargetAgnosticAddListener(s,i,resolver,{once:!0}),"error"!==i&&function addErrorHandlerIfEventEmitter(s,i,u){"function"==typeof s.on&&eventTargetAgnosticAddListener(s,"error",i,u)}(s,errorListener,{once:!0})}))},EventEmitter.EventEmitter=EventEmitter,EventEmitter.prototype._events=void 0,EventEmitter.prototype._eventsCount=0,EventEmitter.prototype._maxListeners=void 0;var x=10;function checkListener(s){if("function"!=typeof s)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof s)}function _getMaxListeners(s){return void 0===s._maxListeners?EventEmitter.defaultMaxListeners:s._maxListeners}function _addListener(s,i,u,_){var w,x,j;if(checkListener(u),void 0===(x=s._events)?(x=s._events=Object.create(null),s._eventsCount=0):(void 0!==x.newListener&&(s.emit("newListener",i,u.listener?u.listener:u),x=s._events),j=x[i]),void 0===j)j=x[i]=u,++s._eventsCount;else if("function"==typeof j?j=x[i]=_?[u,j]:[j,u]:_?j.unshift(u):j.push(u),(w=_getMaxListeners(s))>0&&j.length>w&&!j.warned){j.warned=!0;var L=new Error("Possible EventEmitter memory leak detected. "+j.length+" "+String(i)+" listeners added. Use emitter.setMaxListeners() to increase limit");L.name="MaxListenersExceededWarning",L.emitter=s,L.type=i,L.count=j.length,function ProcessEmitWarning(s){console&&console.warn&&console.warn(s)}(L)}return s}function onceWrapper(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function _onceWrap(s,i,u){var _={fired:!1,wrapFn:void 0,target:s,type:i,listener:u},w=onceWrapper.bind(_);return w.listener=u,_.wrapFn=w,w}function _listeners(s,i,u){var _=s._events;if(void 0===_)return[];var w=_[i];return void 0===w?[]:"function"==typeof w?u?[w.listener||w]:[w]:u?function unwrapListeners(s){for(var i=new Array(s.length),u=0;u0&&(j=i[0]),j instanceof Error)throw j;var L=new Error("Unhandled error."+(j?" ("+j.message+")":""));throw L.context=j,L}var B=x[s];if(void 0===B)return!1;if("function"==typeof B)_(B,this,i);else{var $=B.length,U=arrayClone(B,$);for(u=0;u<$;++u)_(U[u],this,i)}return!0},EventEmitter.prototype.addListener=function addListener(s,i){return _addListener(this,s,i,!1)},EventEmitter.prototype.on=EventEmitter.prototype.addListener,EventEmitter.prototype.prependListener=function prependListener(s,i){return _addListener(this,s,i,!0)},EventEmitter.prototype.once=function once(s,i){return checkListener(i),this.on(s,_onceWrap(this,s,i)),this},EventEmitter.prototype.prependOnceListener=function prependOnceListener(s,i){return checkListener(i),this.prependListener(s,_onceWrap(this,s,i)),this},EventEmitter.prototype.removeListener=function removeListener(s,i){var u,_,w,x,j;if(checkListener(i),void 0===(_=this._events))return this;if(void 0===(u=_[s]))return this;if(u===i||u.listener===i)0==--this._eventsCount?this._events=Object.create(null):(delete _[s],_.removeListener&&this.emit("removeListener",s,u.listener||i));else if("function"!=typeof u){for(w=-1,x=u.length-1;x>=0;x--)if(u[x]===i||u[x].listener===i){j=u[x].listener,w=x;break}if(w<0)return this;0===w?u.shift():function spliceOne(s,i){for(;i+1=0;_--)this.removeListener(s,i[_]);return this},EventEmitter.prototype.listeners=function listeners(s){return _listeners(this,s,!0)},EventEmitter.prototype.rawListeners=function rawListeners(s){return _listeners(this,s,!1)},EventEmitter.listenerCount=function(s,i){return"function"==typeof s.listenerCount?s.listenerCount(i):listenerCount.call(s,i)},EventEmitter.prototype.listenerCount=listenerCount,EventEmitter.prototype.eventNames=function eventNames(){return this._eventsCount>0?i(this._events):[]}},85587:(s,i,u)=>{"use strict";var _=u(26311),w=create(Error);function create(s){return FormattedError.displayName=s.displayName||s.name,FormattedError;function FormattedError(i){return i&&(i=_.apply(null,arguments)),new s(i)}}s.exports=w,w.eval=create(EvalError),w.range=create(RangeError),w.reference=create(ReferenceError),w.syntax=create(SyntaxError),w.type=create(TypeError),w.uri=create(URIError),w.create=create},26311:s=>{!function(){var i;function format(s){for(var i,u,_,w,x=1,j=[].slice.call(arguments),L=0,B=s.length,$="",U=!1,Y=!1,nextArg=function(){return j[x++]},slurpNumber=function(){for(var u="";/\d/.test(s[L]);)u+=s[L++],i=s[L];return u.length>0?parseInt(u):null};L{"use strict";var i=Object.prototype.toString,u=Math.max,_=function concatty(s,i){for(var u=[],_=0;_{"use strict";var _=u(89353);s.exports=Function.prototype.bind||_},70453:(s,i,u)=>{"use strict";var _,w=u(69383),x=u(41237),j=u(79290),L=u(79538),B=u(58068),$=u(69675),U=u(35345),Y=Function,getEvalledConstructor=function(s){try{return Y('"use strict"; return ('+s+").constructor;")()}catch(s){}},Z=Object.getOwnPropertyDescriptor;if(Z)try{Z({},"")}catch(s){Z=null}var throwTypeError=function(){throw new $},ee=Z?function(){try{return throwTypeError}catch(s){try{return Z(arguments,"callee").get}catch(s){return throwTypeError}}}():throwTypeError,ie=u(64039)(),ae=u(80024)(),le=Object.getPrototypeOf||(ae?function(s){return s.__proto__}:null),ce={},pe="undefined"!=typeof Uint8Array&&le?le(Uint8Array):_,de={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?_:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?_:ArrayBuffer,"%ArrayIteratorPrototype%":ie&&le?le([][Symbol.iterator]()):_,"%AsyncFromSyncIteratorPrototype%":_,"%AsyncFunction%":ce,"%AsyncGenerator%":ce,"%AsyncGeneratorFunction%":ce,"%AsyncIteratorPrototype%":ce,"%Atomics%":"undefined"==typeof Atomics?_:Atomics,"%BigInt%":"undefined"==typeof BigInt?_:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?_:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?_:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?_:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":w,"%eval%":eval,"%EvalError%":x,"%Float32Array%":"undefined"==typeof Float32Array?_:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?_:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?_:FinalizationRegistry,"%Function%":Y,"%GeneratorFunction%":ce,"%Int8Array%":"undefined"==typeof Int8Array?_:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?_:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?_:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":ie&&le?le(le([][Symbol.iterator]())):_,"%JSON%":"object"==typeof JSON?JSON:_,"%Map%":"undefined"==typeof Map?_:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&ie&&le?le((new Map)[Symbol.iterator]()):_,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?_:Promise,"%Proxy%":"undefined"==typeof Proxy?_:Proxy,"%RangeError%":j,"%ReferenceError%":L,"%Reflect%":"undefined"==typeof Reflect?_:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?_:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&ie&&le?le((new Set)[Symbol.iterator]()):_,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?_:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":ie&&le?le(""[Symbol.iterator]()):_,"%Symbol%":ie?Symbol:_,"%SyntaxError%":B,"%ThrowTypeError%":ee,"%TypedArray%":pe,"%TypeError%":$,"%Uint8Array%":"undefined"==typeof Uint8Array?_:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?_:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?_:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?_:Uint32Array,"%URIError%":U,"%WeakMap%":"undefined"==typeof WeakMap?_:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?_:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?_:WeakSet};if(le)try{null.error}catch(s){var fe=le(le(s));de["%Error.prototype%"]=fe}var ye=function doEval(s){var i;if("%AsyncFunction%"===s)i=getEvalledConstructor("async function () {}");else if("%GeneratorFunction%"===s)i=getEvalledConstructor("function* () {}");else if("%AsyncGeneratorFunction%"===s)i=getEvalledConstructor("async function* () {}");else if("%AsyncGenerator%"===s){var u=doEval("%AsyncGeneratorFunction%");u&&(i=u.prototype)}else if("%AsyncIteratorPrototype%"===s){var _=doEval("%AsyncGenerator%");_&&le&&(i=le(_.prototype))}return de[s]=i,i},be={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},_e=u(66743),we=u(9957),Se=_e.call(Function.call,Array.prototype.concat),xe=_e.call(Function.apply,Array.prototype.splice),Pe=_e.call(Function.call,String.prototype.replace),Te=_e.call(Function.call,String.prototype.slice),Re=_e.call(Function.call,RegExp.prototype.exec),qe=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,$e=/\\(\\)?/g,ze=function getBaseIntrinsic(s,i){var u,_=s;if(we(be,_)&&(_="%"+(u=be[_])[0]+"%"),we(de,_)){var w=de[_];if(w===ce&&(w=ye(_)),void 0===w&&!i)throw new $("intrinsic "+s+" exists, but is not available. Please file an issue!");return{alias:u,name:_,value:w}}throw new B("intrinsic "+s+" does not exist!")};s.exports=function GetIntrinsic(s,i){if("string"!=typeof s||0===s.length)throw new $("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof i)throw new $('"allowMissing" argument must be a boolean');if(null===Re(/^%?[^%]*%?$/,s))throw new B("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var u=function stringToPath(s){var i=Te(s,0,1),u=Te(s,-1);if("%"===i&&"%"!==u)throw new B("invalid intrinsic syntax, expected closing `%`");if("%"===u&&"%"!==i)throw new B("invalid intrinsic syntax, expected opening `%`");var _=[];return Pe(s,qe,(function(s,i,u,w){_[_.length]=u?Pe(w,$e,"$1"):i||s})),_}(s),_=u.length>0?u[0]:"",w=ze("%"+_+"%",i),x=w.name,j=w.value,L=!1,U=w.alias;U&&(_=U[0],xe(u,Se([0,1],U)));for(var Y=1,ee=!0;Y=u.length){var ce=Z(j,ie);j=(ee=!!ce)&&"get"in ce&&!("originalValue"in ce.get)?ce.get:j[ie]}else ee=we(j,ie),j=j[ie];ee&&!L&&(de[x]=j)}}return j}},75795:(s,i,u)=>{"use strict";var _=u(70453)("%Object.getOwnPropertyDescriptor%",!0);if(_)try{_([],"length")}catch(s){_=null}s.exports=_},30592:(s,i,u)=>{"use strict";var _=u(30655),w=function hasPropertyDescriptors(){return!!_};w.hasArrayLengthDefineBug=function hasArrayLengthDefineBug(){if(!_)return null;try{return 1!==_([],"length",{value:1}).length}catch(s){return!0}},s.exports=w},80024:s=>{"use strict";var i={__proto__:null,foo:{}},u=Object;s.exports=function hasProto(){return{__proto__:i}.foo===i.foo&&!(i instanceof u)}},64039:(s,i,u)=>{"use strict";var _="undefined"!=typeof Symbol&&Symbol,w=u(41333);s.exports=function hasNativeSymbols(){return"function"==typeof _&&("function"==typeof Symbol&&("symbol"==typeof _("foo")&&("symbol"==typeof Symbol("bar")&&w())))}},41333:s=>{"use strict";s.exports=function hasSymbols(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var s={},i=Symbol("test"),u=Object(i);if("string"==typeof i)return!1;if("[object Symbol]"!==Object.prototype.toString.call(i))return!1;if("[object Symbol]"!==Object.prototype.toString.call(u))return!1;for(i in s[i]=42,s)return!1;if("function"==typeof Object.keys&&0!==Object.keys(s).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(s).length)return!1;var _=Object.getOwnPropertySymbols(s);if(1!==_.length||_[0]!==i)return!1;if(!Object.prototype.propertyIsEnumerable.call(s,i))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var w=Object.getOwnPropertyDescriptor(s,i);if(42!==w.value||!0!==w.enumerable)return!1}return!0}},9957:(s,i,u)=>{"use strict";var _=Function.prototype.call,w=Object.prototype.hasOwnProperty,x=u(66743);s.exports=x.call(_,w)},45981:s=>{function deepFreeze(s){return s instanceof Map?s.clear=s.delete=s.set=function(){throw new Error("map is read-only")}:s instanceof Set&&(s.add=s.clear=s.delete=function(){throw new Error("set is read-only")}),Object.freeze(s),Object.getOwnPropertyNames(s).forEach((function(i){var u=s[i];"object"!=typeof u||Object.isFrozen(u)||deepFreeze(u)})),s}var i=deepFreeze,u=deepFreeze;i.default=u;class Response{constructor(s){void 0===s.data&&(s.data={}),this.data=s.data,this.isMatchIgnored=!1}ignoreMatch(){this.isMatchIgnored=!0}}function escapeHTML(s){return s.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function inherit(s,...i){const u=Object.create(null);for(const i in s)u[i]=s[i];return i.forEach((function(s){for(const i in s)u[i]=s[i]})),u}const emitsWrappingTags=s=>!!s.kind;class HTMLRenderer{constructor(s,i){this.buffer="",this.classPrefix=i.classPrefix,s.walk(this)}addText(s){this.buffer+=escapeHTML(s)}openNode(s){if(!emitsWrappingTags(s))return;let i=s.kind;s.sublanguage||(i=`${this.classPrefix}${i}`),this.span(i)}closeNode(s){emitsWrappingTags(s)&&(this.buffer+="")}value(){return this.buffer}span(s){this.buffer+=``}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(s){this.top.children.push(s)}openNode(s){const i={kind:s,children:[]};this.add(i),this.stack.push(i)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(s){return this.constructor._walk(s,this.rootNode)}static _walk(s,i){return"string"==typeof i?s.addText(i):i.children&&(s.openNode(i),i.children.forEach((i=>this._walk(s,i))),s.closeNode(i)),s}static _collapse(s){"string"!=typeof s&&s.children&&(s.children.every((s=>"string"==typeof s))?s.children=[s.children.join("")]:s.children.forEach((s=>{TokenTree._collapse(s)})))}}class TokenTreeEmitter extends TokenTree{constructor(s){super(),this.options=s}addKeyword(s,i){""!==s&&(this.openNode(i),this.addText(s),this.closeNode())}addText(s){""!==s&&this.add(s)}addSublanguage(s,i){const u=s.root;u.kind=i,u.sublanguage=!0,this.add(u)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){return!0}}function source(s){return s?"string"==typeof s?s:s.source:null}const _=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;const w="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",j="\\b\\d+(\\.\\d+)?",L="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",B="\\b(0b[01]+)",$={begin:"\\\\[\\s\\S]",relevance:0},U={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[$]},Y={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[$]},Z={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(s,i,u={}){const _=inherit({className:"comment",begin:s,end:i,contains:[]},u);return _.contains.push(Z),_.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):",relevance:0}),_},ee=COMMENT("//","$"),ie=COMMENT("/\\*","\\*/"),ae=COMMENT("#","$"),le={className:"number",begin:j,relevance:0},ce={className:"number",begin:L,relevance:0},pe={className:"number",begin:B,relevance:0},de={className:"number",begin:j+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},fe={begin:/(?=\/[^/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[$,{begin:/\[/,end:/\]/,relevance:0,contains:[$]}]}]},ye={className:"title",begin:w,relevance:0},be={className:"title",begin:x,relevance:0},_e={begin:"\\.\\s*"+x,relevance:0};var we=Object.freeze({__proto__:null,MATCH_NOTHING_RE:/\b\B/,IDENT_RE:w,UNDERSCORE_IDENT_RE:x,NUMBER_RE:j,C_NUMBER_RE:L,BINARY_NUMBER_RE:B,RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",SHEBANG:(s={})=>{const i=/^#![ ]*\//;return s.binary&&(s.begin=function concat(...s){return s.map((s=>source(s))).join("")}(i,/.*\b/,s.binary,/\b.*/)),inherit({className:"meta",begin:i,end:/$/,relevance:0,"on:begin":(s,i)=>{0!==s.index&&i.ignoreMatch()}},s)},BACKSLASH_ESCAPE:$,APOS_STRING_MODE:U,QUOTE_STRING_MODE:Y,PHRASAL_WORDS_MODE:Z,COMMENT,C_LINE_COMMENT_MODE:ee,C_BLOCK_COMMENT_MODE:ie,HASH_COMMENT_MODE:ae,NUMBER_MODE:le,C_NUMBER_MODE:ce,BINARY_NUMBER_MODE:pe,CSS_NUMBER_MODE:de,REGEXP_MODE:fe,TITLE_MODE:ye,UNDERSCORE_TITLE_MODE:be,METHOD_GUARD:_e,END_SAME_AS_BEGIN:function(s){return Object.assign(s,{"on:begin":(s,i)=>{i.data._beginMatch=s[1]},"on:end":(s,i)=>{i.data._beginMatch!==s[1]&&i.ignoreMatch()}})}});function skipIfhasPrecedingDot(s,i){"."===s.input[s.index-1]&&i.ignoreMatch()}function beginKeywords(s,i){i&&s.beginKeywords&&(s.begin="\\b("+s.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",s.__beforeBegin=skipIfhasPrecedingDot,s.keywords=s.keywords||s.beginKeywords,delete s.beginKeywords,void 0===s.relevance&&(s.relevance=0))}function compileIllegal(s,i){Array.isArray(s.illegal)&&(s.illegal=function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}(...s.illegal))}function compileMatch(s,i){if(s.match){if(s.begin||s.end)throw new Error("begin & end are not supported with match");s.begin=s.match,delete s.match}}function compileRelevance(s,i){void 0===s.relevance&&(s.relevance=1)}const Se=["of","and","for","in","not","or","if","then","parent","list","value"],xe="keyword";function compileKeywords(s,i,u=xe){const _={};return"string"==typeof s?compileList(u,s.split(" ")):Array.isArray(s)?compileList(u,s):Object.keys(s).forEach((function(u){Object.assign(_,compileKeywords(s[u],i,u))})),_;function compileList(s,u){i&&(u=u.map((s=>s.toLowerCase()))),u.forEach((function(i){const u=i.split("|");_[u[0]]=[s,scoreForKeyword(u[0],u[1])]}))}}function scoreForKeyword(s,i){return i?Number(i):function commonKeyword(s){return Se.includes(s.toLowerCase())}(s)?0:1}function compileLanguage(s,{plugins:i}){function langRe(i,u){return new RegExp(source(i),"m"+(s.case_insensitive?"i":"")+(u?"g":""))}class MultiRegex{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(s,i){i.position=this.position++,this.matchIndexes[this.matchAt]=i,this.regexes.push([i,s]),this.matchAt+=function countMatchGroups(s){return new RegExp(s.toString()+"|").exec("").length-1}(s)+1}compile(){0===this.regexes.length&&(this.exec=()=>null);const s=this.regexes.map((s=>s[1]));this.matcherRe=langRe(function join(s,i="|"){let u=0;return s.map((s=>{u+=1;const i=u;let w=source(s),x="";for(;w.length>0;){const s=_.exec(w);if(!s){x+=w;break}x+=w.substring(0,s.index),w=w.substring(s.index+s[0].length),"\\"===s[0][0]&&s[1]?x+="\\"+String(Number(s[1])+i):(x+=s[0],"("===s[0]&&u++)}return x})).map((s=>`(${s})`)).join(i)}(s),!0),this.lastIndex=0}exec(s){this.matcherRe.lastIndex=this.lastIndex;const i=this.matcherRe.exec(s);if(!i)return null;const u=i.findIndex(((s,i)=>i>0&&void 0!==s)),_=this.matchIndexes[u];return i.splice(0,u),Object.assign(i,_)}}class ResumableMultiRegex{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(s){if(this.multiRegexes[s])return this.multiRegexes[s];const i=new MultiRegex;return this.rules.slice(s).forEach((([s,u])=>i.addRule(s,u))),i.compile(),this.multiRegexes[s]=i,i}resumingScanAtSamePosition(){return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(s,i){this.rules.push([s,i]),"begin"===i.type&&this.count++}exec(s){const i=this.getMatcher(this.regexIndex);i.lastIndex=this.lastIndex;let u=i.exec(s);if(this.resumingScanAtSamePosition())if(u&&u.index===this.lastIndex);else{const i=this.getMatcher(0);i.lastIndex=this.lastIndex+1,u=i.exec(s)}return u&&(this.regexIndex+=u.position+1,this.regexIndex===this.count&&this.considerAll()),u}}if(s.compilerExtensions||(s.compilerExtensions=[]),s.contains&&s.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");return s.classNameAliases=inherit(s.classNameAliases||{}),function compileMode(i,u){const _=i;if(i.isCompiled)return _;[compileMatch].forEach((s=>s(i,u))),s.compilerExtensions.forEach((s=>s(i,u))),i.__beforeBegin=null,[beginKeywords,compileIllegal,compileRelevance].forEach((s=>s(i,u))),i.isCompiled=!0;let w=null;if("object"==typeof i.keywords&&(w=i.keywords.$pattern,delete i.keywords.$pattern),i.keywords&&(i.keywords=compileKeywords(i.keywords,s.case_insensitive)),i.lexemes&&w)throw new Error("ERR: Prefer `keywords.$pattern` to `mode.lexemes`, BOTH are not allowed. (see mode reference) ");return w=w||i.lexemes||/\w+/,_.keywordPatternRe=langRe(w,!0),u&&(i.begin||(i.begin=/\B|\b/),_.beginRe=langRe(i.begin),i.endSameAsBegin&&(i.end=i.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(_.endRe=langRe(i.end)),_.terminatorEnd=source(i.end)||"",i.endsWithParent&&u.terminatorEnd&&(_.terminatorEnd+=(i.end?"|":"")+u.terminatorEnd)),i.illegal&&(_.illegalRe=langRe(i.illegal)),i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map((function(s){return function expandOrCloneMode(s){s.variants&&!s.cachedVariants&&(s.cachedVariants=s.variants.map((function(i){return inherit(s,{variants:null},i)})));if(s.cachedVariants)return s.cachedVariants;if(dependencyOnParent(s))return inherit(s,{starts:s.starts?inherit(s.starts):null});if(Object.isFrozen(s))return inherit(s);return s}("self"===s?i:s)}))),i.contains.forEach((function(s){compileMode(s,_)})),i.starts&&compileMode(i.starts,u),_.matcher=function buildModeRegex(s){const i=new ResumableMultiRegex;return s.contains.forEach((s=>i.addRule(s.begin,{rule:s,type:"begin"}))),s.terminatorEnd&&i.addRule(s.terminatorEnd,{type:"end"}),s.illegal&&i.addRule(s.illegal,{type:"illegal"}),i}(_),_}(s)}function dependencyOnParent(s){return!!s&&(s.endsWithParent||dependencyOnParent(s.starts))}function BuildVuePlugin(s){const i={props:["language","code","autodetect"],data:function(){return{detectedLanguage:"",unknownLanguage:!1}},computed:{className(){return this.unknownLanguage?"":"hljs "+this.detectedLanguage},highlighted(){if(!this.autoDetect&&!s.getLanguage(this.language))return console.warn(`The language "${this.language}" you specified could not be found.`),this.unknownLanguage=!0,escapeHTML(this.code);let i={};return this.autoDetect?(i=s.highlightAuto(this.code),this.detectedLanguage=i.language):(i=s.highlight(this.language,this.code,this.ignoreIllegals),this.detectedLanguage=this.language),i.value},autoDetect(){return!this.language||function hasValueOrEmptyAttribute(s){return Boolean(s||""===s)}(this.autodetect)},ignoreIllegals:()=>!0},render(s){return s("pre",{},[s("code",{class:this.className,domProps:{innerHTML:this.highlighted}})])}};return{Component:i,VuePlugin:{install(s){s.component("highlightjs",i)}}}}const Pe={"after:highlightElement":({el:s,result:i,text:u})=>{const _=nodeStream(s);if(!_.length)return;const w=document.createElement("div");w.innerHTML=i.value,i.value=function mergeStreams(s,i,u){let _=0,w="";const x=[];function selectStream(){return s.length&&i.length?s[0].offset!==i[0].offset?s[0].offset"}function close(s){w+=""+tag(s)+">"}function render(s){("start"===s.event?open:close)(s.node)}for(;s.length||i.length;){let i=selectStream();if(w+=escapeHTML(u.substring(_,i[0].offset)),_=i[0].offset,i===s){x.reverse().forEach(close);do{render(i.splice(0,1)[0]),i=selectStream()}while(i===s&&i.length&&i[0].offset===_);x.reverse().forEach(open)}else"start"===i[0].event?x.push(i[0].node):x.pop(),render(i.splice(0,1)[0])}return w+escapeHTML(u.substr(_))}(_,nodeStream(w),u)}};function tag(s){return s.nodeName.toLowerCase()}function nodeStream(s){const i=[];return function _nodeStream(s,u){for(let _=s.firstChild;_;_=_.nextSibling)3===_.nodeType?u+=_.nodeValue.length:1===_.nodeType&&(i.push({event:"start",offset:u,node:_}),u=_nodeStream(_,u),tag(_).match(/br|hr|img|input/)||i.push({event:"stop",offset:u,node:_}));return u}(s,0),i}const Te={},error=s=>{console.error(s)},warn=(s,...i)=>{console.log(`WARN: ${s}`,...i)},deprecated=(s,i)=>{Te[`${s}/${i}`]||(console.log(`Deprecated as of ${s}. ${i}`),Te[`${s}/${i}`]=!0)},Re=escapeHTML,qe=inherit,$e=Symbol("nomatch");var ze=function(s){const u=Object.create(null),_=Object.create(null),w=[];let x=!0;const j=/(^(<[^>]+>|\t|)+|\n)/gm,L="Could not find the language '{}', did you forget to load/include a language module?",B={disableAutodetect:!0,name:"Plain text",contains:[]};let $={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:null,__emitter:TokenTreeEmitter};function shouldNotHighlight(s){return $.noHighlightRe.test(s)}function highlight(s,i,u,_){let w="",x="";"object"==typeof i?(w=s,u=i.ignoreIllegals,x=i.language,_=void 0):(deprecated("10.7.0","highlight(lang, code, ...args) has been deprecated."),deprecated("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),x=s,w=i);const j={code:w,language:x};fire("before:highlight",j);const L=j.result?j.result:_highlight(j.language,j.code,u,_);return L.code=j.code,fire("after:highlight",L),L}function _highlight(s,i,_,j){function keywordData(s,i){const u=U.case_insensitive?i[0].toLowerCase():i[0];return Object.prototype.hasOwnProperty.call(s.keywords,u)&&s.keywords[u]}function processBuffer(){null!=ee.subLanguage?function processSubLanguage(){if(""===le)return;let s=null;if("string"==typeof ee.subLanguage){if(!u[ee.subLanguage])return void ae.addText(le);s=_highlight(ee.subLanguage,le,!0,ie[ee.subLanguage]),ie[ee.subLanguage]=s.top}else s=highlightAuto(le,ee.subLanguage.length?ee.subLanguage:null);ee.relevance>0&&(ce+=s.relevance),ae.addSublanguage(s.emitter,s.language)}():function processKeywords(){if(!ee.keywords)return void ae.addText(le);let s=0;ee.keywordPatternRe.lastIndex=0;let i=ee.keywordPatternRe.exec(le),u="";for(;i;){u+=le.substring(s,i.index);const _=keywordData(ee,i);if(_){const[s,w]=_;if(ae.addText(u),u="",ce+=w,s.startsWith("_"))u+=i[0];else{const u=U.classNameAliases[s]||s;ae.addKeyword(i[0],u)}}else u+=i[0];s=ee.keywordPatternRe.lastIndex,i=ee.keywordPatternRe.exec(le)}u+=le.substr(s),ae.addText(u)}(),le=""}function startNewMode(s){return s.className&&ae.openNode(U.classNameAliases[s.className]||s.className),ee=Object.create(s,{parent:{value:ee}}),ee}function endOfMode(s,i,u){let _=function startsWith(s,i){const u=s&&s.exec(i);return u&&0===u.index}(s.endRe,u);if(_){if(s["on:end"]){const u=new Response(s);s["on:end"](i,u),u.isMatchIgnored&&(_=!1)}if(_){for(;s.endsParent&&s.parent;)s=s.parent;return s}}if(s.endsWithParent)return endOfMode(s.parent,i,u)}function doIgnore(s){return 0===ee.matcher.regexIndex?(le+=s[0],1):(fe=!0,0)}function doBeginMatch(s){const i=s[0],u=s.rule,_=new Response(u),w=[u.__beforeBegin,u["on:begin"]];for(const u of w)if(u&&(u(s,_),_.isMatchIgnored))return doIgnore(i);return u&&u.endSameAsBegin&&(u.endRe=function escape(s){return new RegExp(s.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}(i)),u.skip?le+=i:(u.excludeBegin&&(le+=i),processBuffer(),u.returnBegin||u.excludeBegin||(le=i)),startNewMode(u),u.returnBegin?0:i.length}function doEndMatch(s){const u=s[0],_=i.substr(s.index),w=endOfMode(ee,s,_);if(!w)return $e;const x=ee;x.skip?le+=u:(x.returnEnd||x.excludeEnd||(le+=u),processBuffer(),x.excludeEnd&&(le=u));do{ee.className&&ae.closeNode(),ee.skip||ee.subLanguage||(ce+=ee.relevance),ee=ee.parent}while(ee!==w.parent);return w.starts&&(w.endSameAsBegin&&(w.starts.endRe=w.endRe),startNewMode(w.starts)),x.returnEnd?0:u.length}let B={};function processLexeme(u,w){const j=w&&w[0];if(le+=u,null==j)return processBuffer(),0;if("begin"===B.type&&"end"===w.type&&B.index===w.index&&""===j){if(le+=i.slice(w.index,w.index+1),!x){const i=new Error("0 width match regex");throw i.languageName=s,i.badRule=B.rule,i}return 1}if(B=w,"begin"===w.type)return doBeginMatch(w);if("illegal"===w.type&&!_){const s=new Error('Illegal lexeme "'+j+'" for mode "'+(ee.className||"")+'"');throw s.mode=ee,s}if("end"===w.type){const s=doEndMatch(w);if(s!==$e)return s}if("illegal"===w.type&&""===j)return 1;if(de>1e5&&de>3*w.index){throw new Error("potential infinite loop, way more iterations than matches")}return le+=j,j.length}const U=getLanguage(s);if(!U)throw error(L.replace("{}",s)),new Error('Unknown language: "'+s+'"');const Y=compileLanguage(U,{plugins:w});let Z="",ee=j||Y;const ie={},ae=new $.__emitter($);!function processContinuations(){const s=[];for(let i=ee;i!==U;i=i.parent)i.className&&s.unshift(i.className);s.forEach((s=>ae.openNode(s)))}();let le="",ce=0,pe=0,de=0,fe=!1;try{for(ee.matcher.considerAll();;){de++,fe?fe=!1:ee.matcher.considerAll(),ee.matcher.lastIndex=pe;const s=ee.matcher.exec(i);if(!s)break;const u=processLexeme(i.substring(pe,s.index),s);pe=s.index+u}return processLexeme(i.substr(pe)),ae.closeAllNodes(),ae.finalize(),Z=ae.toHTML(),{relevance:Math.floor(ce),value:Z,language:s,illegal:!1,emitter:ae,top:ee}}catch(u){if(u.message&&u.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:u.message,context:i.slice(pe-100,pe+100),mode:u.mode},sofar:Z,relevance:0,value:Re(i),emitter:ae};if(x)return{illegal:!1,relevance:0,value:Re(i),emitter:ae,language:s,top:ee,errorRaised:u};throw u}}function highlightAuto(s,i){i=i||$.languages||Object.keys(u);const _=function justTextHighlightResult(s){const i={relevance:0,emitter:new $.__emitter($),value:Re(s),illegal:!1,top:B};return i.emitter.addText(s),i}(s),w=i.filter(getLanguage).filter(autoDetection).map((i=>_highlight(i,s,!1)));w.unshift(_);const x=w.sort(((s,i)=>{if(s.relevance!==i.relevance)return i.relevance-s.relevance;if(s.language&&i.language){if(getLanguage(s.language).supersetOf===i.language)return 1;if(getLanguage(i.language).supersetOf===s.language)return-1}return 0})),[j,L]=x,U=j;return U.second_best=L,U}const U={"before:highlightElement":({el:s})=>{$.useBR&&(s.innerHTML=s.innerHTML.replace(/\n/g,"").replace(/ /g,"\n"))},"after:highlightElement":({result:s})=>{$.useBR&&(s.value=s.value.replace(/\n/g," "))}},Y=/^(<[^>]+>|\t)+/gm,Z={"after:highlightElement":({result:s})=>{$.tabReplace&&(s.value=s.value.replace(Y,(s=>s.replace(/\t/g,$.tabReplace))))}};function highlightElement(s){let i=null;const u=function blockLanguage(s){let i=s.className+" ";i+=s.parentNode?s.parentNode.className:"";const u=$.languageDetectRe.exec(i);if(u){const i=getLanguage(u[1]);return i||(warn(L.replace("{}",u[1])),warn("Falling back to no-highlight mode for this block.",s)),i?u[1]:"no-highlight"}return i.split(/\s+/).find((s=>shouldNotHighlight(s)||getLanguage(s)))}(s);if(shouldNotHighlight(u))return;fire("before:highlightElement",{el:s,language:u}),i=s;const w=i.textContent,x=u?highlight(w,{language:u,ignoreIllegals:!0}):highlightAuto(w);fire("after:highlightElement",{el:s,result:x,text:w}),s.innerHTML=x.value,function updateClassName(s,i,u){const w=i?_[i]:u;s.classList.add("hljs"),w&&s.classList.add(w)}(s,u,x.language),s.result={language:x.language,re:x.relevance,relavance:x.relevance},x.second_best&&(s.second_best={language:x.second_best.language,re:x.second_best.relevance,relavance:x.second_best.relevance})}const initHighlighting=()=>{if(initHighlighting.called)return;initHighlighting.called=!0,deprecated("10.6.0","initHighlighting() is deprecated. Use highlightAll() instead.");document.querySelectorAll("pre code").forEach(highlightElement)};let ee=!1;function highlightAll(){if("loading"===document.readyState)return void(ee=!0);document.querySelectorAll("pre code").forEach(highlightElement)}function getLanguage(s){return s=(s||"").toLowerCase(),u[s]||u[_[s]]}function registerAliases(s,{languageName:i}){"string"==typeof s&&(s=[s]),s.forEach((s=>{_[s.toLowerCase()]=i}))}function autoDetection(s){const i=getLanguage(s);return i&&!i.disableAutodetect}function fire(s,i){const u=s;w.forEach((function(s){s[u]&&s[u](i)}))}"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(function boot(){ee&&highlightAll()}),!1),Object.assign(s,{highlight,highlightAuto,highlightAll,fixMarkup:function deprecateFixMarkup(s){return deprecated("10.2.0","fixMarkup will be removed entirely in v11.0"),deprecated("10.2.0","Please see https://github.com/highlightjs/highlight.js/issues/2534"),function fixMarkup(s){return $.tabReplace||$.useBR?s.replace(j,(s=>"\n"===s?$.useBR?" ":s:$.tabReplace?s.replace(/\t/g,$.tabReplace):s)):s}(s)},highlightElement,highlightBlock:function deprecateHighlightBlock(s){return deprecated("10.7.0","highlightBlock will be removed entirely in v12.0"),deprecated("10.7.0","Please use highlightElement now."),highlightElement(s)},configure:function configure(s){s.useBR&&(deprecated("10.3.0","'useBR' will be removed entirely in v11.0"),deprecated("10.3.0","Please see https://github.com/highlightjs/highlight.js/issues/2559")),$=qe($,s)},initHighlighting,initHighlightingOnLoad:function initHighlightingOnLoad(){deprecated("10.6.0","initHighlightingOnLoad() is deprecated. Use highlightAll() instead."),ee=!0},registerLanguage:function registerLanguage(i,_){let w=null;try{w=_(s)}catch(s){if(error("Language definition for '{}' could not be registered.".replace("{}",i)),!x)throw s;error(s),w=B}w.name||(w.name=i),u[i]=w,w.rawDefinition=_.bind(null,s),w.aliases&®isterAliases(w.aliases,{languageName:i})},unregisterLanguage:function unregisterLanguage(s){delete u[s];for(const i of Object.keys(_))_[i]===s&&delete _[i]},listLanguages:function listLanguages(){return Object.keys(u)},getLanguage,registerAliases,requireLanguage:function requireLanguage(s){deprecated("10.4.0","requireLanguage will be removed entirely in v11."),deprecated("10.4.0","Please see https://github.com/highlightjs/highlight.js/pull/2844");const i=getLanguage(s);if(i)return i;throw new Error("The '{}' language is required, but not loaded.".replace("{}",s))},autoDetection,inherit:qe,addPlugin:function addPlugin(s){!function upgradePluginAPI(s){s["before:highlightBlock"]&&!s["before:highlightElement"]&&(s["before:highlightElement"]=i=>{s["before:highlightBlock"](Object.assign({block:i.el},i))}),s["after:highlightBlock"]&&!s["after:highlightElement"]&&(s["after:highlightElement"]=i=>{s["after:highlightBlock"](Object.assign({block:i.el},i))})}(s),w.push(s)},vuePlugin:BuildVuePlugin(s).VuePlugin}),s.debugMode=function(){x=!1},s.safeMode=function(){x=!0},s.versionString="10.7.3";for(const s in we)"object"==typeof we[s]&&i(we[s]);return Object.assign(s,we),s.addPlugin(U),s.addPlugin(Pe),s.addPlugin(Z),s}({});s.exports=ze},35344:s=>{function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function bash(s){const i={},u={begin:/\$\{/,end:/\}/,contains:["self",{begin:/:-/,contains:[i]}]};Object.assign(i,{className:"variable",variants:[{begin:concat(/\$[\w\d#@][\w\d_]*/,"(?![\\w\\d])(?![$])")},u]});const _={className:"subst",begin:/\$\(/,end:/\)/,contains:[s.BACKSLASH_ESCAPE]},w={begin:/<<-?\s*(?=\w+)/,starts:{contains:[s.END_SAME_AS_BEGIN({begin:/(\w+)/,end:/(\w+)/,className:"string"})]}},x={className:"string",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE,i,_]};_.contains.push(x);const j={begin:/\$\(\(/,end:/\)\)/,contains:[{begin:/\d+#[0-9a-f]+/,className:"number"},s.NUMBER_MODE,i]},L=s.SHEBANG({binary:`(${["fish","bash","zsh","sh","csh","ksh","tcsh","dash","scsh"].join("|")})`,relevance:10}),B={className:"function",begin:/\w[\w\d_]*\s*\(\s*\)\s*\{/,returnBegin:!0,contains:[s.inherit(s.TITLE_MODE,{begin:/\w[\w\d_]*/})],relevance:0};return{name:"Bash",aliases:["sh","zsh"],keywords:{$pattern:/\b[a-z._-]+\b/,keyword:"if then else elif fi for while in do done case esac function",literal:"true false",built_in:"break cd continue eval exec exit export getopts hash pwd readonly return shift test times trap umask unset alias bind builtin caller command declare echo enable help let local logout mapfile printf read readarray source type typeset ulimit unalias set shopt autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate fc fg float functions getcap getln history integer jobs kill limit log noglob popd print pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof zpty zregexparse zsocket zstyle ztcp"},contains:[L,s.SHEBANG(),B,j,s.HASH_COMMENT_MODE,w,x,{className:"",begin:/\\"/},{className:"string",begin:/'/,end:/'/},i]}}},73402:s=>{function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function http(s){const i="HTTP/(2|1\\.[01])",u={className:"attribute",begin:concat("^",/[A-Za-z][A-Za-z0-9-]*/,"(?=\\:\\s)"),starts:{contains:[{className:"punctuation",begin:/: /,relevance:0,starts:{end:"$",relevance:0}}]}},_=[u,{begin:"\\n\\n",starts:{subLanguage:[],endsWithParent:!0}}];return{name:"HTTP",aliases:["https"],illegal:/\S/,contains:[{begin:"^(?="+i+" \\d{3})",end:/$/,contains:[{className:"meta",begin:i},{className:"number",begin:"\\b\\d{3}\\b"}],starts:{end:/\b\B/,illegal:/\S/,contains:_}},{begin:"(?=^[A-Z]+ (.*?) "+i+"$)",end:/$/,contains:[{className:"string",begin:" ",end:" ",excludeBegin:!0,excludeEnd:!0},{className:"meta",begin:i},{className:"keyword",begin:"[A-Z]+"}],starts:{end:/\b\B/,illegal:/\S/,contains:_}},s.inherit(u,{relevance:0})]}}},95089:s=>{const i="[A-Za-z$_][0-9A-Za-z$_]*",u=["as","in","of","if","for","while","finally","var","new","function","do","return","void","else","break","catch","instanceof","with","throw","case","default","try","switch","continue","typeof","delete","let","yield","const","class","debugger","async","await","static","import","from","export","extends"],_=["true","false","null","undefined","NaN","Infinity"],w=[].concat(["setInterval","setTimeout","clearInterval","clearTimeout","require","exports","eval","isFinite","isNaN","parseFloat","parseInt","decodeURI","decodeURIComponent","encodeURI","encodeURIComponent","escape","unescape"],["arguments","this","super","console","window","document","localStorage","module","global"],["Intl","DataView","Number","Math","Date","String","RegExp","Object","Function","Boolean","Error","Symbol","Set","Map","WeakSet","WeakMap","Proxy","Reflect","JSON","Promise","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Float32Array","Array","Uint8Array","Uint8ClampedArray","ArrayBuffer","BigInt64Array","BigUint64Array","BigInt"],["EvalError","InternalError","RangeError","ReferenceError","SyntaxError","TypeError","URIError"]);function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>function source(s){return s?"string"==typeof s?s:s.source:null}(s))).join("")}s.exports=function javascript(s){const x=i,j="<>",L=">",B={begin:/<[A-Za-z0-9\\._:-]+/,end:/\/[A-Za-z0-9\\._:-]+>|\/>/,isTrulyOpeningTag:(s,i)=>{const u=s[0].length+s.index,_=s.input[u];"<"!==_?">"===_&&(((s,{after:i})=>{const u=""+s[0].slice(1);return-1!==s.input.indexOf(u,i)})(s,{after:u})||i.ignoreMatch()):i.ignoreMatch()}},$={$pattern:i,keyword:u,literal:_,built_in:w},U="[0-9](_?[0-9])*",Y=`\\.(${U})`,Z="0|[1-9](_?[0-9])*|0[0-7]*[89][0-9]*",ee={className:"number",variants:[{begin:`(\\b(${Z})((${Y})|\\.)?|(${Y}))[eE][+-]?(${U})\\b`},{begin:`\\b(${Z})\\b((${Y})\\b|\\.)?|(${Y})\\b`},{begin:"\\b(0|[1-9](_?[0-9])*)n\\b"},{begin:"\\b0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?\\b"},{begin:"\\b0[bB][0-1](_?[0-1])*n?\\b"},{begin:"\\b0[oO][0-7](_?[0-7])*n?\\b"},{begin:"\\b0[0-7]+n?\\b"}],relevance:0},ie={className:"subst",begin:"\\$\\{",end:"\\}",keywords:$,contains:[]},ae={begin:"html`",end:"",starts:{end:"`",returnEnd:!1,contains:[s.BACKSLASH_ESCAPE,ie],subLanguage:"xml"}},le={begin:"css`",end:"",starts:{end:"`",returnEnd:!1,contains:[s.BACKSLASH_ESCAPE,ie],subLanguage:"css"}},ce={className:"string",begin:"`",end:"`",contains:[s.BACKSLASH_ESCAPE,ie]},pe={className:"comment",variants:[s.COMMENT(/\/\*\*(?!\/)/,"\\*/",{relevance:0,contains:[{className:"doctag",begin:"@[A-Za-z]+",contains:[{className:"type",begin:"\\{",end:"\\}",relevance:0},{className:"variable",begin:x+"(?=\\s*(-)|$)",endsParent:!0,relevance:0},{begin:/(?=[^\n])\s/,relevance:0}]}]}),s.C_BLOCK_COMMENT_MODE,s.C_LINE_COMMENT_MODE]},de=[s.APOS_STRING_MODE,s.QUOTE_STRING_MODE,ae,le,ce,ee,s.REGEXP_MODE];ie.contains=de.concat({begin:/\{/,end:/\}/,keywords:$,contains:["self"].concat(de)});const fe=[].concat(pe,ie.contains),ye=fe.concat([{begin:/\(/,end:/\)/,keywords:$,contains:["self"].concat(fe)}]),be={className:"params",begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:$,contains:ye};return{name:"Javascript",aliases:["js","jsx","mjs","cjs"],keywords:$,exports:{PARAMS_CONTAINS:ye},illegal:/#(?![$_A-z])/,contains:[s.SHEBANG({label:"shebang",binary:"node",relevance:5}),{label:"use_strict",className:"meta",relevance:10,begin:/^\s*['"]use (strict|asm)['"]/},s.APOS_STRING_MODE,s.QUOTE_STRING_MODE,ae,le,ce,pe,ee,{begin:concat(/[{,\n]\s*/,lookahead(concat(/(((\/\/.*$)|(\/\*(\*[^/]|[^*])*\*\/))\s*)*/,x+"\\s*:"))),relevance:0,contains:[{className:"attr",begin:x+lookahead("\\s*:"),relevance:0}]},{begin:"("+s.RE_STARTERS_RE+"|\\b(case|return|throw)\\b)\\s*",keywords:"return throw case",contains:[pe,s.REGEXP_MODE,{className:"function",begin:"(\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)|"+s.UNDERSCORE_IDENT_RE+")\\s*=>",returnBegin:!0,end:"\\s*=>",contains:[{className:"params",variants:[{begin:s.UNDERSCORE_IDENT_RE,relevance:0},{className:null,begin:/\(\s*\)/,skip:!0},{begin:/\(/,end:/\)/,excludeBegin:!0,excludeEnd:!0,keywords:$,contains:ye}]}]},{begin:/,/,relevance:0},{className:"",begin:/\s/,end:/\s*/,skip:!0},{variants:[{begin:j,end:L},{begin:B.begin,"on:begin":B.isTrulyOpeningTag,end:B.end}],subLanguage:"xml",contains:[{begin:B.begin,end:B.end,skip:!0,contains:["self"]}]}],relevance:0},{className:"function",beginKeywords:"function",end:/[{;]/,excludeEnd:!0,keywords:$,contains:["self",s.inherit(s.TITLE_MODE,{begin:x}),be],illegal:/%/},{beginKeywords:"while if switch catch for"},{className:"function",begin:s.UNDERSCORE_IDENT_RE+"\\([^()]*(\\([^()]*(\\([^()]*\\)[^()]*)*\\)[^()]*)*\\)\\s*\\{",returnBegin:!0,contains:[be,s.inherit(s.TITLE_MODE,{begin:x})]},{variants:[{begin:"\\."+x},{begin:"\\$"+x}],relevance:0},{className:"class",beginKeywords:"class",end:/[{;=]/,excludeEnd:!0,illegal:/[:"[\]]/,contains:[{beginKeywords:"extends"},s.UNDERSCORE_TITLE_MODE]},{begin:/\b(?=constructor)/,end:/[{;]/,excludeEnd:!0,contains:[s.inherit(s.TITLE_MODE,{begin:x}),"self",be]},{begin:"(get|set)\\s+(?="+x+"\\()",end:/\{/,keywords:"get set",contains:[s.inherit(s.TITLE_MODE,{begin:x}),{begin:/\(\)/},be]},{begin:/\$[(.]/}]}}},65772:s=>{s.exports=function json(s){const i={literal:"true false null"},u=[s.C_LINE_COMMENT_MODE,s.C_BLOCK_COMMENT_MODE],_=[s.QUOTE_STRING_MODE,s.C_NUMBER_MODE],w={end:",",endsWithParent:!0,excludeEnd:!0,contains:_,keywords:i},x={begin:/\{/,end:/\}/,contains:[{className:"attr",begin:/"/,end:/"/,contains:[s.BACKSLASH_ESCAPE],illegal:"\\n"},s.inherit(w,{begin:/:/})].concat(u),illegal:"\\S"},j={begin:"\\[",end:"\\]",contains:[s.inherit(w)],illegal:"\\S"};return _.push(x,j),u.forEach((function(s){_.push(s)})),{name:"JSON",contains:_,keywords:i,illegal:"\\S"}}},26571:s=>{s.exports=function powershell(s){const i={$pattern:/-?[A-z\.\-]+\b/,keyword:"if else foreach return do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch hidden static parameter",built_in:"ac asnp cat cd CFS chdir clc clear clhy cli clp cls clv cnsn compare copy cp cpi cpp curl cvpa dbp del diff dir dnsn ebp echo|0 epal epcsv epsn erase etsn exsn fc fhx fl ft fw gal gbp gc gcb gci gcm gcs gdr gerr ghy gi gin gjb gl gm gmo gp gps gpv group gsn gsnp gsv gtz gu gv gwmi h history icm iex ihy ii ipal ipcsv ipmo ipsn irm ise iwmi iwr kill lp ls man md measure mi mount move mp mv nal ndr ni nmo npssc nsn nv ogv oh popd ps pushd pwd r rbp rcjb rcsn rd rdr ren ri rjb rm rmdir rmo rni rnp rp rsn rsnp rujb rv rvpa rwmi sajb sal saps sasv sbp sc scb select set shcm si sl sleep sls sort sp spjb spps spsv start stz sujb sv swmi tee trcm type wget where wjb write"},u={begin:"`[\\s\\S]",relevance:0},_={className:"variable",variants:[{begin:/\$\B/},{className:"keyword",begin:/\$this/},{begin:/\$[\w\d][\w\d_:]*/}]},w={className:"string",variants:[{begin:/"/,end:/"/},{begin:/@"/,end:/^"@/}],contains:[u,_,{className:"variable",begin:/\$[A-z]/,end:/[^A-z]/}]},x={className:"string",variants:[{begin:/'/,end:/'/},{begin:/@'/,end:/^'@/}]},j=s.inherit(s.COMMENT(null,null),{variants:[{begin:/#/,end:/$/},{begin:/<#/,end:/#>/}],contains:[{className:"doctag",variants:[{begin:/\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/},{begin:/\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/}]}]}),L={className:"built_in",variants:[{begin:"(".concat("Add|Clear|Close|Copy|Enter|Exit|Find|Format|Get|Hide|Join|Lock|Move|New|Open|Optimize|Pop|Push|Redo|Remove|Rename|Reset|Resize|Search|Select|Set|Show|Skip|Split|Step|Switch|Undo|Unlock|Watch|Backup|Checkpoint|Compare|Compress|Convert|ConvertFrom|ConvertTo|Dismount|Edit|Expand|Export|Group|Import|Initialize|Limit|Merge|Mount|Out|Publish|Restore|Save|Sync|Unpublish|Update|Approve|Assert|Build|Complete|Confirm|Deny|Deploy|Disable|Enable|Install|Invoke|Register|Request|Restart|Resume|Start|Stop|Submit|Suspend|Uninstall|Unregister|Wait|Debug|Measure|Ping|Repair|Resolve|Test|Trace|Connect|Disconnect|Read|Receive|Send|Write|Block|Grant|Protect|Revoke|Unblock|Unprotect|Use|ForEach|Sort|Tee|Where",")+(-)[\\w\\d]+")}]},B={className:"class",beginKeywords:"class enum",end:/\s*[{]/,excludeEnd:!0,relevance:0,contains:[s.TITLE_MODE]},$={className:"function",begin:/function\s+/,end:/\s*\{|$/,excludeEnd:!0,returnBegin:!0,relevance:0,contains:[{begin:"function",relevance:0,className:"keyword"},{className:"title",begin:/\w[\w\d]*((-)[\w\d]+)*/,relevance:0},{begin:/\(/,end:/\)/,className:"params",relevance:0,contains:[_]}]},U={begin:/using\s/,end:/$/,returnBegin:!0,contains:[w,x,{className:"keyword",begin:/(using|assembly|command|module|namespace|type)/}]},Y={variants:[{className:"operator",begin:"(".concat("-and|-as|-band|-bnot|-bor|-bxor|-casesensitive|-ccontains|-ceq|-cge|-cgt|-cle|-clike|-clt|-cmatch|-cne|-cnotcontains|-cnotlike|-cnotmatch|-contains|-creplace|-csplit|-eq|-exact|-f|-file|-ge|-gt|-icontains|-ieq|-ige|-igt|-ile|-ilike|-ilt|-imatch|-in|-ine|-inotcontains|-inotlike|-inotmatch|-ireplace|-is|-isnot|-isplit|-join|-le|-like|-lt|-match|-ne|-not|-notcontains|-notin|-notlike|-notmatch|-or|-regex|-replace|-shl|-shr|-split|-wildcard|-xor",")\\b")},{className:"literal",begin:/(-)[\w\d]+/,relevance:0}]},Z={className:"function",begin:/\[.*\]\s*[\w]+[ ]??\(/,end:/$/,returnBegin:!0,relevance:0,contains:[{className:"keyword",begin:"(".concat(i.keyword.toString().replace(/\s/g,"|"),")\\b"),endsParent:!0,relevance:0},s.inherit(s.TITLE_MODE,{endsParent:!0})]},ee=[Z,j,u,s.NUMBER_MODE,w,x,L,_,{className:"literal",begin:/\$(null|true|false)\b/},{className:"selector-tag",begin:/@\B/,relevance:0}],ie={begin:/\[/,end:/\]/,excludeBegin:!0,excludeEnd:!0,relevance:0,contains:[].concat("self",ee,{begin:"("+["string","char","byte","int","long","bool","decimal","single","double","DateTime","xml","array","hashtable","void"].join("|")+")",className:"built_in",relevance:0},{className:"type",begin:/[\.\w\d]+/,relevance:0})};return Z.contains.unshift(ie),{name:"PowerShell",aliases:["ps","ps1"],case_insensitive:!0,keywords:i,contains:ee.concat(B,$,U,Y,ie)}}},17285:s=>{function source(s){return s?"string"==typeof s?s:s.source:null}function lookahead(s){return concat("(?=",s,")")}function concat(...s){return s.map((s=>source(s))).join("")}function either(...s){return"("+s.map((s=>source(s))).join("|")+")"}s.exports=function xml(s){const i=concat(/[A-Z_]/,function optional(s){return concat("(",s,")?")}(/[A-Z0-9_.-]*:/),/[A-Z0-9_.-]*/),u={className:"symbol",begin:/&[a-z]+;|[0-9]+;|[a-f0-9]+;/},_={begin:/\s/,contains:[{className:"meta-keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]},w=s.inherit(_,{begin:/\(/,end:/\)/}),x=s.inherit(s.APOS_STRING_MODE,{className:"meta-string"}),j=s.inherit(s.QUOTE_STRING_MODE,{className:"meta-string"}),L={endsWithParent:!0,illegal:/,relevance:0,contains:[{className:"attr",begin:/[A-Za-z0-9._:-]+/,relevance:0},{begin:/=\s*/,relevance:0,contains:[{className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[u]},{begin:/'/,end:/'/,contains:[u]},{begin:/[^\s"'=<>`]+/}]}]}]};return{name:"HTML, XML",aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],case_insensitive:!0,contains:[{className:"meta",begin://,relevance:10,contains:[_,j,x,w,{begin:/\[/,end:/\]/,contains:[{className:"meta",begin://,contains:[_,w,j,x]}]}]},s.COMMENT(//,{relevance:10}),{begin://,relevance:10},u,{className:"meta",begin:/<\?xml/,end:/\?>/,relevance:10},{className:"tag",begin:/