From 96ef1ee471336132018773f687e2baec4f284fbc Mon Sep 17 00:00:00 2001 From: Sergey <38931148+SShlykov@users.noreply.github.com> Date: Sun, 3 Mar 2024 23:56:57 +0300 Subject: [PATCH] Bookback (#14) * feat: book events functions * feat: added methrics and refactored controllers --- bookback/docs/docs.go | 816 ------------------ bookback/docs/docs.md | 1 - bookback/docs/swagger.json | 795 ----------------- bookback/docs/swagger.yaml | 524 ----------- bookback/internal/metrics/interface.go | 6 + .../internal/metrics/localmetrics/metrics.go | 61 ++ bookback/internal/metrics/metrics.go | 1 - bookback/internal/models/mapvariables.go | 10 +- bookback/internal/pkg/app/app.go | 3 + bookback/internal/pkg/app/endpoint.go | 104 +-- bookback/internal/pkg/app/metrics.go | 14 + .../http}/circuitbreaker/circuitbreaker.go | 0 .../circuitbreaker/circuitbreaker_test.go | 0 .../servers/http/controllers/book/config.go | 6 + .../http/controllers/book/controller.go | 112 +-- .../servers/http/controllers/book/errors.go | 31 + .../servers/http/controllers/book/models.go | 21 + .../servers/http/controllers/book/routes.go | 31 + .../http/controllers/bookevents/config.go | 3 + .../http/controllers/bookevents/controller.go | 142 +-- .../http/controllers/bookevents/errors.go | 31 + .../http/controllers/bookevents/models.go | 20 + .../http/controllers/bookevents/routes.go | 33 + .../http/controllers/chapter/config.go | 3 + .../http/controllers/chapter/controller.go | 102 ++- .../http/controllers/chapter/errors.go | 31 + .../http/controllers/chapter/models.go | 21 + .../http/controllers/chapter/routes.go | 33 + .../servers/http/controllers/health/config.go | 3 + .../http/controllers/health/controller.go | 24 +- .../servers/http/controllers/health/routes.go | 23 + .../http/controllers/mapvariables/config.go | 3 + .../controllers/mapvariables/controller.go | 240 ++---- .../http/controllers/mapvariables/errors.go | 29 + .../http/controllers/mapvariables/models.go | 20 + .../http/controllers/mapvariables/routes.go | 32 + .../servers/http/controllers/page/config.go | 3 + .../http/controllers/page/controller.go | 107 ++- .../servers/http/controllers/page/errors.go | 31 + .../servers/http/controllers/page/models.go | 21 + .../servers/http/controllers/page/routes.go | 30 + .../http/controllers/paragraph/config.go | 3 + .../http/controllers/paragraph/controller.go | 109 ++- .../http/controllers/paragraph/errors.go | 31 + .../http/controllers/paragraph/models.go | 21 + .../http/controllers/paragraph/routes.go | 30 + .../http/controllers/swagger/controller.go | 12 + .../http/httpmiddlewares/circuitbreaker.go | 28 + .../servers/http/httpmiddlewares/cors.go | 28 + .../http/httpmiddlewares/httplogger.go | 32 + .../http/httpmiddlewares/middleware.go | 29 + .../internal/servers/http/router/router.go | 83 -- bookback/internal/servers/http/server.go | 5 - .../internal/services/book/repository_test.go | 16 +- .../services/chapter/repository_test.go | 16 +- .../services/mapvariables/repository.go | 104 +-- .../internal/services/mapvariables/service.go | 21 - .../internal/services/mapvariables/utils.go | 3 +- .../internal/services/page/repository_test.go | 16 +- .../services/paragraph/repository_test.go | 16 +- .../20240225152254_create_map_variables.sql | 4 +- ...75249_update_map_events_add_updated_at.sql | 9 + bookback/pkg/logger/handler.go | 11 +- bookback/{internal => tests}/mocks/db.go | 0 .../{internal => tests}/mocks/scanresult.go | 0 65 files changed, 1298 insertions(+), 2850 deletions(-) delete mode 100644 bookback/docs/docs.go delete mode 100644 bookback/docs/docs.md delete mode 100644 bookback/docs/swagger.json delete mode 100644 bookback/docs/swagger.yaml create mode 100644 bookback/internal/metrics/interface.go create mode 100644 bookback/internal/metrics/localmetrics/metrics.go delete mode 100644 bookback/internal/metrics/metrics.go create mode 100644 bookback/internal/pkg/app/metrics.go rename bookback/internal/{ => servers/http}/circuitbreaker/circuitbreaker.go (100%) rename bookback/internal/{ => servers/http}/circuitbreaker/circuitbreaker_test.go (100%) create mode 100644 bookback/internal/servers/http/controllers/book/config.go create mode 100644 bookback/internal/servers/http/controllers/book/errors.go create mode 100644 bookback/internal/servers/http/controllers/book/models.go create mode 100644 bookback/internal/servers/http/controllers/book/routes.go create mode 100644 bookback/internal/servers/http/controllers/bookevents/config.go create mode 100644 bookback/internal/servers/http/controllers/bookevents/errors.go create mode 100644 bookback/internal/servers/http/controllers/bookevents/models.go create mode 100644 bookback/internal/servers/http/controllers/bookevents/routes.go create mode 100644 bookback/internal/servers/http/controllers/chapter/config.go create mode 100644 bookback/internal/servers/http/controllers/chapter/errors.go create mode 100644 bookback/internal/servers/http/controllers/chapter/models.go create mode 100644 bookback/internal/servers/http/controllers/chapter/routes.go create mode 100644 bookback/internal/servers/http/controllers/health/config.go create mode 100644 bookback/internal/servers/http/controllers/health/routes.go create mode 100644 bookback/internal/servers/http/controllers/mapvariables/config.go create mode 100644 bookback/internal/servers/http/controllers/mapvariables/errors.go create mode 100644 bookback/internal/servers/http/controllers/mapvariables/models.go create mode 100644 bookback/internal/servers/http/controllers/mapvariables/routes.go create mode 100644 bookback/internal/servers/http/controllers/page/config.go create mode 100644 bookback/internal/servers/http/controllers/page/errors.go create mode 100644 bookback/internal/servers/http/controllers/page/models.go create mode 100644 bookback/internal/servers/http/controllers/page/routes.go create mode 100644 bookback/internal/servers/http/controllers/paragraph/config.go create mode 100644 bookback/internal/servers/http/controllers/paragraph/errors.go create mode 100644 bookback/internal/servers/http/controllers/paragraph/models.go create mode 100644 bookback/internal/servers/http/controllers/paragraph/routes.go create mode 100644 bookback/internal/servers/http/controllers/swagger/controller.go create mode 100644 bookback/internal/servers/http/httpmiddlewares/circuitbreaker.go create mode 100644 bookback/internal/servers/http/httpmiddlewares/cors.go create mode 100644 bookback/internal/servers/http/httpmiddlewares/httplogger.go create mode 100644 bookback/internal/servers/http/httpmiddlewares/middleware.go delete mode 100644 bookback/internal/servers/http/router/router.go delete mode 100644 bookback/internal/servers/http/server.go create mode 100644 bookback/migrations/20240303075249_update_map_events_add_updated_at.sql rename bookback/{internal => tests}/mocks/db.go (100%) rename bookback/{internal => tests}/mocks/scanresult.go (100%) diff --git a/bookback/docs/docs.go b/bookback/docs/docs.go deleted file mode 100644 index 3d7085b..0000000 --- a/bookback/docs/docs.go +++ /dev/null @@ -1,816 +0,0 @@ -// 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": { - "/books": { - "get": { - "description": "Извлекает список всех книг", - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Получить список книг", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Book" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую книгу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Создать книгу", - "parameters": [ - { - "description": "Book object", - "name": "book", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Book" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/books/{id}": { - "get": { - "description": "Извлекает книгу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Получить книгу по ID", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет книгу по ее ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Обновить книгу", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Book object", - "name": "book", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Book" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет книгу по ее ID", - "tags": [ - "Книги" - ], - "summary": "Удалить книгу", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/chapters": { - "get": { - "description": "Извлекает список всех глав", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Получить список глав", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Chapter" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую главу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Создать главу", - "parameters": [ - { - "description": "Chapter object", - "name": "chapter", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Chapter" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/chapters/{id}": { - "get": { - "description": "Извлекает главу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Получить главу по ID", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет главу по ее ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Обновить главу", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Chapter object", - "name": "chapter", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Chapter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет главу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Удалить главу", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "406": { - "description": "Not Acceptable", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/health": { - "get": { - "description": "Возвращает статус приложения", - "produces": [ - "application/json" - ], - "tags": [ - "Статус приложения" - ], - "summary": "Получить статус приложения", - "responses": { - "200": { - "description": "healthy", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/pages": { - "get": { - "description": "Извлекает список всех страниц", - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Получить список страниц", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Page" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую страницу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Создать страницу", - "parameters": [ - { - "description": "Page object", - "name": "page", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Page" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Page" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/pages/{id}": { - "get": { - "description": "Извлекает страницу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Получить страницу по ID", - "parameters": [ - { - "type": "string", - "description": "ID страницы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Page" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/paragraphs": { - "get": { - "description": "Извлекает список всех параграфов", - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Получить список параграфов", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Paragraph" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новый параграф", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Создать параграф", - "parameters": [ - { - "description": "Paragraph object", - "name": "paragraph", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/paragraphs/{id}": { - "get": { - "description": "Извлекает параграф по его ID", - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Получить параграф по ID", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет параграф по его ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Обновить параграф", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Paragraph object", - "name": "paragraph", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет параграф по его ID", - "tags": [ - "Параграфы" - ], - "summary": "Удалить параграф", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - } - }, - "definitions": { - "config.HTTPError": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "models.Book": { - "type": "object", - "properties": { - "author": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "owner": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Chapter": { - "type": "object", - "properties": { - "book_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "number": { - "type": "integer" - }, - "pages": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Page" - } - }, - "text": { - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Page": { - "type": "object", - "properties": { - "chapter_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "text": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Paragraph": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "page_id": { - "type": "string" - }, - "text": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "0.1", - Host: "localhost:7077", - BasePath: "/api/v1", - Schemes: []string{"http"}, - Title: "Book API", - Description: "Это API для работы с книгами", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/bookback/docs/docs.md b/bookback/docs/docs.md deleted file mode 100644 index 1b6ba71..0000000 --- a/bookback/docs/docs.md +++ /dev/null @@ -1 +0,0 @@ -## \ No newline at end of file diff --git a/bookback/docs/swagger.json b/bookback/docs/swagger.json deleted file mode 100644 index bc84a37..0000000 --- a/bookback/docs/swagger.json +++ /dev/null @@ -1,795 +0,0 @@ -{ - "schemes": [ - "http" - ], - "swagger": "2.0", - "info": { - "description": "Это API для работы с книгами", - "title": "Book API", - "contact": {}, - "version": "0.1" - }, - "host": "localhost:7077", - "basePath": "/api/v1", - "paths": { - "/books": { - "get": { - "description": "Извлекает список всех книг", - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Получить список книг", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Book" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую книгу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Создать книгу", - "parameters": [ - { - "description": "Book object", - "name": "book", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Book" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/books/{id}": { - "get": { - "description": "Извлекает книгу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Получить книгу по ID", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет книгу по ее ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Книги" - ], - "summary": "Обновить книгу", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Book object", - "name": "book", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Book" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Book" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет книгу по ее ID", - "tags": [ - "Книги" - ], - "summary": "Удалить книгу", - "parameters": [ - { - "type": "string", - "description": "Book ID", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "204": { - "description": "No Content" - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/chapters": { - "get": { - "description": "Извлекает список всех глав", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Получить список глав", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Chapter" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую главу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Создать главу", - "parameters": [ - { - "description": "Chapter object", - "name": "chapter", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Chapter" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/chapters/{id}": { - "get": { - "description": "Извлекает главу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Получить главу по ID", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет главу по ее ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Обновить главу", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Chapter object", - "name": "chapter", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Chapter" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет главу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Главы" - ], - "summary": "Удалить главу", - "parameters": [ - { - "type": "string", - "description": "ID главы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Chapter" - } - }, - "406": { - "description": "Not Acceptable", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/health": { - "get": { - "description": "Возвращает статус приложения", - "produces": [ - "application/json" - ], - "tags": [ - "Статус приложения" - ], - "summary": "Получить статус приложения", - "responses": { - "200": { - "description": "healthy", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/pages": { - "get": { - "description": "Извлекает список всех страниц", - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Получить список страниц", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Page" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новую страницу", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Создать страницу", - "parameters": [ - { - "description": "Page object", - "name": "page", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Page" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Page" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/pages/{id}": { - "get": { - "description": "Извлекает страницу по ее ID", - "produces": [ - "application/json" - ], - "tags": [ - "Страницы" - ], - "summary": "Получить страницу по ID", - "parameters": [ - { - "type": "string", - "description": "ID страницы", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Page" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/paragraphs": { - "get": { - "description": "Извлекает список всех параграфов", - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Получить список параграфов", - "responses": { - "200": { - "description": "OK", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Paragraph" - } - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "post": { - "description": "Создает новый параграф", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Создать параграф", - "parameters": [ - { - "description": "Paragraph object", - "name": "paragraph", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - }, - "/paragraphs/{id}": { - "get": { - "description": "Извлекает параграф по его ID", - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Получить параграф по ID", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "put": { - "description": "Обновляет параграф по его ID", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "Параграфы" - ], - "summary": "Обновить параграф", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - }, - { - "description": "Paragraph object", - "name": "paragraph", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - }, - "delete": { - "description": "Удаляет параграф по его ID", - "tags": [ - "Параграфы" - ], - "summary": "Удалить параграф", - "parameters": [ - { - "type": "string", - "description": "ID параграфа", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/models.Paragraph" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/config.HTTPError" - } - } - } - } - } - }, - "definitions": { - "config.HTTPError": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - } - }, - "models.Book": { - "type": "object", - "properties": { - "author": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "owner": { - "type": "integer" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Chapter": { - "type": "object", - "properties": { - "book_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "number": { - "type": "integer" - }, - "pages": { - "type": "array", - "items": { - "$ref": "#/definitions/models.Page" - } - }, - "text": { - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Page": { - "type": "object", - "properties": { - "chapter_id": { - "type": "string" - }, - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "text": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - }, - "models.Paragraph": { - "type": "object", - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "string" - }, - "is_public": { - "type": "boolean" - }, - "page_id": { - "type": "string" - }, - "text": { - "type": "string" - }, - "updated_at": { - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/bookback/docs/swagger.yaml b/bookback/docs/swagger.yaml deleted file mode 100644 index 7809889..0000000 --- a/bookback/docs/swagger.yaml +++ /dev/null @@ -1,524 +0,0 @@ -basePath: /api/v1 -definitions: - config.HTTPError: - properties: - message: - type: string - type: object - models.Book: - properties: - author: - type: string - created_at: - type: string - description: - type: string - id: - type: string - is_public: - type: boolean - owner: - type: integer - title: - type: string - updated_at: - type: string - type: object - models.Chapter: - properties: - book_id: - type: string - created_at: - type: string - id: - type: string - is_public: - type: boolean - number: - type: integer - pages: - items: - $ref: '#/definitions/models.Page' - type: array - text: - type: string - title: - type: string - updated_at: - type: string - type: object - models.Page: - properties: - chapter_id: - type: string - created_at: - type: string - id: - type: string - is_public: - type: boolean - text: - type: string - updated_at: - type: string - type: object - models.Paragraph: - properties: - created_at: - type: string - id: - type: string - is_public: - type: boolean - page_id: - type: string - text: - type: string - updated_at: - type: string - type: object -host: localhost:7077 -info: - contact: {} - description: Это API для работы с книгами - title: Book API - version: "0.1" -paths: - /books: - get: - description: Извлекает список всех книг - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Book' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить список книг - tags: - - Книги - post: - consumes: - - application/json - description: Создает новую книгу - parameters: - - description: Book object - in: body - name: book - required: true - schema: - $ref: '#/definitions/models.Book' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Book' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Создать книгу - tags: - - Книги - /books/{id}: - delete: - description: Удаляет книгу по ее ID - parameters: - - description: Book ID - in: path - name: id - required: true - type: string - responses: - "204": - description: No Content - "404": - description: Not Found - schema: - $ref: '#/definitions/config.HTTPError' - summary: Удалить книгу - tags: - - Книги - get: - description: Извлекает книгу по ее ID - parameters: - - description: Book ID - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Book' - "404": - description: Not Found - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить книгу по ID - tags: - - Книги - put: - consumes: - - application/json - description: Обновляет книгу по ее ID - parameters: - - description: Book ID - in: path - name: id - required: true - type: string - - description: Book object - in: body - name: book - required: true - schema: - $ref: '#/definitions/models.Book' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Book' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Обновить книгу - tags: - - Книги - /chapters: - get: - description: Извлекает список всех глав - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Chapter' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить список глав - tags: - - Главы - post: - consumes: - - application/json - description: Создает новую главу - parameters: - - description: Chapter object - in: body - name: chapter - required: true - schema: - $ref: '#/definitions/models.Chapter' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Chapter' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Создать главу - tags: - - Главы - /chapters/{id}: - delete: - description: Удаляет главу по ее ID - parameters: - - description: ID главы - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Chapter' - "406": - description: Not Acceptable - schema: - $ref: '#/definitions/config.HTTPError' - summary: Удалить главу - tags: - - Главы - get: - description: Извлекает главу по ее ID - parameters: - - description: ID главы - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Chapter' - "404": - description: Not Found - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить главу по ID - tags: - - Главы - put: - consumes: - - application/json - description: Обновляет главу по ее ID - parameters: - - description: ID главы - in: path - name: id - required: true - type: string - - description: Chapter object - in: body - name: chapter - required: true - schema: - $ref: '#/definitions/models.Chapter' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Chapter' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Обновить главу - tags: - - Главы - /health: - get: - description: Возвращает статус приложения - produces: - - application/json - responses: - "200": - description: healthy - schema: - type: string - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить статус приложения - tags: - - Статус приложения - /pages: - get: - description: Извлекает список всех страниц - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Page' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить список страниц - tags: - - Страницы - post: - consumes: - - application/json - description: Создает новую страницу - parameters: - - description: Page object - in: body - name: page - required: true - schema: - $ref: '#/definitions/models.Page' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Page' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Создать страницу - tags: - - Страницы - /pages/{id}: - get: - description: Извлекает страницу по ее ID - parameters: - - description: ID страницы - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Page' - "404": - description: Not Found - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить страницу по ID - tags: - - Страницы - /paragraphs: - get: - description: Извлекает список всех параграфов - produces: - - application/json - responses: - "200": - description: OK - schema: - items: - $ref: '#/definitions/models.Paragraph' - type: array - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить список параграфов - tags: - - Параграфы - post: - consumes: - - application/json - description: Создает новый параграф - parameters: - - description: Paragraph object - in: body - name: paragraph - required: true - schema: - $ref: '#/definitions/models.Paragraph' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/models.Paragraph' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Создать параграф - tags: - - Параграфы - /paragraphs/{id}: - delete: - description: Удаляет параграф по его ID - parameters: - - description: ID параграфа - in: path - name: id - required: true - type: string - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Paragraph' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/config.HTTPError' - summary: Удалить параграф - tags: - - Параграфы - get: - description: Извлекает параграф по его ID - parameters: - - description: ID параграфа - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Paragraph' - "404": - description: Not Found - schema: - $ref: '#/definitions/config.HTTPError' - summary: Получить параграф по ID - tags: - - Параграфы - put: - consumes: - - application/json - description: Обновляет параграф по его ID - parameters: - - description: ID параграфа - in: path - name: id - required: true - type: string - - description: Paragraph object - in: body - name: paragraph - required: true - schema: - $ref: '#/definitions/models.Paragraph' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/models.Paragraph' - "400": - description: Bad Request - schema: - $ref: '#/definitions/config.HTTPError' - summary: Обновить параграф - tags: - - Параграфы -schemes: -- http -swagger: "2.0" diff --git a/bookback/internal/metrics/interface.go b/bookback/internal/metrics/interface.go new file mode 100644 index 0000000..439df74 --- /dev/null +++ b/bookback/internal/metrics/interface.go @@ -0,0 +1,6 @@ +package metrics + +type Metrics interface { + IncCounter(name string, labels ...string) + ObserveHistogram(name string, value float64, labels ...string) +} diff --git a/bookback/internal/metrics/localmetrics/metrics.go b/bookback/internal/metrics/localmetrics/metrics.go new file mode 100644 index 0000000..b981468 --- /dev/null +++ b/bookback/internal/metrics/localmetrics/metrics.go @@ -0,0 +1,61 @@ +package localmetrics + +import ( + "log/slog" + "sync" +) + +type MetricValue struct { + Count int + Sum float64 +} + +// LocalMetrics структура для локального хранения метрик. +type LocalMetrics struct { + logger *slog.Logger + mu sync.RWMutex + counters map[string]int + summaries map[string]MetricValue +} + +func NewLocalMetrics(logger *slog.Logger) *LocalMetrics { + return &LocalMetrics{ + logger: logger, + counters: make(map[string]int), + summaries: make(map[string]MetricValue), + } +} + +func (l *LocalMetrics) IncCounter(name string, labels ...string) { + l.mu.Lock() + defer l.mu.Unlock() + + key := buildMetricKey(name, labels) + if _, exists := l.counters[key]; !exists { + l.counters[key]++ + } else { + l.counters[key] = 1 + } + l.logger.Info("IncCounter", slog.String("name", name)) +} + +func (l *LocalMetrics) ObserveHistogram(name string, value float64, labels ...string) { + l.mu.Lock() + defer l.mu.Unlock() + + key := buildMetricKey(name, labels) + summary, exists := l.summaries[key] + if !exists { + summary = MetricValue{} + } + summary.Count++ + summary.Sum += value + l.summaries[key] = summary + + l.logger.Info("ObserveSummary", slog.String("name", name), slog.Float64("value", value)) +} + +// buildMetricKey генерирует уникальный ключ для метрики на основе имени и меток. +func buildMetricKey(name string, _ []string) string { + return name +} diff --git a/bookback/internal/metrics/metrics.go b/bookback/internal/metrics/metrics.go deleted file mode 100644 index 1abe097..0000000 --- a/bookback/internal/metrics/metrics.go +++ /dev/null @@ -1 +0,0 @@ -package metrics diff --git a/bookback/internal/models/mapvariables.go b/bookback/internal/models/mapvariables.go index a372d5e..ff005e4 100644 --- a/bookback/internal/models/mapvariables.go +++ b/bookback/internal/models/mapvariables.go @@ -5,9 +5,9 @@ import ( ) type MapVariable struct { - ID string `json:"id"` - CreatedAt time.Time `json:"created_at"` - + ID string `json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` BookID string `json:"book_id"` ChapterID NullString `json:"chapter_id"` PageID NullString `json:"page_id"` @@ -15,8 +15,8 @@ type MapVariable struct { MapLink string `json:"map_link"` Lat float64 `json:"lat"` Lng float64 `json:"lng"` - Zoom int `json:"zoom"` - Date time.Time `json:"date"` + Zoom NullInt `json:"zoom"` + Date NullString `json:"date"` Description NullString `json:"description"` Link NullString `json:"link"` LinkText NullString `json:"link_text"` diff --git a/bookback/internal/pkg/app/app.go b/bookback/internal/pkg/app/app.go index 6251423..1370834 100644 --- a/bookback/internal/pkg/app/app.go +++ b/bookback/internal/pkg/app/app.go @@ -3,6 +3,7 @@ package app import ( "context" cfg "github.com/SShlykov/zeitment/bookback/internal/config" + "github.com/SShlykov/zeitment/bookback/internal/metrics" "github.com/SShlykov/zeitment/bookback/pkg/db" "github.com/labstack/echo/v4" "log/slog" @@ -17,6 +18,7 @@ type App struct { config *cfg.Config db db.Client Echo *echo.Echo + metrics metrics.Metrics ctx context.Context closeCtx func() @@ -29,6 +31,7 @@ func NewApp(configPath string) (*App, error) { inits := []func(ctx context.Context) error{ app.initConfig, app.initLogger, + app.initMetrics, app.initDB, app.initEndpoint, app.initRouter, diff --git a/bookback/internal/pkg/app/endpoint.go b/bookback/internal/pkg/app/endpoint.go index cc7bec1..82cf348 100644 --- a/bookback/internal/pkg/app/endpoint.go +++ b/bookback/internal/pkg/app/endpoint.go @@ -2,10 +2,18 @@ package app import ( "context" - "errors" - "fmt" - "github.com/SShlykov/zeitment/bookback/internal/circuitbreaker" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/router" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/circuitbreaker" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/book" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/bookevents" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/chapter" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/health" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/mapvariables" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/page" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/paragraph" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/swagger" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + "github.com/SShlykov/zeitment/bookback/pkg/db" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "log/slog" @@ -14,16 +22,15 @@ import ( ) func (app *App) runWebServer(wg *sync.WaitGroup, _ context.Context) { - wg.Add(1) - go func() { + wg.Add(1) defer wg.Done() httpServer := &http.Server{ ReadHeaderTimeout: app.config.Timeout, ReadTimeout: app.config.Timeout, WriteTimeout: app.config.Timeout, IdleTimeout: app.config.IddleTimeout, - Addr: fmt.Sprintf(app.config.Address), + Addr: app.config.Address, Handler: app.Echo, } @@ -37,13 +44,22 @@ func (app *App) runWebServer(wg *sync.WaitGroup, _ context.Context) { func (app *App) initEndpoint(_ context.Context) error { e := echo.New() - cb := circuitbreaker.NewCircuitBreaker(app.config.RequestLimit, app.config.MinRequests, app.config.ErrorThresholdPercentage, - app.config.IntervalDuration, app.config.OpenStateTimeout) + cb := circuitbreaker.NewCircuitBreaker( + app.config.RequestLimit, + app.config.MinRequests, + app.config.ErrorThresholdPercentage, + app.config.IntervalDuration, + app.config.OpenStateTimeout, + ) middlewares := []echo.MiddlewareFunc{ - loggerConfiguration(app.logger), + httpmiddlewares.LoggerConfiguration(app.logger), middleware.Recover(), - createCircuitBreakerMiddleware(cb), + httpmiddlewares.CreateCircuitBreakerMiddleware(cb), + } + + if app.config.CorsEnabled { + middlewares = append(middlewares, httpmiddlewares.CORS()) } e.Use(middlewares...) @@ -53,59 +69,21 @@ func (app *App) initEndpoint(_ context.Context) error { } func (app *App) initRouter(_ context.Context) error { - router.SetCORSConfig(app.Echo, app.config.CorsEnabled) - router.SetHealthController(app.Echo, app.ctx) - router.SetBookController(app.Echo, app.db, app.ctx) - router.SetChapterController(app.Echo, app.db, app.ctx) - router.SetPageController(app.Echo, app.db, app.ctx) - router.SetParagraphController(app.Echo, app.db, app.ctx) - router.SetBookEventController(app.Echo, app.db, app.ctx) - router.SetMapVariablesController(app.Echo, app.db, app.ctx) - router.SetSwagger(app.Echo, app.config.SwaggerEnabled) - - return nil -} - -func createCircuitBreakerMiddleware(cb *circuitbreaker.CircuitBreaker) echo.MiddlewareFunc { - return func(next echo.HandlerFunc) echo.HandlerFunc { - return func(c echo.Context) error { - err := cb.Execute(func() error { - return next(c) - }) - - if err != nil { - if errors.Is(err, circuitbreaker.ErrorCb) { - return c.JSON(http.StatusUnavailableForLegalReasons, - map[string]string{"error": "Server is overloaded, please try again later.", "status": "error"}) - } - return err - } + controllers := []func(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context){ + health.SetHealthController, + book.SetBookController, + chapter.SetChapterController, + page.SetPageController, + paragraph.SetParagraphController, + bookevents.SetBookEventController, + mapvariables.SetMapVariablesController, + } - return nil - } + for _, controller := range controllers { + controller(app.Echo, app.db, app.metrics, app.logger, app.ctx) } -} -func loggerConfiguration(logger *slog.Logger) echo.MiddlewareFunc { - return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ - LogStatus: true, - LogURI: true, - LogError: true, - HandleError: true, - LogValuesFunc: func(_ echo.Context, v middleware.RequestLoggerValues) error { - if v.Error == nil { - logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - ) - } else { - logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", - slog.String("uri", v.URI), - slog.Int("status", v.Status), - slog.String("err", v.Error.Error()), - ) - } - return nil - }, - }) + swagger.SetSwagger(app.Echo, app.config.SwaggerEnabled) + + return nil } diff --git a/bookback/internal/pkg/app/metrics.go b/bookback/internal/pkg/app/metrics.go new file mode 100644 index 0000000..fe1b679 --- /dev/null +++ b/bookback/internal/pkg/app/metrics.go @@ -0,0 +1,14 @@ +package app + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics/localmetrics" +) + +func (app *App) initMetrics(_ context.Context) error { + logger := app.logger + logger.Info("initializing metrics as local metrics") + + app.metrics = localmetrics.NewLocalMetrics(logger) + return nil +} diff --git a/bookback/internal/circuitbreaker/circuitbreaker.go b/bookback/internal/servers/http/circuitbreaker/circuitbreaker.go similarity index 100% rename from bookback/internal/circuitbreaker/circuitbreaker.go rename to bookback/internal/servers/http/circuitbreaker/circuitbreaker.go diff --git a/bookback/internal/circuitbreaker/circuitbreaker_test.go b/bookback/internal/servers/http/circuitbreaker/circuitbreaker_test.go similarity index 100% rename from bookback/internal/circuitbreaker/circuitbreaker_test.go rename to bookback/internal/servers/http/circuitbreaker/circuitbreaker_test.go diff --git a/bookback/internal/servers/http/controllers/book/config.go b/bookback/internal/servers/http/controllers/book/config.go new file mode 100644 index 0000000..1f4fa44 --- /dev/null +++ b/bookback/internal/servers/http/controllers/book/config.go @@ -0,0 +1,6 @@ +package book + +// Тут будет конфигурация для контроллера книг. +// Права доступа, роли, константы и т.п. + +const PathPrefix = "/api/v1/books" diff --git a/bookback/internal/servers/http/controllers/book/controller.go b/bookback/internal/servers/http/controllers/book/controller.go index 9811850..9ec5232 100644 --- a/bookback/internal/servers/http/controllers/book/controller.go +++ b/bookback/internal/servers/http/controllers/book/controller.go @@ -3,31 +3,25 @@ package book import ( "context" "errors" - "fmt" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" service "github.com/SShlykov/zeitment/bookback/internal/services/book" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) // Controller структура для HTTP-контроллера книг. type Controller struct { Service service.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } // NewController создает новый экземпляр Controller. -func NewController(srv service.Service) *Controller { - return &Controller{Service: srv} -} - -// RegisterRoutes регистрирует маршруты для обработки запросов к книгам. -func (bc *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/books", func(c echo.Context) error { return bc.ListBooks(c, ctx) }) - e.POST("/api/v1/books", func(c echo.Context) error { return bc.CreateBook(c, ctx) }) - e.GET("/api/v1/books/:id", func(c echo.Context) error { return bc.GetBookByID(c, ctx) }) - e.PUT("/api/v1/books/:id", func(c echo.Context) error { return bc.UpdateBook(c, ctx) }) - e.DELETE("/api/v1/books/:id", func(c echo.Context) error { return bc.DeleteBook(c, ctx) }) +func NewController(srv service.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // ListBooks обрабатывает запросы на получение списка книг. @@ -37,14 +31,13 @@ func (bc *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @tags Книги // @produce application/json // @success 200 {array} models.Book -// @failure 500 {object} config.HTTPError -func (bc *Controller) ListBooks(c echo.Context, ctx context.Context) error { - books, err := bc.Service.ListBooks(ctx) +// @failure 500 {object} string +func (bc *Controller) ListBooks(c echo.Context) error { + books, err := bc.Service.ListBooks(bc.Ctx) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) + return ErrorUnknown } - return c.JSON(http.StatusOK, books) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Books: books}) } // CreateBook обрабатывает создание новой книги. @@ -56,18 +49,19 @@ func (bc *Controller) ListBooks(c echo.Context, ctx context.Context) error { // @produce application/json // @param book body models.Book true "Book object" // @success 201 {object} models.Book -// @failure 400 {object} config.HTTPError -func (bc *Controller) CreateBook(c echo.Context, ctx context.Context) error { - var book models.Book - if err := c.Bind(&book); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +// @failure 400 {object} string +// @failure 500 {object} string +func (bc *Controller) CreateBook(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - createdBook, err := bc.Service.CreateBook(ctx, &book) + + createdBook, err := bc.Service.CreateBook(bc.Ctx, request.Book) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusNotAcceptable, config.ErrorNotCreated) + return ErrorBookNotCreated } - return c.JSON(http.StatusCreated, createdBook) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", Book: createdBook}) } // GetBookByID обрабатывает запросы на получение книги по ID. @@ -78,14 +72,21 @@ func (bc *Controller) CreateBook(c echo.Context, ctx context.Context) error { // @param id path string true "Book ID" // @produce application/json // @success 200 {object} models.Book -// @failure 404 {object} config.HTTPError -func (bc *Controller) GetBookByID(c echo.Context, ctx context.Context) error { +// @failure 400 {object} string +// @failure 404 {object} string +// @failure 500 {object} string +func (bc *Controller) GetBookByID(c echo.Context) error { id := c.Param("id") - book, err := bc.Service.GetBookByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + book, err := bc.Service.GetBookByID(bc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookNotFound } - return c.JSON(http.StatusOK, book) + + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", Book: book}) } // UpdateBook обрабатывает обновление книги. @@ -97,22 +98,29 @@ func (bc *Controller) GetBookByID(c echo.Context, ctx context.Context) error { // @produce application/json // @param id path string true "Book ID" // @param book body models.Book true "Book object" -// @success 200 {object} models.Book -// @failure 400 {object} config.HTTPError -func (bc *Controller) UpdateBook(c echo.Context, ctx context.Context) error { - var book models.Book - if err := c.Bind(&book); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +// @success 200 {object} responseSingleModel +// @failure 400 {object} string +// @failure 404 {object} string +// @failure 500 {object} string +func (bc *Controller) UpdateBook(c echo.Context) error { + id := c.Param("id") + if id == "" { + return ErrorValidationFailed + } + + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - paramID := c.Param("id") - updatedBook, err := bc.Service.UpdateBook(ctx, paramID, &book) + + updatedBook, err := bc.Service.UpdateBook(bc.Ctx, id, request.Book) if err != nil { if errors.Is(err, config.ErrorNotFound) { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookNotFound } - return echo.NewHTTPError(http.StatusNotAcceptable, config.ErrorNotUpdated) + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedBook) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", Book: updatedBook}) } // DeleteBook обрабатывает удаление книги по ID. @@ -121,13 +129,17 @@ func (bc *Controller) UpdateBook(c echo.Context, ctx context.Context) error { // @description Удаляет книгу по ее ID // @tags Книги // @param id path string true "Book ID" -// @success 204 -// @failure 404 {object} config.HTTPError -func (bc *Controller) DeleteBook(c echo.Context, ctx context.Context) error { +// @success 204 {object} models.Book +// @failure 400 {object} string +func (bc *Controller) DeleteBook(c echo.Context) error { id := c.Param("id") - book, err := bc.Service.DeleteBook(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + book, err := bc.Service.DeleteBook(bc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorDeleteBook } - return c.JSON(http.StatusOK, book) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", Book: book}) } diff --git a/bookback/internal/servers/http/controllers/book/errors.go b/bookback/internal/servers/http/controllers/book/errors.go new file mode 100644 index 0000000..e9a0a6b --- /dev/null +++ b/bookback/internal/servers/http/controllers/book/errors.go @@ -0,0 +1,31 @@ +package book + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +// Возможные ошибки при работе с книгами. + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorBookNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Книга не найдена", + ) + ErrorBookNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания книги. Книга с такими параметрами уже существует.", + ) + ErrorDeleteBook = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления книги", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/book/models.go b/bookback/internal/servers/http/controllers/book/models.go new file mode 100644 index 0000000..c3eea7e --- /dev/null +++ b/bookback/internal/servers/http/controllers/book/models.go @@ -0,0 +1,21 @@ +package book + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct { +} + +type requestModel struct { + Options Options `json:"options"` + Book *models.Book `json:"book"` +} + +type responseSingleModel struct { + Book *models.Book `json:"book"` + Status string `json:"status"` +} + +type responseListModel struct { + Books []models.Book `json:"books"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/book/routes.go b/bookback/internal/servers/http/controllers/book/routes.go new file mode 100644 index 0000000..1f34d67 --- /dev/null +++ b/bookback/internal/servers/http/controllers/book/routes.go @@ -0,0 +1,31 @@ +package book + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + bookrepo "github.com/SShlykov/zeitment/bookback/internal/services/book" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +// SetBookController регистрирует контроллер книг в маршрутизаторе. +func SetBookController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + service := bookrepo.NewService(bookrepo.NewRepository(database)) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +// RegisterRoutes регистрирует маршруты для обработки запросов к книгам. +func (bc *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(bc.Metrics)) + + group.GET("", bc.ListBooks) + group.POST("", bc.CreateBook) + group.GET("/:id", bc.GetBookByID) + group.PUT("/:id", bc.UpdateBook) + group.DELETE("/:id", bc.DeleteBook) +} diff --git a/bookback/internal/servers/http/controllers/bookevents/config.go b/bookback/internal/servers/http/controllers/bookevents/config.go new file mode 100644 index 0000000..08ef6ae --- /dev/null +++ b/bookback/internal/servers/http/controllers/bookevents/config.go @@ -0,0 +1,3 @@ +package bookevents + +const PathPrefix = "/api/v1/bookevents" diff --git a/bookback/internal/servers/http/controllers/bookevents/controller.go b/bookback/internal/servers/http/controllers/bookevents/controller.go index cb1ec5a..5348c02 100644 --- a/bookback/internal/servers/http/controllers/bookevents/controller.go +++ b/bookback/internal/servers/http/controllers/bookevents/controller.go @@ -2,33 +2,25 @@ package bookevents import ( "context" - "fmt" + "errors" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" service "github.com/SShlykov/zeitment/bookback/internal/services/bookevents" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) type Controller struct { - service service.Service + Service service.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -func NewController(service service.Service) *Controller { - return &Controller{ - service: service, - } -} - -func (bec *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/bookevents/:id", func(c echo.Context) error { return bec.GetBookEventByID(c, ctx) }) - e.PUT("/api/v1/bookevents/:id", func(c echo.Context) error { return bec.UpdateBookEvent(c, ctx) }) - e.DELETE("/api/v1/bookevents/:id", func(c echo.Context) error { return bec.DeleteBookEvent(c, ctx) }) - e.POST("/api/v1/bookevents", func(c echo.Context) error { return bec.CreateBookEvent(c, ctx) }) - e.GET("/api/v1/bookevents/book/:id", func(c echo.Context) error { return bec.GetBookEventsByBookID(c, ctx) }) - e.GET("/api/v1/bookevents/chapter/:id", func(c echo.Context) error { return bec.GetBookEventsByChapterID(c, ctx) }) - e.GET("/api/v1/bookevents/page/:id", func(c echo.Context) error { return bec.GetBookEventsByPageID(c, ctx) }) - e.GET("/api/v1/bookevents/paragraph/:id", func(c echo.Context) error { return bec.GetBookEventsByParagraphID(c, ctx) }) +// NewController создает новый экземпляр Controller. +func NewController(srv service.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // GetBookEventByID обрабатывает запросы на получение события книги по идентификатору. @@ -40,14 +32,18 @@ func (bec *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @param id path string true "ID события книги" // @success 200 {object} models.BookEvent // @failure 404 {object} config.HTTPError -func (bec *Controller) GetBookEventByID(c echo.Context, ctx context.Context) error { +func (bec *Controller) GetBookEventByID(c echo.Context) error { id := c.Param("id") - event, err := bec.service.GetBookEventByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + event, err := bec.Service.GetBookEventByID(bec.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookEventNotFound } - return c.JSON(http.StatusOK, event) + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", BookEvent: event}) } // UpdateBookEvent обрабатывает запросы на обновление события книги. @@ -61,20 +57,26 @@ func (bec *Controller) GetBookEventByID(c echo.Context, ctx context.Context) err // @param event body models.BookEvent true "BookEvent object" // @success 200 {object} models.BookEvent // @failure 400 {object} config.HTTPError -func (bec *Controller) UpdateBookEvent(c echo.Context, ctx context.Context) error { - var event models.BookEvent - if err := c.Bind(&event); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +func (bec *Controller) UpdateBookEvent(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } id := c.Param("id") - updatedEvent, err := bec.service.UpdateBookEvent(ctx, id, &event) + if id == "" { + return ErrorValidationFailed + } + + updatedEvent, err := bec.Service.UpdateBookEvent(bec.Ctx, id, request.BookEvents) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) + if errors.Is(err, config.ErrorNotFound) { + return ErrorBookEventNotFound + } + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedEvent) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", BookEvent: updatedEvent}) } // DeleteBookEvent обрабатывает запросы на удаление события книги. @@ -86,15 +88,18 @@ func (bec *Controller) UpdateBookEvent(c echo.Context, ctx context.Context) erro // @param id path string true "ID события книги" // @success 204 // @failure 404 {object} config.HTTPError -func (bec *Controller) DeleteBookEvent(c echo.Context, ctx context.Context) error { +func (bec *Controller) DeleteBookEvent(c echo.Context) error { id := c.Param("id") - deletedEvent, err := bec.service.DeleteBookEvent(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + deletedEvent, err := bec.Service.DeleteBookEvent(bec.Ctx, id) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorDeleteBookEvent } - return c.JSON(http.StatusOK, deletedEvent) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", BookEvent: deletedEvent}) } // CreateBookEvent обрабатывает запросы на создание события книги. @@ -107,17 +112,17 @@ func (bec *Controller) DeleteBookEvent(c echo.Context, ctx context.Context) erro // @param event body models.BookEvent true "BookEvent object" // @success 201 {object} models.BookEvent // @failure 400 {object} config.HTTPError -func (bec *Controller) CreateBookEvent(c echo.Context, ctx context.Context) error { - var event models.BookEvent - if err := c.Bind(&event); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +func (bec *Controller) CreateBookEvent(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - createdEvent, err := bec.service.CreateBookEvent(ctx, &event) + + createdEvent, err := bec.Service.CreateBookEvent(bec.Ctx, request.BookEvents) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusNotAcceptable, config.ErrorNotCreated) + return ErrorBookEventNotCreated } - return c.JSON(http.StatusCreated, createdEvent) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", BookEvent: createdEvent}) } // GetBookEventsByBookID обрабатывает запросы на получение событий книги по ID книги. @@ -129,13 +134,17 @@ func (bec *Controller) CreateBookEvent(c echo.Context, ctx context.Context) erro // @param id path string true "ID книги" // @success 200 {object} []models.BookEvent // @failure 404 {object} config.HTTPError -func (bec *Controller) GetBookEventsByBookID(c echo.Context, ctx context.Context) error { +func (bec *Controller) GetBookEventsByBookID(c echo.Context) error { id := c.Param("id") - events, err := bec.service.GetBookEventsByBookID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + events, err := bec.Service.GetBookEventsByBookID(bec.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookEventNotFound } - return c.JSON(http.StatusOK, events) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", BookEvents: events}) } // GetBookEventsByChapterID обрабатывает запросы на получение событий книги по ID главы. @@ -147,13 +156,18 @@ func (bec *Controller) GetBookEventsByBookID(c echo.Context, ctx context.Context // @param id path string true "ID главы" // @success 200 {object} []models.BookEvent // @failure 404 {object} config.HTTPError -func (bec *Controller) GetBookEventsByChapterID(c echo.Context, ctx context.Context) error { +func (bec *Controller) GetBookEventsByChapterID(c echo.Context) error { id := c.Param("id") - events, err := bec.service.GetBookEventsByChapterID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + events, err := bec.Service.GetBookEventsByChapterID(bec.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookEventNotFound } - return c.JSON(http.StatusOK, events) + + return c.JSON(http.StatusOK, responseListModel{Status: "ok", BookEvents: events}) } // GetBookEventsByPageID обрабатывает запросы на получение событий книги по ID страницы. @@ -165,13 +179,18 @@ func (bec *Controller) GetBookEventsByChapterID(c echo.Context, ctx context.Cont // @param id path string true "ID страницы" // @success 200 {object} []models.BookEvent // @failure 404 {object} config.HTTPError -func (bec *Controller) GetBookEventsByPageID(c echo.Context, ctx context.Context) error { +func (bec *Controller) GetBookEventsByPageID(c echo.Context) error { id := c.Param("id") - events, err := bec.service.GetBookEventsByPageID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + events, err := bec.Service.GetBookEventsByPageID(bec.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookEventNotFound } - return c.JSON(http.StatusOK, events) + + return c.JSON(http.StatusOK, responseListModel{Status: "ok", BookEvents: events}) } // GetBookEventsByParagraphID обрабатывает запросы на получение событий книги по ID параграфа. @@ -183,11 +202,16 @@ func (bec *Controller) GetBookEventsByPageID(c echo.Context, ctx context.Context // @param id path string true "ID параграфа" // @success 200 {object} []models.BookEvent // @failure 404 {object} config.HTTPError -func (bec *Controller) GetBookEventsByParagraphID(c echo.Context, ctx context.Context) error { +func (bec *Controller) GetBookEventsByParagraphID(c echo.Context) error { id := c.Param("id") - events, err := bec.service.GetBookEventsByParagraphID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + events, err := bec.Service.GetBookEventsByParagraphID(bec.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorBookEventNotFound } - return c.JSON(http.StatusOK, events) + + return c.JSON(http.StatusOK, responseListModel{Status: "ok", BookEvents: events}) } diff --git a/bookback/internal/servers/http/controllers/bookevents/errors.go b/bookback/internal/servers/http/controllers/bookevents/errors.go new file mode 100644 index 0000000..f5151aa --- /dev/null +++ b/bookback/internal/servers/http/controllers/bookevents/errors.go @@ -0,0 +1,31 @@ +package bookevents + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +// Возможные ошибки при работе с событиями книг. + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorBookEventNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Событие книги не найдено", + ) + ErrorBookEventNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания события книги. Событие с такими параметрами уже существует.", + ) + ErrorDeleteBookEvent = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления события книги", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/bookevents/models.go b/bookback/internal/servers/http/controllers/bookevents/models.go new file mode 100644 index 0000000..4bb9ab4 --- /dev/null +++ b/bookback/internal/servers/http/controllers/bookevents/models.go @@ -0,0 +1,20 @@ +package bookevents + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct{} + +type requestModel struct { + Options Options `json:"options"` + BookEvents *models.BookEvent `json:"book_events"` +} + +type responseSingleModel struct { + BookEvent *models.BookEvent `json:"book_event"` + Status string `json:"status"` +} + +type responseListModel struct { + BookEvents []models.BookEvent `json:"book_events"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/bookevents/routes.go b/bookback/internal/servers/http/controllers/bookevents/routes.go new file mode 100644 index 0000000..c8900cf --- /dev/null +++ b/bookback/internal/servers/http/controllers/bookevents/routes.go @@ -0,0 +1,33 @@ +package bookevents + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + bookeventsrepo "github.com/SShlykov/zeitment/bookback/internal/services/bookevents" + + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetBookEventController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + service := bookeventsrepo.NewService(bookeventsrepo.NewRepository(database)) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (bec *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(bec.Metrics)) + + group.POST("", bec.CreateBookEvent) + group.GET("/:id", bec.GetBookEventByID) + group.PUT("/:id", bec.UpdateBookEvent) + group.DELETE("/:id", bec.DeleteBookEvent) + group.GET("/book/:id", bec.GetBookEventsByBookID) + group.GET("/chapter/:id", bec.GetBookEventsByChapterID) + group.GET("/page/:id", bec.GetBookEventsByPageID) + group.GET("/paragraph/:id", bec.GetBookEventsByParagraphID) +} diff --git a/bookback/internal/servers/http/controllers/chapter/config.go b/bookback/internal/servers/http/controllers/chapter/config.go new file mode 100644 index 0000000..de90e41 --- /dev/null +++ b/bookback/internal/servers/http/controllers/chapter/config.go @@ -0,0 +1,3 @@ +package chapter + +const pathPrefix = "/api/v1/chapters" diff --git a/bookback/internal/servers/http/controllers/chapter/controller.go b/bookback/internal/servers/http/controllers/chapter/controller.go index d6610e0..ded89ad 100644 --- a/bookback/internal/servers/http/controllers/chapter/controller.go +++ b/bookback/internal/servers/http/controllers/chapter/controller.go @@ -2,30 +2,26 @@ package chapter import ( "context" + "errors" "fmt" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" "github.com/SShlykov/zeitment/bookback/internal/services/chapter" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) type Controller struct { Service chapter.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -func NewController(srv chapter.Service) *Controller { - return &Controller{Service: srv} -} - -func (ch *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/chapters", func(c echo.Context) error { return ch.ListChapters(c, ctx) }) - e.POST("/api/v1/chapters", func(c echo.Context) error { return ch.CreateChapter(c, ctx) }) - e.GET("/api/v1/chapters/:id", func(c echo.Context) error { return ch.GetChapterByID(c, ctx) }) - e.PUT("/api/v1/chapters/:id", func(c echo.Context) error { return ch.UpdateChapter(c, ctx) }) - e.DELETE("/api/v1/chapters/:id", func(c echo.Context) error { return ch.DeleteChapter(c, ctx) }) - - e.GET("/api/v1/chapters/book/:id", func(c echo.Context) error { return ch.GetChapterByBookID(c, ctx) }) +// NewController создает новый экземпляр Controller. +func NewController(srv chapter.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // ListChapters список глав @@ -36,13 +32,12 @@ func (ch *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @produce application/json // @success 200 {array} models.Chapter // @failure 500 {object} config.HTTPError -func (ch *Controller) ListChapters(c echo.Context, ctx context.Context) error { - chapters, err := ch.Service.ListChapters(ctx) +func (ch *Controller) ListChapters(c echo.Context) error { + chapters, err := ch.Service.ListChapters(ch.Ctx) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) + return ErrorUnknown } - return c.JSON(http.StatusOK, chapters) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Chapters: chapters}) } // CreateChapter создание новой главы @@ -55,18 +50,17 @@ func (ch *Controller) ListChapters(c echo.Context, ctx context.Context) error { // @param chapter body models.Chapter true "Chapter object" // @success 201 {object} models.Chapter // @failure 400 {object} config.HTTPError -func (ch *Controller) CreateChapter(c echo.Context, ctx context.Context) error { - var chap models.Chapter +func (ch *Controller) CreateChapter(c echo.Context) error { + var chap requestModel if err := c.Bind(&chap); err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) + return ErrorValidationFailed } - createdChapter, err := ch.Service.CreateChapter(ctx, &chap) + + createdChapter, err := ch.Service.CreateChapter(ch.Ctx, chap.Chapter) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotCreated) + return ErrorChapterNotCreated } - return c.JSON(http.StatusCreated, createdChapter) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", Chapter: createdChapter}) } // GetChapterByID получение главы по ID @@ -78,14 +72,17 @@ func (ch *Controller) CreateChapter(c echo.Context, ctx context.Context) error { // @produce application/json // @success 200 {object} models.Chapter // @failure 404 {object} config.HTTPError -func (ch *Controller) GetChapterByID(c echo.Context, ctx context.Context) error { +func (ch *Controller) GetChapterByID(c echo.Context) error { id := c.Param("id") - chapt, err := ch.Service.GetChapterByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + chapt, err := ch.Service.GetChapterByID(ch.Ctx, id) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorChapterNotFound } - return c.JSON(http.StatusOK, chapt) + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", Chapter: chapt}) } // UpdateChapter обновление главы @@ -99,17 +96,26 @@ func (ch *Controller) GetChapterByID(c echo.Context, ctx context.Context) error // @param chapter body models.Chapter true "Chapter object" // @success 200 {object} models.Chapter // @failure 400 {object} config.HTTPError -func (ch *Controller) UpdateChapter(c echo.Context, ctx context.Context) error { +func (ch *Controller) UpdateChapter(c echo.Context) error { id := c.Param("id") - var chap models.Chapter - if err := c.Bind(&chap); err != nil { + if id == "" { + return ErrorValidationFailed + } + + var request requestModel + if err := c.Bind(&request); err != nil { return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) } - updatedChapter, err := ch.Service.UpdateChapter(ctx, id, &chap) + + updatedChapter, err := ch.Service.UpdateChapter(ch.Ctx, id, request.Chapter) if err != nil { - return echo.NewHTTPError(http.StatusNotAcceptable, config.ErrorNotUpdated) + if errors.Is(err, config.ErrorNotFound) { + return ErrorChapterNotFound + } + fmt.Println(err) + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedChapter) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", Chapter: updatedChapter}) } // DeleteChapter удаление главы @@ -121,13 +127,17 @@ func (ch *Controller) UpdateChapter(c echo.Context, ctx context.Context) error { // @produce application/json // @success 200 {object} models.Chapter // @failure 406 {object} config.HTTPError -func (ch *Controller) DeleteChapter(c echo.Context, ctx context.Context) error { +func (ch *Controller) DeleteChapter(c echo.Context) error { id := c.Param("id") - chapt, err := ch.Service.DeleteChapter(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + chapt, err := ch.Service.DeleteChapter(ch.Ctx, id) if err != nil { return echo.NewHTTPError(http.StatusNotAcceptable, config.ErrorNotDeleted) } - return c.JSON(http.StatusOK, chapt) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", Chapter: chapt}) } // GetChapterByBookID получение глав по ID книги @@ -139,11 +149,15 @@ func (ch *Controller) DeleteChapter(c echo.Context, ctx context.Context) error { // @produce application/json // @success 200 {array} models.Chapter // @failure 404 {object} config.HTTPError -func (ch *Controller) GetChapterByBookID(c echo.Context, ctx context.Context) error { +func (ch *Controller) GetChapterByBookID(c echo.Context) error { id := c.Param("id") - chapters, err := ch.Service.GetChapterByBookID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + chapters, err := ch.Service.GetChapterByBookID(ch.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorDeleteChapter } - return c.JSON(http.StatusOK, chapters) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Chapters: chapters}) } diff --git a/bookback/internal/servers/http/controllers/chapter/errors.go b/bookback/internal/servers/http/controllers/chapter/errors.go new file mode 100644 index 0000000..0e98c3e --- /dev/null +++ b/bookback/internal/servers/http/controllers/chapter/errors.go @@ -0,0 +1,31 @@ +package chapter + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +// Возможные ошибки при работе с главами книги. + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorChapterNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Глава не найдена", + ) + ErrorChapterNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания главы. Глава с такими параметрами уже существует.", + ) + ErrorDeleteChapter = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления главы", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/chapter/models.go b/bookback/internal/servers/http/controllers/chapter/models.go new file mode 100644 index 0000000..cebb62c --- /dev/null +++ b/bookback/internal/servers/http/controllers/chapter/models.go @@ -0,0 +1,21 @@ +package chapter + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct { +} + +type requestModel struct { + Options Options `json:"options"` + Chapter *models.Chapter `json:"chapter"` +} + +type responseSingleModel struct { + Chapter *models.Chapter `json:"chapter"` + Status string `json:"status"` +} + +type responseListModel struct { + Chapters []models.Chapter `json:"chapters"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/chapter/routes.go b/bookback/internal/servers/http/controllers/chapter/routes.go new file mode 100644 index 0000000..1701b00 --- /dev/null +++ b/bookback/internal/servers/http/controllers/chapter/routes.go @@ -0,0 +1,33 @@ +package chapter + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + bookrepo "github.com/SShlykov/zeitment/bookback/internal/services/book" + chapterrepo "github.com/SShlykov/zeitment/bookback/internal/services/chapter" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetChapterController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + chapterRepo := chapterrepo.NewRepository(database) + bookRepo := bookrepo.NewRepository(database) + service := chapterrepo.NewService(chapterRepo, bookRepo) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (ch *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(pathPrefix) + group.Use(httpmiddlewares.MetricsLogger(ch.Metrics)) + + group.GET("", ch.ListChapters) + group.POST("", ch.CreateChapter) + group.GET("/:id", ch.GetChapterByID) + group.PUT("/:id", ch.UpdateChapter) + group.DELETE("/:id", ch.DeleteChapter) + group.GET("/book/:id", ch.GetChapterByBookID) +} diff --git a/bookback/internal/servers/http/controllers/health/config.go b/bookback/internal/servers/http/controllers/health/config.go new file mode 100644 index 0000000..a3c7e5b --- /dev/null +++ b/bookback/internal/servers/http/controllers/health/config.go @@ -0,0 +1,3 @@ +package health + +const PathPrefix = "/api/v1/health" diff --git a/bookback/internal/servers/http/controllers/health/controller.go b/bookback/internal/servers/http/controllers/health/controller.go index caadcc9..d80dec2 100644 --- a/bookback/internal/servers/http/controllers/health/controller.go +++ b/bookback/internal/servers/http/controllers/health/controller.go @@ -2,25 +2,21 @@ package health import ( "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) -type Controller interface { - GetHealthCheck(c echo.Context) error - RegisterRoutes(e *echo.Echo, _ context.Context) +type Controller struct { + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -type healthController struct { -} - -// NewController is constructor. -func NewController() Controller { - return &healthController{} -} - -func (hc *healthController) RegisterRoutes(e *echo.Echo, _ context.Context) { - e.GET("/api/v1/health", hc.GetHealthCheck) +// NewController создает новый экземпляр Controller. +func NewController(metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Metrics: metric, Logger: logger, Ctx: ctx} } // GetHealthCheck возвращает статус приложения. @@ -31,6 +27,6 @@ func (hc *healthController) RegisterRoutes(e *echo.Echo, _ context.Context) { // @produce application/json // @success 200 {string} string "healthy" // @failure 500 {object} config.HTTPError -func (hc *healthController) GetHealthCheck(c echo.Context) error { +func (hc *Controller) GetHealthCheck(c echo.Context) error { return c.JSON(http.StatusOK, "healthy") } diff --git a/bookback/internal/servers/http/controllers/health/routes.go b/bookback/internal/servers/http/controllers/health/routes.go new file mode 100644 index 0000000..7ab912e --- /dev/null +++ b/bookback/internal/servers/http/controllers/health/routes.go @@ -0,0 +1,23 @@ +package health + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetHealthController(e *echo.Echo, _ db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + controller := NewController(metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (hc *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(hc.Metrics)) + + group.GET("/", hc.GetHealthCheck) +} diff --git a/bookback/internal/servers/http/controllers/mapvariables/config.go b/bookback/internal/servers/http/controllers/mapvariables/config.go new file mode 100644 index 0000000..d4be744 --- /dev/null +++ b/bookback/internal/servers/http/controllers/mapvariables/config.go @@ -0,0 +1,3 @@ +package mapvariables + +const PathPrefix = "/api/v1/mapvariables" diff --git a/bookback/internal/servers/http/controllers/mapvariables/controller.go b/bookback/internal/servers/http/controllers/mapvariables/controller.go index fe39b0a..176fa1f 100644 --- a/bookback/internal/servers/http/controllers/mapvariables/controller.go +++ b/bookback/internal/servers/http/controllers/mapvariables/controller.go @@ -2,48 +2,27 @@ package mapvariables import ( "context" + "errors" + "fmt" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" service "github.com/SShlykov/zeitment/bookback/internal/services/mapvariables" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) +// Controller структура для HTTP-контроллера книг. type Controller struct { - service service.Service + Service service.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -func NewController(service service.Service) *Controller { - return &Controller{ - service: service, - } -} - -func (mvc *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/mapvariables/:id", - func(c echo.Context) error { return mvc.GetMapVariableByID(c, ctx) }) - e.PUT("/api/v1/mapvariables/:id", - func(c echo.Context) error { return mvc.UpdateMapVariable(c, ctx) }) - e.DELETE("/api/v1/mapvariables/:id", - func(c echo.Context) error { return mvc.DeleteMapVariable(c, ctx) }) - e.POST("/api/v1/mapvariables", - func(c echo.Context) error { return mvc.CreateMapVariable(c, ctx) }) - e.GET("/api/v1/mapvariables/book/:id", - func(c echo.Context) error { return mvc.GetMapVariablesByBookID(c, ctx) }) - e.GET("/api/v1/mapvariables/chapter/:id", - func(c echo.Context) error { return mvc.GetMapVariablesByChapterID(c, ctx) }) - e.GET("/api/v1/mapvariables/page/:id", - func(c echo.Context) error { return mvc.GetMapVariablesByPageID(c, ctx) }) - e.GET("/api/v1/mapvariables/paragraph/:id", - func(c echo.Context) error { return mvc.GetMapVariablesByParagraphID(c, ctx) }) - e.GET("/api/v1/bookevents/maplink/:link/book/:id/", - func(c echo.Context) error { return mvc.GetMapVariablesByMapLinkAndBookID(c, ctx) }) - e.GET("/api/v1/bookevents/maplink/:link/chapter/:id/", - func(c echo.Context) error { return mvc.GetMapVariablesByMapLinkAndChapterID(c, ctx) }) - e.GET("/api/v1/bookevents/maplink/:link/page/:id/", - func(c echo.Context) error { return mvc.GetMapVariablesByMapLinkAndPageID(c, ctx) }) - e.GET("/api/v1/bookevents/maplink/:link/paragraph/:id/", - func(c echo.Context) error { return mvc.GetMapVariablesByMapLinkAndParagraphID(c, ctx) }) +// NewController создает новый экземпляр Controller. +func NewController(srv service.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // GetMapVariableByID обрабатывает запросы на получение переменной карты по идентификатору. @@ -55,14 +34,19 @@ func (mvc *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @param id path string true "ID переменной карты" // @success 200 {object} models.MapVariable // @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariableByID(c echo.Context, ctx context.Context) error { +func (mvc *Controller) GetMapVariableByID(c echo.Context) error { id := c.Param("id") - variable, err := mvc.service.GetMapVariableByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + variable, err := mvc.Service.GetMapVariableByID(mvc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + fmt.Println(err) + return ErrorMapVariableNotFound } - return c.JSON(http.StatusOK, variable) + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", MapVariable: variable}) } // UpdateMapVariable обрабатывает запросы на обновление переменной карты. @@ -76,19 +60,26 @@ func (mvc *Controller) GetMapVariableByID(c echo.Context, ctx context.Context) e // @param variable body models.MapVariable true "MapVariable object" // @success 200 {object} models.MapVariable // @failure 400 {object} config.HTTPError -func (mvc *Controller) UpdateMapVariable(c echo.Context, ctx context.Context) error { +func (mvc *Controller) UpdateMapVariable(c echo.Context) error { id := c.Param("id") - var variable models.MapVariable - if err := c.Bind(&variable); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) + if id == "" { + return ErrorValidationFailed + } + + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - updatedVariable, err := mvc.service.UpdateMapVariable(ctx, id, &variable) + updatedVariable, err := mvc.Service.UpdateMapVariable(mvc.Ctx, id, request.MapVariables) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorForbidden) + if errors.Is(err, config.ErrorNotFound) { + return ErrorMapVariableNotFound + } + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedVariable) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", MapVariable: updatedVariable}) } // DeleteMapVariable обрабатывает запросы на удаление переменной карты. @@ -99,13 +90,18 @@ func (mvc *Controller) UpdateMapVariable(c echo.Context, ctx context.Context) er // @param id path string true "ID переменной карты" // @success 204 // @failure 404 {object} config.HTTPError -func (mvc *Controller) DeleteMapVariable(c echo.Context, ctx context.Context) error { +func (mvc *Controller) DeleteMapVariable(c echo.Context) error { id := c.Param("id") - if _, err := mvc.service.DeleteMapVariable(ctx, id); err != nil { + if id == "" { + return ErrorValidationFailed + } + + mapVariable, err := mvc.Service.DeleteMapVariable(mvc.Ctx, id) + if err != nil { return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) } - return c.NoContent(http.StatusNoContent) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", MapVariable: mapVariable}) } // GetMapVariablesByBookID обрабатывает запросы на получение переменных карты по идентификатору книги. @@ -117,14 +113,18 @@ func (mvc *Controller) DeleteMapVariable(c echo.Context, ctx context.Context) er // @param id path string true "ID книги" // @success 200 {array} models.MapVariable // @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByBookID(c echo.Context, ctx context.Context) error { +func (mvc *Controller) GetMapVariablesByBookID(c echo.Context) error { id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByBookID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + variables, err := mvc.Service.GetMapVariablesByBookID(mvc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorMapVariableNotFound } - return c.JSON(http.StatusOK, variables) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", MapVariables: variables}) } // GetMapVariablesByChapterID обрабатывает запросы на получение переменных карты по идентификатору главы. @@ -136,14 +136,18 @@ func (mvc *Controller) GetMapVariablesByBookID(c echo.Context, ctx context.Conte // @param id path string true "ID главы" // @success 200 {array} models.MapVariable // @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByChapterID(c echo.Context, ctx context.Context) error { +func (mvc *Controller) GetMapVariablesByChapterID(c echo.Context) error { id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByChapterID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + variables, err := mvc.Service.GetMapVariablesByChapterID(mvc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorMapVariableNotFound } - return c.JSON(http.StatusOK, variables) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", MapVariables: variables}) } // GetMapVariablesByPageID обрабатывает запросы на получение переменных карты по идентификатору страницы. @@ -155,14 +159,18 @@ func (mvc *Controller) GetMapVariablesByChapterID(c echo.Context, ctx context.Co // @param id path string true "ID страницы" // @success 200 {array} models.MapVariable // @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByPageID(c echo.Context, ctx context.Context) error { +func (mvc *Controller) GetMapVariablesByPageID(c echo.Context) error { id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByPageID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + variables, err := mvc.Service.GetMapVariablesByPageID(mvc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorMapVariableNotFound } - return c.JSON(http.StatusOK, variables) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", MapVariables: variables}) } // GetMapVariablesByParagraphID обрабатывает запросы на получение переменных карты по идентификатору параграфа. @@ -174,14 +182,18 @@ func (mvc *Controller) GetMapVariablesByPageID(c echo.Context, ctx context.Conte // @param id path string true "ID параграфа" // @success 200 {array} models.MapVariable // @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByParagraphID(c echo.Context, ctx context.Context) error { +func (mvc *Controller) GetMapVariablesByParagraphID(c echo.Context) error { id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByParagraphID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + variables, err := mvc.Service.GetMapVariablesByParagraphID(mvc.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorMapVariableNotFound } - return c.JSON(http.StatusOK, variables) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", MapVariables: variables}) } // CreateMapVariable обрабатывает создание новой переменной карты. @@ -194,104 +206,16 @@ func (mvc *Controller) GetMapVariablesByParagraphID(c echo.Context, ctx context. // @param variable body models.MapVariable true "MapVariable object" // @success 201 {object} models.MapVariable // @failure 400 {object} config.HTTPError -func (mvc *Controller) CreateMapVariable(c echo.Context, ctx context.Context) error { - var variable models.MapVariable - if err := c.Bind(&variable); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +func (mvc *Controller) CreateMapVariable(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - createdVariable, err := mvc.service.CreateMapVariable(ctx, &variable) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorForbidden) - } - - return c.JSON(http.StatusCreated, createdVariable) -} - -// GetMapVariablesByMapLinkAndBookID обрабатывает запросы на получение переменных карты по идентификатору карты -// и идентификатору книги. -// @router /bookevents/maplink/{link}/book/{id} [get] -// @summary Получить переменные карты по идентификатору карты и идентификатору книги -// @description Извлекает переменные карты по идентификатору карты и идентификатору книги -// @tags Переменные карты -// @produce application/json -// @param link path string true "ID карты" -// @param id path string true "ID книги" -// @success 200 {array} models.MapVariable -// @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByMapLinkAndBookID(c echo.Context, ctx context.Context) error { - link := c.Param("link") - id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByMapLinkAndBookID(ctx, link, id) + createdVariable, err := mvc.Service.CreateMapVariable(mvc.Ctx, request.MapVariables) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) - } - - return c.JSON(http.StatusOK, variables) -} - -// GetMapVariablesByMapLinkAndChapterID обрабатывает запросы на получение переменных карты по идентификатору карты -// и идентификатору главы. -// @router /bookevents/maplink/{link}/chapter/{id} [get] -// @summary Получить переменные карты по идентификатору карты и идентификатору главы -// @description Извлекает переменные карты по идентификатору карты и идентификатору главы -// @tags Переменные карты -// @produce application/json -// @param link path string true "ID карты" -// @param id path string true "ID главы" -// @success 200 {array} models.MapVariable -// @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByMapLinkAndChapterID(c echo.Context, ctx context.Context) error { - link := c.Param("link") - id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByMapLinkAndChapterID(ctx, link, id) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) - } - - return c.JSON(http.StatusOK, variables) -} - -// GetMapVariablesByMapLinkAndPageID обрабатывает запросы на получение переменных карты по идентификатору карты -// и идентификатору страницы. -// @router /bookevents/maplink/{link}/page/{id} [get] -// @summary Получить переменные карты по идентификатору карты и идентификатору страницы -// @description Извлекает переменные карты по идентификатору карты и идентификатору страницы -// @tags Переменные карты -// @produce application/json -// @param link path string true "ID карты" -// @param id path string true "ID страницы" -// @success 200 {array} models.MapVariable -// @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByMapLinkAndPageID(c echo.Context, ctx context.Context) error { - link := c.Param("link") - id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByMapLinkAndPageID(ctx, link, id) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) - } - - return c.JSON(http.StatusOK, variables) -} - -// GetMapVariablesByMapLinkAndParagraphID обрабатывает запросы на получение переменных карты по идентификатору карты -// и идентификатору параграфа. -// @router /bookevents/maplink/{link}/paragraph/{id} [get] -// @summary Получить переменные карты по идентификатору карты и идентификатору параграфа -// @description Извлекает переменные карты по идентификатору карты и идентификатору параграфа -// @tags Переменные карты -// @produce application/json -// @param link path string true "ID карты" -// @param id path string true "ID параграфа" -// @success 200 {array} models.MapVariable -// @failure 404 {object} config.HTTPError -func (mvc *Controller) GetMapVariablesByMapLinkAndParagraphID(c echo.Context, ctx context.Context) error { - link := c.Param("link") - id := c.Param("id") - variables, err := mvc.service.GetMapVariablesByMapLinkAndParagraphID(ctx, link, id) - if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorMapVariableNotCreated } - return c.JSON(http.StatusOK, variables) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", MapVariable: createdVariable}) } diff --git a/bookback/internal/servers/http/controllers/mapvariables/errors.go b/bookback/internal/servers/http/controllers/mapvariables/errors.go new file mode 100644 index 0000000..672992d --- /dev/null +++ b/bookback/internal/servers/http/controllers/mapvariables/errors.go @@ -0,0 +1,29 @@ +package mapvariables + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorMapVariableNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Переменная не найдена", + ) + ErrorMapVariableNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания переменной. Переменная с такими параметрами уже существует.", + ) + ErrorDeleteMapVariable = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления переменной", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/mapvariables/models.go b/bookback/internal/servers/http/controllers/mapvariables/models.go new file mode 100644 index 0000000..07d0511 --- /dev/null +++ b/bookback/internal/servers/http/controllers/mapvariables/models.go @@ -0,0 +1,20 @@ +package mapvariables + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct{} + +type requestModel struct { + Options Options `json:"options"` + MapVariables *models.MapVariable `json:"map_variables"` +} + +type responseSingleModel struct { + MapVariable *models.MapVariable `json:"map_variable"` + Status string `json:"status"` +} + +type responseListModel struct { + MapVariables []models.MapVariable `json:"map_variables"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/mapvariables/routes.go b/bookback/internal/servers/http/controllers/mapvariables/routes.go new file mode 100644 index 0000000..7adcc95 --- /dev/null +++ b/bookback/internal/servers/http/controllers/mapvariables/routes.go @@ -0,0 +1,32 @@ +package mapvariables + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + mapvariablesrepo "github.com/SShlykov/zeitment/bookback/internal/services/mapvariables" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetMapVariablesController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + service := mapvariablesrepo.NewService(mapvariablesrepo.NewRepository(database)) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (mvc *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(mvc.Metrics)) + + group.GET("/:id", mvc.GetMapVariableByID) + group.PUT("/:id", mvc.UpdateMapVariable) + group.DELETE("/:id", mvc.DeleteMapVariable) + group.POST("", mvc.CreateMapVariable) + group.GET("/book/:id", mvc.GetMapVariablesByBookID) + group.GET("/chapter/:id", mvc.GetMapVariablesByChapterID) + group.GET("/page/:id", mvc.GetMapVariablesByPageID) + group.GET("/paragraph/:id", mvc.GetMapVariablesByParagraphID) +} diff --git a/bookback/internal/servers/http/controllers/page/config.go b/bookback/internal/servers/http/controllers/page/config.go new file mode 100644 index 0000000..940710e --- /dev/null +++ b/bookback/internal/servers/http/controllers/page/config.go @@ -0,0 +1,3 @@ +package page + +const PathPrefix = "/api/v1/pages" diff --git a/bookback/internal/servers/http/controllers/page/controller.go b/bookback/internal/servers/http/controllers/page/controller.go index 452ce32..034ef87 100644 --- a/bookback/internal/servers/http/controllers/page/controller.go +++ b/bookback/internal/servers/http/controllers/page/controller.go @@ -2,30 +2,24 @@ package page import ( "context" - "fmt" + "errors" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" service "github.com/SShlykov/zeitment/bookback/internal/services/page" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) type Controller struct { Service service.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -func NewController(srv service.Service) *Controller { - return &Controller{Service: srv} -} - -func (p *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/pages", func(c echo.Context) error { return p.ListPages(c, ctx) }) - e.POST("/api/v1/pages", func(c echo.Context) error { return p.CreatePage(c, ctx) }) - e.GET("/api/v1/pages/:id", func(c echo.Context) error { return p.GetPageByID(c, ctx) }) - e.PUT("/api/v1/pages/:id", func(c echo.Context) error { return p.UpdatePage(c, ctx) }) - e.DELETE("/api/v1/pages/:id", func(c echo.Context) error { return p.DeletePage(c, ctx) }) - - e.GET("/api/v1/chapters/:id/pages", func(c echo.Context) error { return p.GetPagesByChapterID(c, ctx) }) +func NewController(srv service.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // ListPages список страниц @@ -36,13 +30,12 @@ func (p *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @produce application/json // @success 200 {array} models.Page // @failure 500 {object} config.HTTPError -func (p *Controller) ListPages(c echo.Context, ctx context.Context) error { - pages, err := p.Service.ListPages(ctx) +func (p *Controller) ListPages(c echo.Context) error { + pages, err := p.Service.ListPages(p.Ctx) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) + return ErrorUnknown } - return c.JSON(http.StatusOK, pages) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Pages: pages}) } // CreatePage создание новой страницы @@ -55,16 +48,17 @@ func (p *Controller) ListPages(c echo.Context, ctx context.Context) error { // @param page body models.Page true "Page object" // @success 201 {object} models.Page // @failure 400 {object} config.HTTPError -func (p *Controller) CreatePage(c echo.Context, ctx context.Context) error { - var page models.Page - if err := c.Bind(&page); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +func (p *Controller) CreatePage(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - createdPage, err := p.Service.CreatePage(ctx, &page) + + createdPage, err := p.Service.CreatePage(p.Ctx, request.Page) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotCreated) + return ErrorPageNotCreated } - return c.JSON(http.StatusCreated, createdPage) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", Page: createdPage}) } // GetPageByID получение страницы по ID @@ -76,13 +70,18 @@ func (p *Controller) CreatePage(c echo.Context, ctx context.Context) error { // @param id path string true "ID страницы" // @success 200 {object} models.Page // @failure 404 {object} config.HTTPError -func (p *Controller) GetPageByID(c echo.Context, ctx context.Context) error { +func (p *Controller) GetPageByID(c echo.Context) error { id := c.Param("id") - page, err := p.Service.GetPageByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + page, err := p.Service.GetPageByID(p.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorPageNotFound } - return c.JSON(http.StatusOK, page) + + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", Page: page}) } // UpdatePage обновление страницы @@ -96,18 +95,25 @@ func (p *Controller) GetPageByID(c echo.Context, ctx context.Context) error { // @param page body models.Page true "Page object" // @success 200 {object} models.Page // @failure 400 {object} config.HTTPError -func (p *Controller) UpdatePage(c echo.Context, ctx context.Context) error { +func (p *Controller) UpdatePage(c echo.Context) error { id := c.Param("id") - var page models.Page - if err := c.Bind(&page); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) + if id == "" { + return ErrorValidationFailed } - updatedPage, err := p.Service.UpdatePage(ctx, id, &page) + + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed + } + + updatedPage, err := p.Service.UpdatePage(p.Ctx, id, request.Page) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotUpdated) + if errors.Is(err, config.ErrorNotFound) { + return ErrorPageNotFound + } + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedPage) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", Page: updatedPage}) } // DeletePage удаление страницы @@ -119,14 +125,17 @@ func (p *Controller) UpdatePage(c echo.Context, ctx context.Context) error { // @produce application/json // @success 200 {object} models.Page // @failure 500 {object} config.HTTPError -func (p *Controller) DeletePage(c echo.Context, ctx context.Context) error { +func (p *Controller) DeletePage(c echo.Context) error { id := c.Param("id") - deletedPage, err := p.Service.DeletePage(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + deletedPage, err := p.Service.DeletePage(p.Ctx, id) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotDeleted) + return ErrorDeletePage } - return c.JSON(http.StatusOK, deletedPage) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", Page: deletedPage}) } // GetPagesByChapterID получение страниц по ID главы @@ -138,11 +147,15 @@ func (p *Controller) DeletePage(c echo.Context, ctx context.Context) error { // @param id path string true "ID главы" // @success 200 {array} models.Page // @failure 404 {object} config.HTTPError -func (p *Controller) GetPagesByChapterID(c echo.Context, ctx context.Context) error { +func (p *Controller) GetPagesByChapterID(c echo.Context) error { id := c.Param("id") - pages, err := p.Service.GetPagesByChapterID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + pages, err := p.Service.GetPagesByChapterID(p.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorPageNotFound } - return c.JSON(http.StatusOK, pages) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Pages: pages}) } diff --git a/bookback/internal/servers/http/controllers/page/errors.go b/bookback/internal/servers/http/controllers/page/errors.go new file mode 100644 index 0000000..38e1e3b --- /dev/null +++ b/bookback/internal/servers/http/controllers/page/errors.go @@ -0,0 +1,31 @@ +package page + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +// Возможные ошибки, которые могут возникнуть при работе с контроллером страниц. + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorPageNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Страница не найдена", + ) + ErrorPageNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания страницы. Страница с такими параметрами уже существует.", + ) + ErrorDeletePage = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления страницы", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/page/models.go b/bookback/internal/servers/http/controllers/page/models.go new file mode 100644 index 0000000..75b06d4 --- /dev/null +++ b/bookback/internal/servers/http/controllers/page/models.go @@ -0,0 +1,21 @@ +package page + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct { +} + +type requestModel struct { + Options Options `json:"options"` + Page *models.Page `json:"page"` +} + +type responseSingleModel struct { + Page *models.Page `json:"page"` + Status string `json:"status"` +} + +type responseListModel struct { + Pages []models.Page `json:"pages"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/page/routes.go b/bookback/internal/servers/http/controllers/page/routes.go new file mode 100644 index 0000000..c1e0c53 --- /dev/null +++ b/bookback/internal/servers/http/controllers/page/routes.go @@ -0,0 +1,30 @@ +package page + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + pagerepo "github.com/SShlykov/zeitment/bookback/internal/services/page" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetPageController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + service := pagerepo.NewService(pagerepo.NewRepository(database)) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (p *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(p.Metrics)) + + group.GET("", p.ListPages) + group.POST("", p.CreatePage) + group.GET("/:id", p.GetPageByID) + group.PUT("/:id", p.UpdatePage) + group.DELETE("/:id", p.DeletePage) + group.GET("/chapters/:id", p.GetPagesByChapterID) +} diff --git a/bookback/internal/servers/http/controllers/paragraph/config.go b/bookback/internal/servers/http/controllers/paragraph/config.go new file mode 100644 index 0000000..fdea98e --- /dev/null +++ b/bookback/internal/servers/http/controllers/paragraph/config.go @@ -0,0 +1,3 @@ +package paragraph + +const PathPrefix = "/api/v1/paragraphs" diff --git a/bookback/internal/servers/http/controllers/paragraph/controller.go b/bookback/internal/servers/http/controllers/paragraph/controller.go index ebdd52c..e66bff4 100644 --- a/bookback/internal/servers/http/controllers/paragraph/controller.go +++ b/bookback/internal/servers/http/controllers/paragraph/controller.go @@ -2,30 +2,25 @@ package paragraph import ( "context" - "fmt" + "errors" "github.com/SShlykov/zeitment/bookback/internal/config" - "github.com/SShlykov/zeitment/bookback/internal/models" + "github.com/SShlykov/zeitment/bookback/internal/metrics" service "github.com/SShlykov/zeitment/bookback/internal/services/paragraph" "github.com/labstack/echo/v4" + "log/slog" "net/http" ) type Controller struct { Service service.Service + Metrics metrics.Metrics + Logger *slog.Logger + Ctx context.Context } -func NewController(srv service.Service) *Controller { - return &Controller{Service: srv} -} - -func (p *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { - e.GET("/api/v1/paragraphs", func(c echo.Context) error { return p.ListParagraphs(c, ctx) }) - e.POST("/api/v1/paragraphs", func(c echo.Context) error { return p.CreateParagraph(c, ctx) }) - e.GET("/api/v1/paragraphs/:id", func(c echo.Context) error { return p.GetParagraphByID(c, ctx) }) - e.PUT("/api/v1/paragraphs/:id", func(c echo.Context) error { return p.UpdateParagraph(c, ctx) }) - e.DELETE("/api/v1/paragraphs/:id", func(c echo.Context) error { return p.DeleteParagraph(c, ctx) }) - - e.GET("/api/v1/pages/:id/paragraphs", func(c echo.Context) error { return p.GetParagraphsByPageID(c, ctx) }) +// NewController создает новый экземпляр Controller. +func NewController(srv service.Service, metric metrics.Metrics, logger *slog.Logger, ctx context.Context) *Controller { + return &Controller{Service: srv, Metrics: metric, Logger: logger, Ctx: ctx} } // ListParagraphs список параграфов @@ -36,13 +31,12 @@ func (p *Controller) RegisterRoutes(e *echo.Echo, ctx context.Context) { // @produce application/json // @success 200 {array} models.Paragraph // @failure 500 {object} config.HTTPError -func (p *Controller) ListParagraphs(c echo.Context, ctx context.Context) error { - paragraphs, err := p.Service.ListParagraphs(ctx) +func (p *Controller) ListParagraphs(c echo.Context) error { + paragraphs, err := p.Service.ListParagraphs(p.Ctx) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusBadGateway, config.ErrorForbidden) + return ErrorUnknown } - return c.JSON(http.StatusOK, paragraphs) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Paragraphs: paragraphs}) } // CreateParagraph создание нового параграфа @@ -55,17 +49,17 @@ func (p *Controller) ListParagraphs(c echo.Context, ctx context.Context) error { // @param paragraph body models.Paragraph true "Paragraph object" // @success 201 {object} models.Paragraph // @failure 400 {object} config.HTTPError -func (p *Controller) CreateParagraph(c echo.Context, ctx context.Context) error { - var paragraph models.Paragraph - if err := c.Bind(¶graph); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) +func (p *Controller) CreateParagraph(c echo.Context) error { + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed } - createdParagraph, err := p.Service.CreateParagraph(ctx, ¶graph) + + createdParagraph, err := p.Service.CreateParagraph(p.Ctx, request.Paragraph) if err != nil { - fmt.Println(err) - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotCreated) + return ErrorParagraphNotFound } - return c.JSON(http.StatusCreated, createdParagraph) + return c.JSON(http.StatusCreated, responseSingleModel{Status: "created", Paragraph: createdParagraph}) } // GetParagraphByID получение параграфа по идентификатору @@ -77,13 +71,18 @@ func (p *Controller) CreateParagraph(c echo.Context, ctx context.Context) error // @produce application/json // @success 200 {object} models.Paragraph // @failure 404 {object} config.HTTPError -func (p *Controller) GetParagraphByID(c echo.Context, ctx context.Context) error { +func (p *Controller) GetParagraphByID(c echo.Context) error { id := c.Param("id") - paragraph, err := p.Service.GetParagraphByID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + paragraph, err := p.Service.GetParagraphByID(p.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorParagraphNotFound } - return c.JSON(http.StatusOK, paragraph) + + return c.JSON(http.StatusOK, responseSingleModel{Status: "ok", Paragraph: paragraph}) } // UpdateParagraph обновление параграфа @@ -97,17 +96,25 @@ func (p *Controller) GetParagraphByID(c echo.Context, ctx context.Context) error // @param paragraph body models.Paragraph true "Paragraph object" // @success 200 {object} models.Paragraph // @failure 400 {object} config.HTTPError -func (p *Controller) UpdateParagraph(c echo.Context, ctx context.Context) error { +func (p *Controller) UpdateParagraph(c echo.Context) error { id := c.Param("id") - var paragraph models.Paragraph - if err := c.Bind(¶graph); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, config.ErrorBadInput) + if id == "" { + return ErrorValidationFailed } - updatedParagraph, err := p.Service.UpdateParagraph(ctx, id, ¶graph) + + var request requestModel + if err := c.Bind(&request); err != nil { + return ErrorValidationFailed + } + + updatedParagraph, err := p.Service.UpdateParagraph(p.Ctx, id, request.Paragraph) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotUpdated) + if errors.Is(err, config.ErrorNotFound) { + return ErrorParagraphNotFound + } + return ErrorUnknown } - return c.JSON(http.StatusOK, updatedParagraph) + return c.JSON(http.StatusOK, responseSingleModel{Status: "updated", Paragraph: updatedParagraph}) } // DeleteParagraph удаление параграфа @@ -118,17 +125,21 @@ func (p *Controller) UpdateParagraph(c echo.Context, ctx context.Context) error // @param id path string true "ID параграфа" // @success 200 {object} models.Paragraph // @failure 500 {object} config.HTTPError -func (p *Controller) DeleteParagraph(c echo.Context, ctx context.Context) error { +func (p *Controller) DeleteParagraph(c echo.Context) error { id := c.Param("id") - deletedParagraph, err := p.Service.DeleteParagraph(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + deletedParagraph, err := p.Service.DeleteParagraph(p.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, config.ErrorNotDeleted) + return ErrorDeleteParagraph } - return c.JSON(http.StatusOK, deletedParagraph) + return c.JSON(http.StatusOK, responseSingleModel{Status: "deleted", Paragraph: deletedParagraph}) } // GetParagraphsByPageID получение параграфов по ID страницы -// @router /pages/{id}/paragraphs [get] +// @router /paragraphs/pages/{id} [get] // @summary Получить параграфы по ID страницы // @description Извлекает параграфы по ID страницы // @tags Параграфы @@ -136,11 +147,15 @@ func (p *Controller) DeleteParagraph(c echo.Context, ctx context.Context) error // @produce application/json // @success 200 {array} models.Paragraph // @failure 404 {object} config.HTTPError -func (p *Controller) GetParagraphsByPageID(c echo.Context, ctx context.Context) error { +func (p *Controller) GetParagraphsByPageID(c echo.Context) error { id := c.Param("id") - paragraphs, err := p.Service.GetParagraphsByPageID(ctx, id) + if id == "" { + return ErrorValidationFailed + } + + paragraphs, err := p.Service.GetParagraphsByPageID(p.Ctx, id) if err != nil { - return echo.NewHTTPError(http.StatusNotFound, config.ErrorNotFound) + return ErrorParagraphNotFound } - return c.JSON(http.StatusOK, paragraphs) + return c.JSON(http.StatusOK, responseListModel{Status: "ok", Paragraphs: paragraphs}) } diff --git a/bookback/internal/servers/http/controllers/paragraph/errors.go b/bookback/internal/servers/http/controllers/paragraph/errors.go new file mode 100644 index 0000000..28dc31f --- /dev/null +++ b/bookback/internal/servers/http/controllers/paragraph/errors.go @@ -0,0 +1,31 @@ +package paragraph + +import ( + "github.com/labstack/echo/v4" + "net/http" +) + +// Возможные ошибки при работе с книгами. + +var ( + ErrorValidationFailed = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка валидации полей ввода! Проверьте введенные данные и попробуйте снова.", + ) + ErrorParagraphNotFound = echo.NewHTTPError( + http.StatusNotFound, + "Параграф не найдена", + ) + ErrorParagraphNotCreated = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка создания параграфа. Параграф с такими параметрами уже существует.", + ) + ErrorDeleteParagraph = echo.NewHTTPError( + http.StatusBadRequest, + "Ошибка удаления параграфа", + ) + ErrorUnknown = echo.NewHTTPError( + http.StatusInternalServerError, + "Неизвестная ошибка", + ) +) diff --git a/bookback/internal/servers/http/controllers/paragraph/models.go b/bookback/internal/servers/http/controllers/paragraph/models.go new file mode 100644 index 0000000..6c57dd2 --- /dev/null +++ b/bookback/internal/servers/http/controllers/paragraph/models.go @@ -0,0 +1,21 @@ +package paragraph + +import "github.com/SShlykov/zeitment/bookback/internal/models" + +type Options struct { +} + +type requestModel struct { + Options Options `json:"options"` + Paragraph *models.Paragraph `json:"paragraph"` +} + +type responseSingleModel struct { + Paragraph *models.Paragraph `json:"paragraph"` + Status string `json:"status"` +} + +type responseListModel struct { + Paragraphs []models.Paragraph `json:"paragraphs"` + Status string `json:"status"` +} diff --git a/bookback/internal/servers/http/controllers/paragraph/routes.go b/bookback/internal/servers/http/controllers/paragraph/routes.go new file mode 100644 index 0000000..e644b98 --- /dev/null +++ b/bookback/internal/servers/http/controllers/paragraph/routes.go @@ -0,0 +1,30 @@ +package paragraph + +import ( + "context" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/httpmiddlewares" + paragraphrepo "github.com/SShlykov/zeitment/bookback/internal/services/paragraph" + "github.com/SShlykov/zeitment/bookback/pkg/db" + "github.com/labstack/echo/v4" + "log/slog" +) + +func SetParagraphController(e *echo.Echo, database db.Client, metrics metrics.Metrics, logger *slog.Logger, ctx context.Context) { + service := paragraphrepo.NewService(paragraphrepo.NewRepository(database)) + controller := NewController(service, metrics, logger, ctx) + + controller.RegisterRoutes(e) +} + +func (p *Controller) RegisterRoutes(e *echo.Echo) { + group := e.Group(PathPrefix) + group.Use(httpmiddlewares.MetricsLogger(p.Metrics)) + + group.GET("", p.ListParagraphs) + group.POST("", p.CreateParagraph) + group.GET("/:id", p.GetParagraphByID) + group.PUT("/:id", p.UpdateParagraph) + group.DELETE("/:id", p.DeleteParagraph) + group.GET("/pages/:id", p.GetParagraphsByPageID) +} diff --git a/bookback/internal/servers/http/controllers/swagger/controller.go b/bookback/internal/servers/http/controllers/swagger/controller.go new file mode 100644 index 0000000..74f598b --- /dev/null +++ b/bookback/internal/servers/http/controllers/swagger/controller.go @@ -0,0 +1,12 @@ +package swagger + +import ( + "github.com/labstack/echo/v4" + echoSwagger "github.com/swaggo/echo-swagger" +) + +func SetSwagger(e *echo.Echo, swaggerEnabled bool) { + if swaggerEnabled { + e.GET("/swagger/*", echoSwagger.WrapHandler) + } +} diff --git a/bookback/internal/servers/http/httpmiddlewares/circuitbreaker.go b/bookback/internal/servers/http/httpmiddlewares/circuitbreaker.go new file mode 100644 index 0000000..b105dd9 --- /dev/null +++ b/bookback/internal/servers/http/httpmiddlewares/circuitbreaker.go @@ -0,0 +1,28 @@ +package httpmiddlewares + +import ( + "errors" + "github.com/SShlykov/zeitment/bookback/internal/servers/http/circuitbreaker" + "github.com/labstack/echo/v4" + "net/http" +) + +func CreateCircuitBreakerMiddleware(cb *circuitbreaker.CircuitBreaker) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + err := cb.Execute(func() error { + return next(c) + }) + + if err != nil { + if errors.Is(err, circuitbreaker.ErrorCb) { + return c.JSON(http.StatusUnavailableForLegalReasons, + map[string]string{"error": "Server is overloaded, please try again later.", "status": "error"}) + } + return err + } + + return nil + } + } +} diff --git a/bookback/internal/servers/http/httpmiddlewares/cors.go b/bookback/internal/servers/http/httpmiddlewares/cors.go new file mode 100644 index 0000000..c1d6017 --- /dev/null +++ b/bookback/internal/servers/http/httpmiddlewares/cors.go @@ -0,0 +1,28 @@ +package httpmiddlewares + +import ( + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "net/http" +) + +func CORS() echo.MiddlewareFunc { + return middleware.CORSWithConfig(middleware.CORSConfig{ + AllowCredentials: true, + UnsafeWildcardOriginWithAllowCredentials: true, + AllowOrigins: []string{"*"}, + AllowHeaders: []string{ + echo.HeaderAccessControlAllowHeaders, + echo.HeaderContentType, + echo.HeaderContentLength, + echo.HeaderAcceptEncoding, + }, + AllowMethods: []string{ + http.MethodGet, + http.MethodPost, + http.MethodPut, + http.MethodDelete, + }, + MaxAge: 86400, + }) +} diff --git a/bookback/internal/servers/http/httpmiddlewares/httplogger.go b/bookback/internal/servers/http/httpmiddlewares/httplogger.go new file mode 100644 index 0000000..6ddb637 --- /dev/null +++ b/bookback/internal/servers/http/httpmiddlewares/httplogger.go @@ -0,0 +1,32 @@ +package httpmiddlewares + +import ( + "context" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "log/slog" +) + +func LoggerConfiguration(logger *slog.Logger) echo.MiddlewareFunc { + return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ + LogStatus: true, + LogURI: true, + LogError: true, + HandleError: true, + LogValuesFunc: func(_ echo.Context, v middleware.RequestLoggerValues) error { + if v.Error == nil { + logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + ) + } else { + logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR", + slog.String("uri", v.URI), + slog.Int("status", v.Status), + slog.String("err", v.Error.Error()), + ) + } + return nil + }, + }) +} diff --git a/bookback/internal/servers/http/httpmiddlewares/middleware.go b/bookback/internal/servers/http/httpmiddlewares/middleware.go new file mode 100644 index 0000000..f5fad9f --- /dev/null +++ b/bookback/internal/servers/http/httpmiddlewares/middleware.go @@ -0,0 +1,29 @@ +package httpmiddlewares + +import ( + "fmt" + "github.com/SShlykov/zeitment/bookback/internal/metrics" + "github.com/labstack/echo/v4" + "time" +) + +func MetricsLogger(metrics metrics.Metrics) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + start := time.Now() + + err := next(c) + + duration := time.Since(start) + + req, res := c.Request(), c.Response() + + // Метрики + status := fmt.Sprintf("%d", res.Status) + metrics.IncCounter("http_requests_total", "method", req.Method, "path", req.URL.Path, "status", status) + metrics.ObserveHistogram("http_request_duration_seconds", duration.Seconds(), "method", req.Method, "path", req.URL.Path) + + return err + } + } +} diff --git a/bookback/internal/servers/http/router/router.go b/bookback/internal/servers/http/router/router.go deleted file mode 100644 index 5553f33..0000000 --- a/bookback/internal/servers/http/router/router.go +++ /dev/null @@ -1,83 +0,0 @@ -package router - -import ( - "context" - _ "github.com/SShlykov/zeitment/bookback/docs" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/book" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/bookevents" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/chapter" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/health" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/mapvariables" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/page" - "github.com/SShlykov/zeitment/bookback/internal/servers/http/controllers/paragraph" - bookrepo "github.com/SShlykov/zeitment/bookback/internal/services/book" - bookeventsrepo "github.com/SShlykov/zeitment/bookback/internal/services/bookevents" - chapterrepo "github.com/SShlykov/zeitment/bookback/internal/services/chapter" - mapvariablesrepo "github.com/SShlykov/zeitment/bookback/internal/services/mapvariables" - pagerepo "github.com/SShlykov/zeitment/bookback/internal/services/page" - paragraphrepo "github.com/SShlykov/zeitment/bookback/internal/services/paragraph" - "github.com/SShlykov/zeitment/bookback/pkg/db" - "github.com/labstack/echo/v4" - "github.com/labstack/echo/v4/middleware" - echoSwagger "github.com/swaggo/echo-swagger" - "net/http" -) - -func SetCORSConfig(e *echo.Echo, corsEnabled bool) { - if corsEnabled { - e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ - AllowCredentials: true, - UnsafeWildcardOriginWithAllowCredentials: true, - AllowOrigins: []string{"*"}, - AllowHeaders: []string{ - echo.HeaderAccessControlAllowHeaders, - echo.HeaderContentType, - echo.HeaderContentLength, - echo.HeaderAcceptEncoding, - }, - AllowMethods: []string{ - http.MethodGet, - http.MethodPost, - http.MethodPut, - http.MethodDelete, - }, - MaxAge: 86400, - })) - } -} - -func SetHealthController(e *echo.Echo, ctx context.Context) { - health.NewController().RegisterRoutes(e, ctx) -} - -func SetBookController(e *echo.Echo, database db.Client, ctx context.Context) { - book.NewController(bookrepo.NewService(bookrepo.NewRepository(database))).RegisterRoutes(e, ctx) -} - -func SetPageController(e *echo.Echo, database db.Client, ctx context.Context) { - page.NewController(pagerepo.NewService(pagerepo.NewRepository(database))).RegisterRoutes(e, ctx) -} - -func SetChapterController(e *echo.Echo, database db.Client, ctx context.Context) { - chapterRepo := chapterrepo.NewRepository(database) - bookRepo := bookrepo.NewRepository(database) - chapter.NewController(chapterrepo.NewService(chapterRepo, bookRepo)).RegisterRoutes(e, ctx) -} - -func SetParagraphController(e *echo.Echo, database db.Client, ctx context.Context) { - paragraph.NewController(paragraphrepo.NewService(paragraphrepo.NewRepository(database))).RegisterRoutes(e, ctx) -} - -func SetMapVariablesController(e *echo.Echo, database db.Client, ctx context.Context) { - mapvariables.NewController(mapvariablesrepo.NewService(mapvariablesrepo.NewRepository(database))).RegisterRoutes(e, ctx) -} - -func SetBookEventController(e *echo.Echo, database db.Client, ctx context.Context) { - bookevents.NewController(bookeventsrepo.NewService(bookeventsrepo.NewRepository(database))).RegisterRoutes(e, ctx) -} - -func SetSwagger(e *echo.Echo, swaggerEnabled bool) { - if swaggerEnabled { - e.GET("/swagger/*", echoSwagger.WrapHandler) - } -} diff --git a/bookback/internal/servers/http/server.go b/bookback/internal/servers/http/server.go deleted file mode 100644 index c441750..0000000 --- a/bookback/internal/servers/http/server.go +++ /dev/null @@ -1,5 +0,0 @@ -package http - -func Start() { - -} diff --git a/bookback/internal/services/book/repository_test.go b/bookback/internal/services/book/repository_test.go index 1226bca..e7c925a 100644 --- a/bookback/internal/services/book/repository_test.go +++ b/bookback/internal/services/book/repository_test.go @@ -1,8 +1,8 @@ package book import ( - "github.com/SShlykov/zeitment/bookback/internal/mocks" "github.com/SShlykov/zeitment/bookback/internal/models" + mocks2 "github.com/SShlykov/zeitment/bookback/tests/mocks" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -26,16 +26,16 @@ func newTestBook() *models.Book { } } -func rowFromBook(book *models.Book) *mocks.ScanResult { - return mocks.NewScanResult([]interface{}{book.ID, book.CreatedAt, book.UpdatedAt, book.DeletedAt, book.Owner, //nolint:gofmt +func rowFromBook(book *models.Book) *mocks2.ScanResult { + return mocks2.NewScanResult([]interface{}{book.ID, book.CreatedAt, book.UpdatedAt, book.DeletedAt, book.Owner, //nolint:gofmt book.Title, book.Author, book.Description, book.IsPublic, book.Publication, book.ImageLink, book.MapLink, book.MapParamsID, book.Variables, }) } func inits(ctrl *gomock.Controller) (Repository, *models.Book) { - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testBook := newTestBook() row := rowFromBook(testBook) @@ -63,12 +63,12 @@ func TestRepository_Create(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testBook := &models.Book{} - row := mocks.NewScanResult([]interface{}{faker.UUIDHyphenated()}) + row := mocks2.NewScanResult([]interface{}{faker.UUIDHyphenated()}) client.EXPECT().DB().Return(db) db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) diff --git a/bookback/internal/services/chapter/repository_test.go b/bookback/internal/services/chapter/repository_test.go index 7a19213..e9b7320 100644 --- a/bookback/internal/services/chapter/repository_test.go +++ b/bookback/internal/services/chapter/repository_test.go @@ -1,8 +1,8 @@ package chapter import ( - "github.com/SShlykov/zeitment/bookback/internal/mocks" "github.com/SShlykov/zeitment/bookback/internal/models" + mocks2 "github.com/SShlykov/zeitment/bookback/tests/mocks" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -22,16 +22,16 @@ func newTestChapter() *models.Chapter { } } -func rowFromChapter(chapter *models.Chapter) *mocks.ScanResult { - return mocks.NewScanResult([]interface{}{chapter.ID, chapter.CreatedAt, chapter.UpdatedAt, chapter.DeletedAt, +func rowFromChapter(chapter *models.Chapter) *mocks2.ScanResult { + return mocks2.NewScanResult([]interface{}{chapter.ID, chapter.CreatedAt, chapter.UpdatedAt, chapter.DeletedAt, chapter.Title, chapter.Number, chapter.Text, chapter.BookID, chapter.IsPublic, chapter.MapLink, chapter.MapParamsID, }) } func initChapters(ctrl *gomock.Controller) (Repository, *models.Chapter) { - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testChapter := newTestChapter() row := rowFromChapter(testChapter) @@ -59,12 +59,12 @@ func TestRepository_Create(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testChapter := &models.Chapter{} - row := mocks.NewScanResult([]interface{}{faker.UUIDHyphenated()}) + row := mocks2.NewScanResult([]interface{}{faker.UUIDHyphenated()}) client.EXPECT().DB().Return(db) db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) diff --git a/bookback/internal/services/mapvariables/repository.go b/bookback/internal/services/mapvariables/repository.go index 2b6bb74..6cc5dcc 100644 --- a/bookback/internal/services/mapvariables/repository.go +++ b/bookback/internal/services/mapvariables/repository.go @@ -11,7 +11,8 @@ import ( const ( tableName = "map_variables" columnID = "id" - columnInsertedAt = "inserted_at" + columnCreatedAt = "created_at" + columnUpdatedAt = "updated_at" columnBookID = "book_id" columnChapterID = "chapter_id" columnPageID = "page_id" @@ -38,37 +39,39 @@ type Repository interface { GetByChapterID(ctx context.Context, chapterID string) ([]models.MapVariable, error) GetByPageID(ctx context.Context, pageID string) ([]models.MapVariable, error) GetByParagraphID(ctx context.Context, paragraphID string) ([]models.MapVariable, error) - - GetByMapLinkAndBookID(ctx context.Context, mapLink, bookID string) ([]models.MapVariable, error) - GetByMapLinkAndChapterID(ctx context.Context, mapLink, chapterID string) ([]models.MapVariable, error) - GetByMapLinkAndPageID(ctx context.Context, mapLink, pageID string) ([]models.MapVariable, error) - GetByMapLinkAndParagraphID(ctx context.Context, mapLink, paragraphID string) ([]models.MapVariable, error) } type repository struct { db db.Client } +func NewRepository(database db.Client) Repository { + return &repository{database} +} + func allItems() string { - cols := []string{columnID, columnInsertedAt, columnBookID, columnChapterID, columnPageID, + cols := []string{columnID, columnCreatedAt, columnUpdatedAt, columnBookID, columnChapterID, columnPageID, columnParagraphID, columnMapLink, columnLat, columnLng, columnZoom, columnDate, columnDescription, columnLink, columnLinkText, columnLinkType, columnLinkImage, columnImage} return strings.Join(cols, ", ") } -func NewRepository(database db.Client) Repository { - return &repository{database} +func insertItems() string { + cols := []string{columnBookID, columnChapterID, columnPageID, columnParagraphID, columnMapLink, + columnLat, columnLng, columnZoom, columnDate, columnDescription, columnLink, columnLinkText, + columnLinkType, columnLinkImage, columnImage} + + return strings.Join(cols, ", ") } func (r *repository) Create(ctx context.Context, variable *models.MapVariable) (string, error) { - query := `INSERT INTO` + " " + tableName + ` (` + allItems() + - `) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id` + query := `INSERT INTO` + " " + tableName + ` (` + insertItems() + + `) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id` - args := []interface{}{variable.ID, variable.CreatedAt, variable.BookID, variable.ChapterID, //nolint:gofmt - variable.PageID, variable.ParagraphID, variable.MapLink, variable.Lat, variable.Lng, - variable.Zoom, variable.Date, variable.Description, variable.Link, variable.LinkText, - variable.LinkType, variable.LinkImage, variable.Image} + args := []interface{}{variable.BookID, variable.ChapterID, variable.PageID, variable.ParagraphID, variable.MapLink, //nolint:gofmt + variable.Lat, variable.Lng, variable.Zoom, variable.Date, variable.Description, + variable.Link, variable.LinkText, variable.LinkType, variable.LinkImage, variable.Image} q := db.Query{Name: "MapVariableRepository.Insert", Raw: query} @@ -91,18 +94,15 @@ func (r *repository) FindByID(ctx context.Context, id string) (*models.MapVariab } func (r *repository) Update(ctx context.Context, id string, variable *models.MapVariable) (*models.MapVariable, error) { - query := `UPDATE ` + tableName + ` SET ` + - columnInsertedAt + ` = $1, ` + columnBookID + ` = $2, ` + columnChapterID + ` = $3, ` + - columnPageID + ` = $4, ` + columnParagraphID + ` = $5, ` + columnMapLink + ` = $6, ` + - columnLat + ` = $7, ` + columnLng + ` = $8, ` + columnZoom + ` = $9, ` + - columnDate + ` = $10, ` + columnDescription + ` = $11, ` + columnLink + ` = $12, ` + - columnLinkText + ` = $13, ` + columnLinkType + ` = $14, ` + columnLinkImage + ` = $15, ` + - columnImage + ` = $16 WHERE ` + columnID + ` = $17 RETURNING ` + allItems() - - args := []interface{}{variable.CreatedAt, variable.BookID, variable.ChapterID, variable.PageID, //nolint:gofmt - variable.ParagraphID, variable.MapLink, variable.Lat, variable.Lng, variable.Zoom, - variable.Date, variable.Description, variable.Link, variable.LinkText, variable.LinkType, - variable.LinkImage, variable.Image, id} + query := `UPDATE ` + tableName + ` SET ` + services.ParamsToQuery(", ", + columnBookID, columnChapterID, columnPageID, columnParagraphID, columnMapLink, + columnLat, columnLng, columnZoom, columnDate, columnDescription, columnLink, columnLinkText, + columnLinkType, columnLinkImage, columnImage) + + ` WHERE ` + columnID + ` = $16 RETURNING ` + allItems() + + args := []interface{}{variable.BookID, variable.ChapterID, variable.PageID, variable.ParagraphID, variable.MapLink, //nolint:gofmt + variable.Lat, variable.Lng, variable.Zoom, variable.Date, variable.Description, + variable.Link, variable.LinkText, variable.LinkType, variable.LinkImage, variable.Image, id} q := db.Query{Name: "MapVariableRepository.Update", Raw: query} @@ -112,7 +112,7 @@ func (r *repository) Update(ctx context.Context, id string, variable *models.Map } func (r *repository) Delete(ctx context.Context, id string) (*models.MapVariable, error) { - query := `DELETE FROM` + " " + tableName + ` WHERE ` + columnID + ` = $1 RETURNING ` + allItems() + query := services.DeleteQuery(tableName, columnID) + ` RETURNING ` + allItems() q := db.Query{Name: "MapVariableRepository.Delete", Raw: query} row := r.db.DB().QueryRowContext(ctx, q, id) @@ -166,51 +166,3 @@ func (r *repository) GetByParagraphID(ctx context.Context, paragraphID string) ( return readList(rows) } - -func (r *repository) GetByMapLinkAndBookID(ctx context.Context, mapLink, bookID string) ([]models.MapVariable, error) { - query := services.SelectWhere(allItems, tableName, columnMapLink, columnBookID) - - q := db.Query{Name: "MapVariableRepository.GetByMapLinkAndBookID", Raw: query} - rows, err := r.db.DB().QueryContext(ctx, q, mapLink, bookID) - if err != nil { - return nil, err - } - - return readList(rows) -} - -func (r *repository) GetByMapLinkAndChapterID(ctx context.Context, mapLink, chapterID string) ([]models.MapVariable, error) { - query := services.SelectWhere(allItems, tableName, columnMapLink, columnChapterID) - - q := db.Query{Name: "MapVariableRepository.GetByMapLinkAndChapterID", Raw: query} - rows, err := r.db.DB().QueryContext(ctx, q, mapLink, chapterID) - if err != nil { - return nil, err - } - - return readList(rows) -} - -func (r *repository) GetByMapLinkAndPageID(ctx context.Context, mapLink, pageID string) ([]models.MapVariable, error) { - query := services.SelectWhere(allItems, tableName, columnMapLink, columnPageID) - - q := db.Query{Name: "MapVariableRepository.GetByMapLinkAndPageID", Raw: query} - rows, err := r.db.DB().QueryContext(ctx, q, mapLink, pageID) - if err != nil { - return nil, err - } - - return readList(rows) -} - -func (r *repository) GetByMapLinkAndParagraphID(ctx context.Context, mapLink, paragraphID string) ([]models.MapVariable, error) { - query := services.SelectWhere(allItems, tableName, columnMapLink, columnParagraphID) - - q := db.Query{Name: "MapVariableRepository.GetByMapLinkAndParagraphID", Raw: query} - rows, err := r.db.DB().QueryContext(ctx, q, mapLink, paragraphID) - if err != nil { - return nil, err - } - - return readList(rows) -} diff --git a/bookback/internal/services/mapvariables/service.go b/bookback/internal/services/mapvariables/service.go index 9791581..cedbe76 100644 --- a/bookback/internal/services/mapvariables/service.go +++ b/bookback/internal/services/mapvariables/service.go @@ -14,11 +14,6 @@ type Service interface { GetMapVariablesByChapterID(ctx context.Context, chapterID string) ([]models.MapVariable, error) GetMapVariablesByPageID(ctx context.Context, pageID string) ([]models.MapVariable, error) GetMapVariablesByParagraphID(ctx context.Context, paragraphID string) ([]models.MapVariable, error) - - GetMapVariablesByMapLinkAndBookID(ctx context.Context, mapLink, bookID string) ([]models.MapVariable, error) - GetMapVariablesByMapLinkAndChapterID(ctx context.Context, mapLink, chapterID string) ([]models.MapVariable, error) - GetMapVariablesByMapLinkAndPageID(ctx context.Context, mapLink, pageID string) ([]models.MapVariable, error) - GetMapVariablesByMapLinkAndParagraphID(ctx context.Context, mapLink, paragraphID string) ([]models.MapVariable, error) } type service struct { @@ -66,19 +61,3 @@ func (s *service) GetMapVariablesByPageID(ctx context.Context, pageID string) ([ func (s *service) GetMapVariablesByParagraphID(ctx context.Context, paragraphID string) ([]models.MapVariable, error) { return s.repo.GetByParagraphID(ctx, paragraphID) } - -func (s *service) GetMapVariablesByMapLinkAndBookID(ctx context.Context, mapLink, bookID string) ([]models.MapVariable, error) { - return s.repo.GetByMapLinkAndBookID(ctx, mapLink, bookID) -} - -func (s *service) GetMapVariablesByMapLinkAndChapterID(ctx context.Context, mapLink, chapterID string) ([]models.MapVariable, error) { - return s.repo.GetByMapLinkAndChapterID(ctx, mapLink, chapterID) -} - -func (s *service) GetMapVariablesByMapLinkAndPageID(ctx context.Context, mapLink, pageID string) ([]models.MapVariable, error) { - return s.repo.GetByMapLinkAndPageID(ctx, mapLink, pageID) -} - -func (s *service) GetMapVariablesByMapLinkAndParagraphID(ctx context.Context, mapLink, paragraphID string) ([]models.MapVariable, error) { - return s.repo.GetByMapLinkAndParagraphID(ctx, mapLink, paragraphID) -} diff --git a/bookback/internal/services/mapvariables/utils.go b/bookback/internal/services/mapvariables/utils.go index 34ab2f6..8dac089 100644 --- a/bookback/internal/services/mapvariables/utils.go +++ b/bookback/internal/services/mapvariables/utils.go @@ -24,7 +24,8 @@ func readList(rows pgx.Rows) ([]models.MapVariable, error) { func readItem(row pgx.Row) (*models.MapVariable, error) { var variable models.MapVariable - if err := row.Scan(&variable.ID, &variable.CreatedAt, &variable.BookID, + + if err := row.Scan(&variable.ID, &variable.CreatedAt, &variable.UpdatedAt, &variable.BookID, &variable.ChapterID, &variable.PageID, &variable.ParagraphID, &variable.MapLink, &variable.Lat, &variable.Lng, &variable.Zoom, &variable.Date, &variable.Description, &variable.Link, &variable.LinkText, &variable.LinkType, &variable.LinkImage, &variable.Image); err != nil { diff --git a/bookback/internal/services/page/repository_test.go b/bookback/internal/services/page/repository_test.go index 3c0059c..a939b75 100644 --- a/bookback/internal/services/page/repository_test.go +++ b/bookback/internal/services/page/repository_test.go @@ -1,8 +1,8 @@ package page import ( - "github.com/SShlykov/zeitment/bookback/internal/mocks" "github.com/SShlykov/zeitment/bookback/internal/models" + mocks2 "github.com/SShlykov/zeitment/bookback/tests/mocks" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -23,14 +23,14 @@ func newTestPage() *models.Page { } } -func rowFromPage(page *models.Page) *mocks.ScanResult { - return mocks.NewScanResult([]interface{}{page.ID, page.CreatedAt, page.UpdatedAt, page.DeletedAt, +func rowFromPage(page *models.Page) *mocks2.ScanResult { + return mocks2.NewScanResult([]interface{}{page.ID, page.CreatedAt, page.UpdatedAt, page.DeletedAt, page.Title, page.Text, page.ChapterID, page.IsPublic, page.MapParamsID}) } func initPages(ctrl *gomock.Controller) (Repository, *models.Page) { - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testPage := newTestPage() row := rowFromPage(testPage) @@ -59,12 +59,12 @@ func TestRepository_Create(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testPage := &models.Page{} - row := mocks.NewScanResult([]interface{}{faker.UUIDHyphenated()}) + row := mocks2.NewScanResult([]interface{}{faker.UUIDHyphenated()}) client.EXPECT().DB().Return(db) db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) diff --git a/bookback/internal/services/paragraph/repository_test.go b/bookback/internal/services/paragraph/repository_test.go index 4d34199..8ea0824 100644 --- a/bookback/internal/services/paragraph/repository_test.go +++ b/bookback/internal/services/paragraph/repository_test.go @@ -1,8 +1,8 @@ package paragraph import ( - "github.com/SShlykov/zeitment/bookback/internal/mocks" "github.com/SShlykov/zeitment/bookback/internal/models" + mocks2 "github.com/SShlykov/zeitment/bookback/tests/mocks" "github.com/go-faker/faker/v4" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -23,14 +23,14 @@ func newTestParagraph() *models.Paragraph { } } -func rowFromParagraph(paragraph *models.Paragraph) *mocks.ScanResult { - return mocks.NewScanResult([]interface{}{paragraph.ID, paragraph.CreatedAt, paragraph.UpdatedAt, paragraph.DeletedAt, +func rowFromParagraph(paragraph *models.Paragraph) *mocks2.ScanResult { + return mocks2.NewScanResult([]interface{}{paragraph.ID, paragraph.CreatedAt, paragraph.UpdatedAt, paragraph.DeletedAt, paragraph.Title, paragraph.Text, paragraph.Type, paragraph.IsPublic, paragraph.PageID}) } func initParagraphs(ctrl *gomock.Controller) (Repository, *models.Paragraph) { - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testParagraph := newTestParagraph() row := rowFromParagraph(testParagraph) @@ -59,12 +59,12 @@ func TestRepository_Create(t *testing.T) { t.Parallel() ctrl := gomock.NewController(t) defer ctrl.Finish() - client := mocks.NewMockClient(ctrl) - db := mocks.NewMockDB(ctrl) + client := mocks2.NewMockClient(ctrl) + db := mocks2.NewMockDB(ctrl) testParagraph := &models.Paragraph{} - row := mocks.NewScanResult([]interface{}{faker.UUIDHyphenated()}) + row := mocks2.NewScanResult([]interface{}{faker.UUIDHyphenated()}) client.EXPECT().DB().Return(db) db.EXPECT().QueryRowContext(gomock.Any(), gomock.Any(), gomock.Any()).Return(row) diff --git a/bookback/migrations/20240225152254_create_map_variables.sql b/bookback/migrations/20240225152254_create_map_variables.sql index e7137b1..a72c1e3 100644 --- a/bookback/migrations/20240225152254_create_map_variables.sql +++ b/bookback/migrations/20240225152254_create_map_variables.sql @@ -10,8 +10,8 @@ CREATE TABLE IF NOT EXISTS map_variables ( map_link TEXT NOT NULL, lat DOUBLE PRECISION NOT NULL, lng DOUBLE PRECISION NOT NULL, - zoom INTEGER NOT NULL, - date TIMESTAMP WITH TIME ZONE NOT NULL, + zoom INTEGER, + date varchar(255), description TEXT, link TEXT, link_text TEXT, diff --git a/bookback/migrations/20240303075249_update_map_events_add_updated_at.sql b/bookback/migrations/20240303075249_update_map_events_add_updated_at.sql new file mode 100644 index 0000000..e3d79b6 --- /dev/null +++ b/bookback/migrations/20240303075249_update_map_events_add_updated_at.sql @@ -0,0 +1,9 @@ +-- +goose Up +-- +goose StatementBegin +ALTER TABLE map_variables ADD COLUMN updated_at TIMESTAMP; +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE map_variables DROP COLUMN updated_at; +-- +goose StatementEnd diff --git a/bookback/pkg/logger/handler.go b/bookback/pkg/logger/handler.go index 36f9c6f..dd01a76 100644 --- a/bookback/pkg/logger/handler.go +++ b/bookback/pkg/logger/handler.go @@ -7,6 +7,7 @@ import ( "io" "log" "log/slog" + "strings" ) type PrettyHandlerOptions struct { @@ -39,23 +40,21 @@ func (h *PrettyHandler) Handle(_ context.Context, r slog.Record) error { return true }) - b, err := json.MarshalIndent(fields, "", " ") + b, err := json.MarshalIndent(fields, "", "") if err != nil { return err } + opts := strings.Replace(string(b), "\n", " ", -1) timeStr := r.Time.Format("[15:05:05.000]") msg := color.CyanString(r.Message) - h.l.Println(timeStr, level, msg, color.WhiteString(string(b))) + h.l.Println(timeStr, level, msg, color.WhiteString(opts)) return nil } -func NewPrettyHandler( - out io.Writer, - opts PrettyHandlerOptions, -) *PrettyHandler { +func NewPrettyHandler(out io.Writer, opts PrettyHandlerOptions) *PrettyHandler { h := &PrettyHandler{ Handler: slog.NewJSONHandler(out, &opts.SlogOpts), l: log.New(out, "", 0), diff --git a/bookback/internal/mocks/db.go b/bookback/tests/mocks/db.go similarity index 100% rename from bookback/internal/mocks/db.go rename to bookback/tests/mocks/db.go diff --git a/bookback/internal/mocks/scanresult.go b/bookback/tests/mocks/scanresult.go similarity index 100% rename from bookback/internal/mocks/scanresult.go rename to bookback/tests/mocks/scanresult.go