diff --git a/Makefile b/Makefile index 039259c..4d0517a 100644 --- a/Makefile +++ b/Makefile @@ -52,3 +52,12 @@ run-migration: create-mocks: @rm -rf mocks mockery --all + +.PHONY: swag-init +swag-init: + swag init -g cmd/psqlqueue/main.go + swag fmt + +.PHONY: run-server +run-server: + go run cmd/psqlqueue/main.go server diff --git a/cmd/psqlqueue/main.go b/cmd/psqlqueue/main.go index b8ef815..bc3a8ea 100644 --- a/cmd/psqlqueue/main.go +++ b/cmd/psqlqueue/main.go @@ -8,6 +8,9 @@ import ( "github.com/allisson/psqlqueue/db/migrations" "github.com/allisson/psqlqueue/domain" + "github.com/allisson/psqlqueue/http" + "github.com/allisson/psqlqueue/repository" + "github.com/allisson/psqlqueue/service" ) func main() { @@ -28,6 +31,35 @@ func main() { return migrations.Migrate(cfg.DatabaseURL) }, }, + { + Name: "server", + Aliases: []string{"s"}, + Usage: "run http server", + Action: func(c *cli.Context) error { + pool, err := repository.SetupDatabaseConnection(c.Context, cfg) + if err != nil { + return err + } + defer pool.Close() + + // repositories + queueRepository := repository.NewQueue(pool) + messageRepository := repository.NewMessage(pool) + + // services + queueService := service.NewQueue(queueRepository) + messageService := service.NewMessage(messageRepository, queueRepository) + + // http handlers + queueHandler := http.NewQueueHandler(queueService) + messageHandler := http.NewMessageHandler(messageService) + + // run http server + http.RunServer(c.Context, cfg, http.SetupRouter(logger, queueHandler, messageHandler)) + + return nil + }, + }, }, } diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..4a3bf6b --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,813 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/queue/{queue_id}/messages": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Add a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "description": "Add a message", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MessageRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "List queues", + "parameters": [ + { + "type": "integer", + "description": "The limit indicates the maximum number of items to return", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "The offset indicates the starting position of the query in relation to the complete set of unpaginated items", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Add a queue", + "parameters": [ + { + "description": "Add a queue", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QueueRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Show a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Update a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "description": "Update a queue", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QueueUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Delete a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/cleanup": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Cleanup a queue removing expired and acked messages", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "List messages", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Label", + "name": "label", + "in": "path" + }, + { + "type": "integer", + "description": "The limit indicates the maximum number of items to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/MessageListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages/{message_id}/ack": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Ack a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message id", + "name": "message_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages/{message_id}/nack": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Nack a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message id", + "name": "message_id", + "in": "path", + "required": true + }, + { + "description": "Nack a message", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MessageNackRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/purge": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Purge a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/stats": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Get the queue stats", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueStatsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { + "$ref": "#/definitions/ErrorResponseCode" + }, + "details": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "ErrorResponseCode": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "internalServerErrorCode", + "malformedRequest", + "requestValidationFailedCode", + "queueAlreadyExists", + "queueNotFound", + "messageNotFound" + ] + }, + "MessageListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/MessageResponse" + } + }, + "limit": { + "type": "integer", + "example": 10 + } + } + }, + "MessageNackRequest": { + "type": "object", + "required": [ + "visibility_timeout_seconds" + ], + "properties": { + "visibility_timeout_seconds": { + "type": "integer" + } + } + }, + "MessageRequest": { + "type": "object", + "required": [ + "body" + ], + "properties": { + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string" + }, + "label": { + "type": "string" + } + } + }, + "MessageResponse": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + }, + "delivery_attempts": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "string", + "example": "7b98fe50-affd-4685-bd7d-3ae5e41493af" + }, + "label": { + "type": "string" + }, + "queue_id": { + "type": "string", + "example": "my-new-queue" + } + } + }, + "QueueListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/QueueResponse" + } + }, + "limit": { + "type": "integer", + "example": 10 + }, + "offset": { + "type": "integer", + "example": 0 + } + } + }, + "QueueRequest": { + "type": "object", + "required": [ + "ack_deadline_seconds", + "delivery_delay_seconds", + "id", + "message_retention_seconds" + ], + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "id": { + "type": "string", + "example": "my-new-queue" + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + } + } + }, + "QueueResponse": { + "type": "object", + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "created_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "id": { + "type": "string", + "example": "my-new-queue" + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + }, + "updated_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + } + } + }, + "QueueStatsResponse": { + "type": "object", + "properties": { + "num_undelivered_messages": { + "type": "integer", + "example": 1 + }, + "oldest_unacked_message_age_seconds": { + "type": "integer", + "example": 1 + } + } + }, + "QueueUpdateRequest": { + "type": "object", + "required": [ + "ack_deadline_seconds", + "delivery_delay_seconds", + "message_retention_seconds" + ], + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + } + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..a1691b5 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,784 @@ +{ + "swagger": "2.0", + "info": { + "contact": {} + }, + "paths": { + "/queue/{queue_id}/messages": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Add a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "description": "Add a message", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MessageRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "List queues", + "parameters": [ + { + "type": "integer", + "description": "The limit indicates the maximum number of items to return", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "The offset indicates the starting position of the query in relation to the complete set of unpaginated items", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueListResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Add a queue", + "parameters": [ + { + "description": "Add a queue", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QueueRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Show a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Update a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "description": "Update a queue", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/QueueUpdateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + }, + "delete": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Delete a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/cleanup": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Cleanup a queue removing expired and acked messages", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "List messages", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Label", + "name": "label", + "in": "path" + }, + { + "type": "integer", + "description": "The limit indicates the maximum number of items to return", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/MessageListResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages/{message_id}/ack": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Ack a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message id", + "name": "message_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/messages/{message_id}/nack": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "messages" + ], + "summary": "Nack a message", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Message id", + "name": "message_id", + "in": "path", + "required": true + }, + { + "description": "Nack a message", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/MessageNackRequest" + } + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/purge": { + "put": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Purge a queue", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + }, + "/queues/{queue_id}/stats": { + "get": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "queues" + ], + "summary": "Get the queue stats", + "parameters": [ + { + "type": "string", + "description": "Queue id", + "name": "queue_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/QueueStatsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/ErrorResponse" + } + } + } + } + } + }, + "definitions": { + "ErrorResponse": { + "type": "object", + "properties": { + "code": { + "$ref": "#/definitions/ErrorResponseCode" + }, + "details": { + "type": "string" + }, + "message": { + "type": "string" + } + } + }, + "ErrorResponseCode": { + "type": "integer", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], + "x-enum-varnames": [ + "internalServerErrorCode", + "malformedRequest", + "requestValidationFailedCode", + "queueAlreadyExists", + "queueNotFound", + "messageNotFound" + ] + }, + "MessageListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/MessageResponse" + } + }, + "limit": { + "type": "integer", + "example": 10 + } + } + }, + "MessageNackRequest": { + "type": "object", + "required": [ + "visibility_timeout_seconds" + ], + "properties": { + "visibility_timeout_seconds": { + "type": "integer" + } + } + }, + "MessageRequest": { + "type": "object", + "required": [ + "body" + ], + "properties": { + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string" + }, + "label": { + "type": "string" + } + } + }, + "MessageResponse": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "body": { + "type": "string" + }, + "created_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + }, + "delivery_attempts": { + "type": "integer", + "example": 1 + }, + "id": { + "type": "string", + "example": "7b98fe50-affd-4685-bd7d-3ae5e41493af" + }, + "label": { + "type": "string" + }, + "queue_id": { + "type": "string", + "example": "my-new-queue" + } + } + }, + "QueueListResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/QueueResponse" + } + }, + "limit": { + "type": "integer", + "example": 10 + }, + "offset": { + "type": "integer", + "example": 0 + } + } + }, + "QueueRequest": { + "type": "object", + "required": [ + "ack_deadline_seconds", + "delivery_delay_seconds", + "id", + "message_retention_seconds" + ], + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "id": { + "type": "string", + "example": "my-new-queue" + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + } + } + }, + "QueueResponse": { + "type": "object", + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "created_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "id": { + "type": "string", + "example": "my-new-queue" + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + }, + "updated_at": { + "type": "string", + "example": "2023-08-17T00:00:00Z" + } + } + }, + "QueueStatsResponse": { + "type": "object", + "properties": { + "num_undelivered_messages": { + "type": "integer", + "example": 1 + }, + "oldest_unacked_message_age_seconds": { + "type": "integer", + "example": 1 + } + } + }, + "QueueUpdateRequest": { + "type": "object", + "required": [ + "ack_deadline_seconds", + "delivery_delay_seconds", + "message_retention_seconds" + ], + "properties": { + "ack_deadline_seconds": { + "type": "integer", + "example": 30 + }, + "delivery_delay_seconds": { + "type": "integer", + "example": 0 + }, + "message_retention_seconds": { + "type": "integer", + "example": 604800 + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..a9d5ed8 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,524 @@ +definitions: + ErrorResponse: + properties: + code: + $ref: '#/definitions/ErrorResponseCode' + details: + type: string + message: + type: string + type: object + ErrorResponseCode: + enum: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + type: integer + x-enum-varnames: + - internalServerErrorCode + - malformedRequest + - requestValidationFailedCode + - queueAlreadyExists + - queueNotFound + - messageNotFound + MessageListResponse: + properties: + data: + items: + $ref: '#/definitions/MessageResponse' + type: array + limit: + example: 10 + type: integer + type: object + MessageNackRequest: + properties: + visibility_timeout_seconds: + type: integer + required: + - visibility_timeout_seconds + type: object + MessageRequest: + properties: + attributes: + additionalProperties: + type: string + type: object + body: + type: string + label: + type: string + required: + - body + type: object + MessageResponse: + properties: + attributes: + additionalProperties: + type: string + type: object + body: + type: string + created_at: + example: "2023-08-17T00:00:00Z" + type: string + delivery_attempts: + example: 1 + type: integer + id: + example: 7b98fe50-affd-4685-bd7d-3ae5e41493af + type: string + label: + type: string + queue_id: + example: my-new-queue + type: string + type: object + QueueListResponse: + properties: + data: + items: + $ref: '#/definitions/QueueResponse' + type: array + limit: + example: 10 + type: integer + offset: + example: 0 + type: integer + type: object + QueueRequest: + properties: + ack_deadline_seconds: + example: 30 + type: integer + delivery_delay_seconds: + example: 0 + type: integer + id: + example: my-new-queue + type: string + message_retention_seconds: + example: 604800 + type: integer + required: + - ack_deadline_seconds + - delivery_delay_seconds + - id + - message_retention_seconds + type: object + QueueResponse: + properties: + ack_deadline_seconds: + example: 30 + type: integer + created_at: + example: "2023-08-17T00:00:00Z" + type: string + delivery_delay_seconds: + example: 0 + type: integer + id: + example: my-new-queue + type: string + message_retention_seconds: + example: 604800 + type: integer + updated_at: + example: "2023-08-17T00:00:00Z" + type: string + type: object + QueueStatsResponse: + properties: + num_undelivered_messages: + example: 1 + type: integer + oldest_unacked_message_age_seconds: + example: 1 + type: integer + type: object + QueueUpdateRequest: + properties: + ack_deadline_seconds: + example: 30 + type: integer + delivery_delay_seconds: + example: 0 + type: integer + message_retention_seconds: + example: 604800 + type: integer + required: + - ack_deadline_seconds + - delivery_delay_seconds + - message_retention_seconds + type: object +info: + contact: {} +paths: + /queue/{queue_id}/messages: + post: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + - description: Add a message + in: body + name: request + required: true + schema: + $ref: '#/definitions/MessageRequest' + produces: + - application/json + responses: + "204": + description: No Content + "400": + description: Bad Request + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Add a message + tags: + - messages + /queues: + get: + consumes: + - application/json + parameters: + - description: The limit indicates the maximum number of items to return + in: query + name: limit + type: integer + - description: The offset indicates the starting position of the query in relation + to the complete set of unpaginated items + in: query + name: offset + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/QueueListResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: List queues + tags: + - queues + post: + consumes: + - application/json + parameters: + - description: Add a queue + in: body + name: request + required: true + schema: + $ref: '#/definitions/QueueRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/QueueResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Add a queue + tags: + - queues + /queues/{queue_id}: + delete: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Delete a queue + tags: + - queues + get: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/QueueResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Show a queue + tags: + - queues + put: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + - description: Update a queue + in: body + name: request + required: true + schema: + $ref: '#/definitions/QueueUpdateRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/QueueResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Update a queue + tags: + - queues + /queues/{queue_id}/cleanup: + put: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Cleanup a queue removing expired and acked messages + tags: + - queues + /queues/{queue_id}/messages: + get: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + - description: Label + in: path + name: label + type: string + - description: The limit indicates the maximum number of items to return + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/MessageListResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: List messages + tags: + - messages + /queues/{queue_id}/messages/{message_id}/ack: + put: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + - description: Message id + in: path + name: message_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Ack a message + tags: + - messages + /queues/{queue_id}/messages/{message_id}/nack: + put: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + - description: Message id + in: path + name: message_id + required: true + type: string + - description: Nack a message + in: body + name: request + required: true + schema: + $ref: '#/definitions/MessageNackRequest' + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Nack a message + tags: + - messages + /queues/{queue_id}/purge: + put: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Purge a queue + tags: + - queues + /queues/{queue_id}/stats: + get: + consumes: + - application/json + parameters: + - description: Queue id + in: path + name: queue_id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/QueueStatsResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/ErrorResponse' + summary: Get the queue stats + tags: + - queues +swagger: "2.0" diff --git a/domain/message.go b/domain/message.go index 9203d3d..12b151d 100644 --- a/domain/message.go +++ b/domain/message.go @@ -71,7 +71,7 @@ type MessageRepository interface { // MessageService is the service interface for the Message entity. type MessageService interface { Create(ctx context.Context, message *Message) error - List(ctx context.Context, queueID, label *string, limit uint) ([]*Message, error) + List(ctx context.Context, queueID string, label *string, limit uint) ([]*Message, error) Ack(ctx context.Context, id string) error Nack(ctx context.Context, id string, visibilityTimeoutSeconds uint) error } diff --git a/go.mod b/go.mod index df5d0b7..8f81c33 100644 --- a/go.mod +++ b/go.mod @@ -6,19 +6,40 @@ require ( github.com/allisson/go-env v0.4.0 github.com/allisson/pgxutil/v2 v2.3.1 github.com/allisson/sqlquery v1.3.1 + github.com/gin-gonic/gin v1.9.1 github.com/golang-migrate/migrate/v4 v4.17.0 github.com/jackc/pgx/v5 v5.5.1 github.com/jellydator/validation v1.1.0 github.com/joho/godotenv v1.5.1 github.com/oklog/ulid/v2 v2.1.0 + github.com/samber/slog-gin v1.7.1 github.com/stretchr/testify v1.8.4 + github.com/swaggo/files v1.0.1 + github.com/swaggo/gin-swagger v1.6.0 + github.com/swaggo/swag v1.16.2 github.com/urfave/cli/v2 v2.27.0 ) require ( + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/PuerkitoBio/purell v1.1.1 // indirect + github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/georgysavva/scany/v2 v2.0.0 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.19.6 // indirect + github.com/go-openapi/spec v0.20.4 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/uuid v1.4.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/go-sqlbuilder v1.20.0 // indirect @@ -33,13 +54,32 @@ require ( github.com/jackc/pgtype v1.14.0 // indirect github.com/jackc/pgx/v4 v4.18.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect go.uber.org/atomic v1.7.0 // indirect + golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.18.0 // indirect golang.org/x/sync v0.5.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/tools v0.10.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 211b9b3..1029505 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,16 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/allisson/go-env v0.4.0 h1:ZfN9L8RkgbMuOpmPNZaXVnoF7rZ1PphG4TNNcoLoX3M= github.com/allisson/go-env v0.4.0/go.mod h1:3cDykA7gUjwFmN5O1yA7Y0+xgFdygX5UN6Pl5tc8O8A= github.com/allisson/pgxutil/v2 v2.3.1 h1:xwAua0ng4YYTAdMxtJrfe594m45dPI6YJ8Vo1FhM4W0= @@ -13,6 +19,12 @@ github.com/allisson/sqlquery v1.3.1 h1:D/DBvZRqmw4bPP5v5zrNooVAgHtU1pZf/CT5mMrW3 github.com/allisson/sqlquery v1.3.1/go.mod h1:SfAu/CPxjTQU7cYVGljdFtwT8s0QZ/6BM6bzrxenoBo= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= @@ -22,6 +34,7 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +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= @@ -35,11 +48,39 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU= github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38= +github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= +github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= +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-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-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M= +github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +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.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= @@ -48,7 +89,14 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-migrate/migrate/v4 v4.17.0 h1:rd40H3QXU0AA4IoLllFcEAEo9dYKRHYND2gB4p7xcaU= github.com/golang-migrate/migrate/v4 v4.17.0/go.mod h1:+Cp2mtLP4/aXDTKb9wmXYitdrNx2HGs45rbWAo6OsKM= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -118,7 +166,14 @@ github.com/jellydator/validation v1.1.0 h1:TBkx56y6dd0By2AhtStRdTIhDjtcuoSE9w6G6 github.com/jellydator/validation v1.1.0/go.mod h1:AaCjfkQ4Ykdcb+YCwqCtaI3wDsf2UAGhJ06lJs0VgOw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -129,21 +184,35 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 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/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/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -153,6 +222,8 @@ github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM github.com/pashagolub/pgxmock/v2 v2.6.0 h1:Dk07FlzdMOrqdxrelmEeWP9uqcDm757HZWrIwyvdPsE= github.com/pashagolub/pgxmock/v2 v2.6.0/go.mod h1:FsT+LxxrLNqeRWHzk2SBrSW+5m+kXLcKoVZxigHVHeI= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 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= @@ -166,6 +237,8 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 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/samber/slog-gin v1.7.1 h1:GSCy0hYdM75h3Wa8qtLHcj1e4ozb/ygmAY2G/PxaWpo= +github.com/samber/slog-gin v1.7.1/go.mod h1:rOS5GQQd/Dq4tTczgvdnqfATXk0ReEoVu5mpdMGMBrY= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= @@ -182,18 +255,35 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf 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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M= +github.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo= +github.com/swaggo/swag v1.16.2 h1:28Pp+8DkQoV+HLzLx8RGJZXNGKbFqnuvSbAAtoxiY04= +github.com/swaggo/swag v1.16.2/go.mod h1:6YzXnDcpr0767iOejs318CwYkCQqyGer6BizOg03f+E= +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/urfave/cli/v2 v2.27.0 h1:uNs1K8JwTFL84X68j5Fjny6hfANh9nTlJ6dRtZAFAHY= github.com/urfave/cli/v2 v2.27.0/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -207,6 +297,9 @@ go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9E go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -231,8 +324,10 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/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-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= 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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -250,10 +345,13 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/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-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -285,15 +383,24 @@ golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 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-20200227125254-8fa46927fb4f/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/http/error.go b/http/error.go new file mode 100644 index 0000000..35eadb9 --- /dev/null +++ b/http/error.go @@ -0,0 +1,81 @@ +package http + +import ( + "log/slog" + "net/http" + + "github.com/jellydator/validation" + + "github.com/allisson/psqlqueue/domain" +) + +type errorResponseCode int //@name ErrorResponseCode + +const ( + internalServerErrorCode errorResponseCode = iota + 1 + malformedRequest + requestValidationFailedCode + queueAlreadyExists + queueNotFound + messageNotFound +) + +var errorResponses = map[string]errorResponse{ + "internal_server_error": { + Code: internalServerErrorCode, + Message: "internal server error", + StatusCode: http.StatusInternalServerError, + }, + "malformed_request": { + Code: malformedRequest, + Message: "malformed request body", + StatusCode: http.StatusBadRequest, + }, + "request_validation_failed": { + Code: requestValidationFailedCode, + Message: "request validation failed", + StatusCode: http.StatusBadRequest, + }, + "queue_already_exists": { + Code: queueAlreadyExists, + Message: "queue already exists", + StatusCode: http.StatusBadRequest, + }, + "queue_not_found": { + Code: queueNotFound, + Message: "queue not found", + StatusCode: http.StatusNotFound, + }, + "message_not_found": { + Code: messageNotFound, + Message: "message not found", + StatusCode: http.StatusNotFound, + }, +} + +type errorResponse struct { + Code errorResponseCode `json:"code"` + Message string `json:"message"` + Details string `json:"details,omitempty"` + StatusCode int `json:"-"` +} //@name ErrorResponse + +func parseServiceError(serviceName, serviceMethod string, err error) errorResponse { + if _, ok := err.(validation.Errors); ok { + er := errorResponses["request_validation_failed"] + er.Details = err.Error() + return er + } + + switch err { + case domain.ErrQueueAlreadyExists: + return errorResponses["queue_already_exists"] + case domain.ErrQueueNotFound: + return errorResponses["queue_not_found"] + case domain.ErrMessageNotFound: + return errorResponses["message_not_found"] + default: + slog.Error(serviceName, "method", serviceMethod, "error", err.Error()) + return errorResponses["internal_server_error"] + } +} diff --git a/http/message.go b/http/message.go new file mode 100644 index 0000000..241662f --- /dev/null +++ b/http/message.go @@ -0,0 +1,184 @@ +package http + +import ( + "log/slog" + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "github.com/allisson/psqlqueue/domain" +) + +// nolint:unused +type messageRequest struct { + Body string `json:"body" validate:"required"` + Label *string `json:"label" validate:"optional"` + Attributes map[string]string `json:"attributes" validate:"optional"` +} //@name MessageRequest + +// nolint:unused +type messageResponse struct { + ID string `json:"id" example:"7b98fe50-affd-4685-bd7d-3ae5e41493af"` + QueueID string `json:"queue_id" example:"my-new-queue"` + Label *string `json:"label"` + Body string `json:"body"` + Attributes map[string]string `json:"attributes"` + DeliveryAttempts int `json:"delivery_attempts" example:"1"` + CreatedAt time.Time `json:"created_at" db:"created_at" example:"2023-08-17T00:00:00Z"` +} //@name MessageResponse + +// nolint:unused +type messageListRequest struct { + Label *string `form:"label" validate:"optional"` + Offset uint `form:"offset" validate:"required"` + Limit uint `form:"limit" validate:"required"` +} //@name MessageListRequest + +// nolint:unused +type messageListResponse struct { + Data []*messageResponse `json:"data"` + Limit int `json:"limit" example:"10"` +} //@name MessageListResponse + +// nolint:unused +type messageNackRequest struct { + VisibilityTimeoutSeconds uint `form:"visibility_timeout_seconds" validate:"required"` +} //@name MessageNackRequest + +// Message exposes a REST API for domain.MessageService. +type MessageHandler struct { + messageService domain.MessageService + cfg *domain.Config +} + +// Create a message. +// +// @Summary Add a message +// @Tags messages +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Param request body messageRequest true "Add a message" +// @Success 204 "No Content" +// @Failure 400 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queue/{queue_id}/messages [post] +func (m *MessageHandler) Create(c *gin.Context) { + message := domain.Message{} + + if err := c.ShouldBindJSON(&message); err != nil { + slog.Error("malformed request", "error", err.Error()) + er := errorResponses["malformed_request"] + c.JSON(er.StatusCode, &er) + return + } + + message.QueueID = c.Param("queue_id") + + if err := m.messageService.Create(c.Request.Context(), &message); err != nil { + er := parseServiceError("messageService", "Create", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// List messages. +// +// @Summary List messages +// @Tags messages +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Param label path string false "Label" +// @Param limit query int false "The limit indicates the maximum number of items to return" +// @Success 200 {object} messageListResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/messages [get] +func (m *MessageHandler) List(c *gin.Context) { + queueID := c.Param("queue_id") + + request := messageListRequest{Offset: 0, Limit: 10} + if err := c.ShouldBindQuery(&request); err != nil { + slog.Warn("message list request error", "error", err) + } + + request.Limit = min(request.Limit, m.cfg.QueueMaxNumberOfMessages) + request.Offset = 0 + + messages, err := m.messageService.List(c.Request.Context(), queueID, request.Label, request.Limit) + if err != nil { + er := parseServiceError("messageService", "List", err) + c.JSON(er.StatusCode, &er) + return + } + + response := listResponse{Data: messages, Offset: request.Offset, Limit: request.Limit} + + c.JSON(http.StatusOK, response) +} + +// Ack a message. +// +// @Summary Ack a message +// @Tags messages +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Param message_id path string true "Message id" +// @Success 204 "No Content" +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/messages/{message_id}/ack [put] +func (m *MessageHandler) Ack(c *gin.Context) { + messageID := c.Param("message_id") + + if err := m.messageService.Ack(c.Request.Context(), messageID); err != nil { + er := parseServiceError("messageService", "Ack", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// Nack a message. +// +// @Summary Nack a message +// @Tags messages +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Param message_id path string true "Message id" +// @Param request body messageNackRequest true "Nack a message" +// @Success 204 "No Content" +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/messages/{message_id}/nack [put] +func (m *MessageHandler) Nack(c *gin.Context) { + messageID := c.Param("message_id") + + request := messageNackRequest{} + if err := c.ShouldBindQuery(&request); err != nil { + slog.Warn("message nack request error", "error", err) + } + + if err := m.messageService.Nack(c.Request.Context(), messageID, request.VisibilityTimeoutSeconds); err != nil { + er := parseServiceError("messageService", "Ack", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// NewMessageHandler returns a new MessageHandler. +func NewMessageHandler(messageService domain.MessageService) *MessageHandler { + return &MessageHandler{ + messageService: messageService, + cfg: domain.NewConfig(), + } +} diff --git a/http/message_test.go b/http/message_test.go new file mode 100644 index 0000000..183a142 --- /dev/null +++ b/http/message_test.go @@ -0,0 +1,98 @@ +package http + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/allisson/psqlqueue/domain" +) + +func nilString() *string { + var s *string = nil + return s +} + +func TestMessage(t *testing.T) { + t.Run("Create with invalid request", func(t *testing.T) { + expectedPayload := `{"code":2,"message":"malformed request body"}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues/my-queue/messages", bytes.NewBuffer([]byte(`{`))) + + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Create with validation error", func(t *testing.T) { + expectedPayload := `{"code":3,"message":"request validation failed","details":"body: cannot be blank."}` + message := domain.Message{QueueID: "my-queue"} + jsonMessage, _ := json.Marshal(&message) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues/my-queue/messages", bytes.NewBuffer(jsonMessage)) + + tc.messageService.On("Create", mock.Anything, &message).Return(message.Validate()) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Create", func(t *testing.T) { + message := domain.Message{QueueID: "my-queue", Body: `{"message": true}`} + jsonMessage, _ := json.Marshal(&message) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues/my-queue/messages", bytes.NewBuffer(jsonMessage)) + + tc.messageService.On("Create", mock.Anything, &message).Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) + + t.Run("List", func(t *testing.T) { + expectedPayload := `{"data":[{"id":"","queue_id":"my-queue","label":null,"body":"{\"message\": true}","attributes":null,"delivery_attempts":0,"created_at":"0001-01-01T00:00:00Z"},{"id":"","queue_id":"my-queue","label":null,"body":"{\"message\": true}","attributes":null,"delivery_attempts":0,"created_at":"0001-01-01T00:00:00Z"}],"limit":10}` + message1 := domain.Message{QueueID: "my-queue", Body: `{"message": true}`} + message2 := domain.Message{QueueID: "my-queue", Body: `{"message": true}`} + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/queues/my-queue/messages", nil) + + tc.messageService.On("List", mock.Anything, "my-queue", nilString(), uint(10)).Return([]*domain.Message{&message1, &message2}, nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusOK, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Ack", func(t *testing.T) { + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue/messages/message-id/ack", nil) + + tc.messageService.On("Ack", mock.Anything, "message-id").Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) + + t.Run("Nack", func(t *testing.T) { + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue/messages/message-id/nack", bytes.NewBuffer([]byte(`{"visibility_timeout_seconds": 0}`))) + + tc.messageService.On("Nack", mock.Anything, "message-id", uint(0)).Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) +} diff --git a/http/queue.go b/http/queue.go new file mode 100644 index 0000000..859d3a2 --- /dev/null +++ b/http/queue.go @@ -0,0 +1,266 @@ +package http + +import ( + "log/slog" + "net/http" + "time" + + "github.com/gin-gonic/gin" + + "github.com/allisson/psqlqueue/domain" +) + +// nolint:unused +type queueRequest struct { + ID string `json:"id" example:"my-new-queue" validate:"required"` + AckDeadlineSeconds int `json:"ack_deadline_seconds" example:"30" validate:"required"` + MessageRetentionSeconds int `json:"message_retention_seconds" example:"604800" validate:"required"` + DeliveryDelaySeconds int `json:"delivery_delay_seconds" example:"0" validate:"required"` +} //@name QueueRequest + +// nolint:unused +type queueUpdateRequest struct { + AckDeadlineSeconds int `json:"ack_deadline_seconds" example:"30" validate:"required"` + MessageRetentionSeconds int `json:"message_retention_seconds" example:"604800" validate:"required"` + DeliveryDelaySeconds int `json:"delivery_delay_seconds" example:"0" validate:"required"` +} //@name QueueUpdateRequest + +// nolint:unused +type queueResponse struct { + ID string `json:"id" example:"my-new-queue"` + AckDeadlineSeconds int `json:"ack_deadline_seconds" example:"30"` + MessageRetentionSeconds int `json:"message_retention_seconds" example:"604800"` + DeliveryDelaySeconds int `json:"delivery_delay_seconds" example:"0"` + CreatedAt time.Time `json:"created_at" example:"2023-08-17T00:00:00Z"` + UpdatedAt time.Time `json:"updated_at" example:"2023-08-17T00:00:00Z"` +} //@name QueueResponse + +// nolint:unused +type queueListResponse struct { + Data []*queueResponse `json:"data"` + Offset int `json:"offset" example:"0"` + Limit int `json:"limit" example:"10"` +} //@name QueueListResponse + +// nolint:unused +type queueStatsResponse struct { + NumUndeliveredMessages int `json:"num_undelivered_messages" example:"1"` + OldestUnackedMessageAgeSeconds int `json:"oldest_unacked_message_age_seconds" example:"1"` +} //@name QueueStatsResponse + +// Queue exposes a REST API for domain.QueueService. +type QueueHandler struct { + queueService domain.QueueService +} + +// Create a queue. +// +// @Summary Add a queue +// @Tags queues +// @Accept json +// @Produce json +// @Param request body queueRequest true "Add a queue" +// @Success 201 {object} queueResponse +// @Failure 400 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues [post] +func (q *QueueHandler) Create(c *gin.Context) { + queue := domain.Queue{} + + if err := c.ShouldBindJSON(&queue); err != nil { + slog.Error("malformed request", "error", err.Error()) + er := errorResponses["malformed_request"] + c.JSON(er.StatusCode, &er) + return + } + + if err := q.queueService.Create(c.Request.Context(), &queue); err != nil { + er := parseServiceError("queueService", "Create", err) + c.JSON(er.StatusCode, &er) + return + } + + c.JSON(http.StatusCreated, &queue) +} + +// Update a queue. +// +// @Summary Update a queue +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Param request body queueUpdateRequest true "Update a queue" +// @Success 200 {object} queueResponse +// @Failure 400 {object} errorResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id} [put] +func (q *QueueHandler) Update(c *gin.Context) { + queue := domain.Queue{} + + if err := c.ShouldBindJSON(&queue); err != nil { + slog.Error("malformed request", "error", err.Error()) + er := errorResponses["malformed_request"] + c.JSON(er.StatusCode, &er) + return + } + + queue.ID = c.Param("queue_id") + + if err := q.queueService.Update(c.Request.Context(), &queue); err != nil { + er := parseServiceError("queueService", "Update", err) + c.JSON(er.StatusCode, &er) + return + } + + c.JSON(http.StatusOK, &queue) +} + +// Get a queue. +// +// @Summary Show a queue +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Success 200 {object} queueResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id} [get] +func (q *QueueHandler) Get(c *gin.Context) { + id := c.Param("queue_id") + + queue, err := q.queueService.Get(c.Request.Context(), id) + if err != nil { + er := parseServiceError("queueService", "Get", err) + c.JSON(er.StatusCode, &er) + return + } + + c.JSON(http.StatusOK, &queue) +} + +// List queues. +// +// @Summary List queues +// @Tags queues +// @Accept json +// @Produce json +// @Param limit query int false "The limit indicates the maximum number of items to return" +// @Param offset query int false "The offset indicates the starting position of the query in relation to the complete set of unpaginated items" +// @Success 200 {object} queueListResponse +// @Failure 500 {object} errorResponse +// @Router /queues [get] +func (q *QueueHandler) List(c *gin.Context) { + request := newListRequestFromGIN(c) + + queues, err := q.queueService.List(c.Request.Context(), int(request.Offset), int(request.Limit)) + if err != nil { + er := parseServiceError("queueService", "List", err) + c.JSON(er.StatusCode, &er) + return + } + + response := listResponse{Data: queues, Offset: request.Offset, Limit: request.Limit} + + c.JSON(http.StatusOK, response) +} + +// Delete a queue. +// +// @Summary Delete a queue +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Success 204 "No Content" +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id} [delete] +func (q *QueueHandler) Delete(c *gin.Context) { + id := c.Param("queue_id") + + if err := q.queueService.Delete(c.Request.Context(), id); err != nil { + er := parseServiceError("queueService", "Delete", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// Get the queue stats. +// +// @Summary Get the queue stats +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Success 200 {object} queueStatsResponse +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/stats [get] +func (q *QueueHandler) Stats(c *gin.Context) { + id := c.Param("queue_id") + + stats, err := q.queueService.Stats(c.Request.Context(), id) + if err != nil { + er := parseServiceError("queueService", "Stats", err) + c.JSON(er.StatusCode, &er) + return + } + + c.JSON(http.StatusOK, &stats) +} + +// Purge a queue. +// +// @Summary Purge a queue +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Success 204 "No Content" +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/purge [put] +func (q *QueueHandler) Purge(c *gin.Context) { + id := c.Param("queue_id") + + if err := q.queueService.Purge(c.Request.Context(), id); err != nil { + er := parseServiceError("queueService", "Purge", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// Cleanup a queue. +// +// @Summary Cleanup a queue removing expired and acked messages +// @Tags queues +// @Accept json +// @Produce json +// @Param queue_id path string true "Queue id" +// @Success 204 "No Content" +// @Failure 404 {object} errorResponse +// @Failure 500 {object} errorResponse +// @Router /queues/{queue_id}/cleanup [put] +func (q *QueueHandler) Cleanup(c *gin.Context) { + id := c.Param("queue_id") + + if err := q.queueService.Cleanup(c.Request.Context(), id); err != nil { + er := parseServiceError("queueService", "Cleanup", err) + c.JSON(er.StatusCode, &er) + return + } + + c.Status(http.StatusNoContent) +} + +// NewQueueHandler returns a new QueueHandler. +func NewQueueHandler(queueService domain.QueueService) *QueueHandler { + return &QueueHandler{queueService: queueService} +} diff --git a/http/queue_test.go b/http/queue_test.go new file mode 100644 index 0000000..c9b681b --- /dev/null +++ b/http/queue_test.go @@ -0,0 +1,227 @@ +package http + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/allisson/psqlqueue/domain" + "github.com/allisson/psqlqueue/mocks" +) + +type testContext struct { + queueService *mocks.QueueService + queueHandler *QueueHandler + messageService *mocks.MessageService + messageHandler *MessageHandler + router *gin.Engine +} + +func makeTestContext(t *testing.T) *testContext { + logger := domain.NewLogger("debug") + queueService := mocks.NewQueueService(t) + queueHandler := NewQueueHandler(queueService) + messageService := mocks.NewMessageService(t) + messageHandler := NewMessageHandler(messageService) + router := SetupRouter(logger, queueHandler, messageHandler) + return &testContext{ + queueService: queueService, + queueHandler: queueHandler, + messageService: messageService, + messageHandler: messageHandler, + router: router, + } +} + +func TestQueue(t *testing.T) { + t.Run("Create with invalid request", func(t *testing.T) { + expectedPayload := `{"code":2,"message":"malformed request body"}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues", bytes.NewBuffer([]byte(`{`))) + + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Create with validation error", func(t *testing.T) { + expectedPayload := `{"code":3,"message":"request validation failed","details":"ack_deadline_seconds: cannot be blank; id: must be in a valid format; message_retention_seconds: cannot be blank."}` + queue := domain.Queue{ID: "my@queue"} + jsonQueue, _ := json.Marshal(&queue) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues", bytes.NewBuffer(jsonQueue)) + + tc.queueService.On("Create", mock.Anything, &queue).Return(queue.Validate()) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Create", func(t *testing.T) { + expectedPayload := `{"id":"my-queue","ack_deadline_seconds":0,"message_retention_seconds":0,"delivery_delay_seconds":0,"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}` + queue := domain.Queue{ID: "my-queue"} + jsonQueue, _ := json.Marshal(&queue) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/v1/queues", bytes.NewBuffer(jsonQueue)) + + tc.queueService.On("Create", mock.Anything, &queue).Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusCreated, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Get with object not found", func(t *testing.T) { + expectedPayload := `{"code":5,"message":"queue not found"}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/queues/my-queue", nil) + + tc.queueService.On("Get", mock.Anything, "my-queue").Return(nil, domain.ErrQueueNotFound) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNotFound, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Get", func(t *testing.T) { + expectedPayload := `{"id":"my-queue","ack_deadline_seconds":0,"message_retention_seconds":0,"delivery_delay_seconds":0,"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}` + queue := domain.Queue{ID: "my-queue"} + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/queues/my-queue", nil) + + tc.queueService.On("Get", mock.Anything, queue.ID).Return(&queue, nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusOK, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Update with invalid request", func(t *testing.T) { + expectedPayload := `{"code":2,"message":"malformed request body"}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue", bytes.NewBuffer([]byte(`{`))) + + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Update with validation error", func(t *testing.T) { + expectedPayload := `{"code":3,"message":"request validation failed","details":"ack_deadline_seconds: cannot be blank; message_retention_seconds: cannot be blank."}` + queue := domain.Queue{ID: "my-queue"} + jsonQueue, _ := json.Marshal(&queue) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue", bytes.NewBuffer(jsonQueue)) + + tc.queueService.On("Update", mock.Anything, &queue).Return(queue.Validate()) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusBadRequest, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Update", func(t *testing.T) { + expectedPayload := `{"id":"my-queue","ack_deadline_seconds":0,"message_retention_seconds":0,"delivery_delay_seconds":0,"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}` + queue := domain.Queue{ID: "my-queue"} + jsonQueue, _ := json.Marshal(&queue) + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue", bytes.NewBuffer(jsonQueue)) + + tc.queueService.On("Update", mock.Anything, &queue).Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusOK, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("List", func(t *testing.T) { + expectedPayload := `{"data":[{"id":"my-queue-1","ack_deadline_seconds":0,"message_retention_seconds":0,"delivery_delay_seconds":0,"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"},{"id":"my-queue-2","ack_deadline_seconds":0,"message_retention_seconds":0,"delivery_delay_seconds":0,"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z"}],"limit":1}` + queue1 := domain.Queue{ID: "my-queue-1"} + queue2 := domain.Queue{ID: "my-queue-2"} + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/queues", nil) + + tc.queueService.On("List", mock.Anything, 0, 1).Return([]*domain.Queue{&queue1, &queue2}, nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusOK, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Delete with object not found", func(t *testing.T) { + expectedPayload := `{"code":5,"message":"queue not found"}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/v1/queues/my-queue", nil) + + tc.queueService.On("Delete", mock.Anything, "my-queue").Return(domain.ErrQueueNotFound) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNotFound, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Delete", func(t *testing.T) { + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/v1/queues/my-queue", nil) + + tc.queueService.On("Delete", mock.Anything, "my-queue").Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) + + t.Run("Stats", func(t *testing.T) { + expectedPayload := `{"num_undelivered_messages":0,"oldest_unacked_message_age_seconds":0}` + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/v1/queues/my-queue/stats", nil) + + tc.queueService.On("Stats", mock.Anything, "my-queue").Return(&domain.QueueStats{}, nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusOK, reqRec.Code) + assert.Equal(t, expectedPayload, reqRec.Body.String()) + }) + + t.Run("Purge", func(t *testing.T) { + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue/purge", nil) + + tc.queueService.On("Purge", mock.Anything, "my-queue").Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) + + t.Run("Cleanup", func(t *testing.T) { + tc := makeTestContext(t) + reqRec := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/v1/queues/my-queue/cleanup", nil) + + tc.queueService.On("Cleanup", mock.Anything, "my-queue").Return(nil) + tc.router.ServeHTTP(reqRec, req) + + assert.Equal(t, http.StatusNoContent, reqRec.Code) + }) +} diff --git a/http/request.go b/http/request.go new file mode 100644 index 0000000..74c3445 --- /dev/null +++ b/http/request.go @@ -0,0 +1,20 @@ +package http + +import ( + "log/slog" + + "github.com/gin-gonic/gin" +) + +type listRequest struct { + Offset uint `form:"offset"` + Limit uint `form:"limit"` +} + +func newListRequestFromGIN(c *gin.Context) listRequest { + request := listRequest{Offset: 0, Limit: 1} + if err := c.ShouldBindQuery(&request); err != nil { + slog.Warn("list request error", "error", err) + } + return request +} diff --git a/http/response.go b/http/response.go new file mode 100644 index 0000000..1676c9e --- /dev/null +++ b/http/response.go @@ -0,0 +1,7 @@ +package http + +type listResponse struct { + Data interface{} `json:"data"` + Offset uint `json:"offset,omitempty"` + Limit uint `json:"limit"` +} //@name ListResponse diff --git a/http/setup.go b/http/setup.go new file mode 100644 index 0000000..7590fe8 --- /dev/null +++ b/http/setup.go @@ -0,0 +1,97 @@ +package http + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" + sloggin "github.com/samber/slog-gin" + swaggerfiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" + + docs "github.com/allisson/psqlqueue/docs" + "github.com/allisson/psqlqueue/domain" +) + +func SetupRouter(logger *slog.Logger, queueHandler *QueueHandler, messageHandler *MessageHandler) *gin.Engine { + // router setup + gin.SetMode(gin.ReleaseMode) + router := gin.New() + router.Use(sloggin.New(logger), gin.Recovery()) + + // swagger config + docs.SwaggerInfo.Title = "PSQL Queue API" + docs.SwaggerInfo.BasePath = "/v1" + + // v1 group setup + v1 := router.Group("/v1") + + // swagger + v1.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler)) + + // queue handler + v1.POST("/queues", queueHandler.Create) + v1.PUT("/queues/:queue_id", queueHandler.Update) + v1.GET("/queues/:queue_id", queueHandler.Get) + v1.GET("/queues", queueHandler.List) + v1.DELETE("/queues/:queue_id", queueHandler.Delete) + v1.GET("/queues/:queue_id/stats", queueHandler.Stats) + v1.PUT("/queues/:queue_id/purge", queueHandler.Purge) + v1.PUT("/queues/:queue_id/cleanup", queueHandler.Cleanup) + + // message handler + v1.POST("/queues/:queue_id/messages", messageHandler.Create) + v1.GET("/queues/:queue_id/messages", messageHandler.List) + v1.PUT("/queues/:queue_id/messages/:message_id/ack", messageHandler.Ack) + v1.PUT("/queues/:queue_id/messages/:message_id/nack", messageHandler.Nack) + + return router +} + +// RunServer runs an HTTP server based on config and router. +func RunServer(ctx context.Context, cfg *domain.Config, router *gin.Engine) { + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM) + defer stop() + + srv := &http.Server{ + Addr: fmt.Sprintf("%s:%d", cfg.ServerHost, cfg.ServerPort), + Handler: router, + ReadHeaderTimeout: time.Second * time.Duration(cfg.ServerReadHeaderTimeoutSeconds), + } + + // Initializing the server in a goroutine so that + // it won't block the graceful shutdown handling below + go func() { + slog.Info("http server starting", "host", cfg.ServerHost, "port", cfg.ServerPort) + + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + slog.Error("http server listen error", err) + os.Exit(1) + } + }() + + // Listen for the interrupt signal. + <-ctx.Done() + + // Restore default behavior on the interrupt signal and notify user of shutdown. + stop() + slog.Info("http server shutting down gracefully, press Ctrl+C again to force") + + // The context is used to inform the server it has 5 seconds to finish + // the request it is currently handling + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + slog.Error("http server forced to shutdown: ", err) + os.Exit(1) + } + + slog.Info("http server exiting") +} diff --git a/mocks/MessageService.go b/mocks/MessageService.go index be7cc78..c81705b 100644 --- a/mocks/MessageService.go +++ b/mocks/MessageService.go @@ -51,7 +51,7 @@ func (_m *MessageService) Create(ctx context.Context, message *domain.Message) e } // List provides a mock function with given fields: ctx, queueID, label, limit -func (_m *MessageService) List(ctx context.Context, queueID *string, label *string, limit uint) ([]*domain.Message, error) { +func (_m *MessageService) List(ctx context.Context, queueID string, label *string, limit uint) ([]*domain.Message, error) { ret := _m.Called(ctx, queueID, label, limit) if len(ret) == 0 { @@ -60,10 +60,10 @@ func (_m *MessageService) List(ctx context.Context, queueID *string, label *stri var r0 []*domain.Message var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *string, *string, uint) ([]*domain.Message, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, *string, uint) ([]*domain.Message, error)); ok { return rf(ctx, queueID, label, limit) } - if rf, ok := ret.Get(0).(func(context.Context, *string, *string, uint) []*domain.Message); ok { + if rf, ok := ret.Get(0).(func(context.Context, string, *string, uint) []*domain.Message); ok { r0 = rf(ctx, queueID, label, limit) } else { if ret.Get(0) != nil { @@ -71,7 +71,7 @@ func (_m *MessageService) List(ctx context.Context, queueID *string, label *stri } } - if rf, ok := ret.Get(1).(func(context.Context, *string, *string, uint) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, string, *string, uint) error); ok { r1 = rf(ctx, queueID, label, limit) } else { r1 = ret.Error(1)