You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
202 lines
6.6 KiB
Go
202 lines
6.6 KiB
Go
package http
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
|
|
"github.com/gofiber/fiber/v3"
|
|
|
|
appq "knowfoolery/backend/services/question-bank-service/internal/application/question"
|
|
domain "knowfoolery/backend/services/question-bank-service/internal/domain/question"
|
|
sharederrors "knowfoolery/backend/shared/domain/errors"
|
|
"knowfoolery/backend/shared/infra/observability/logging"
|
|
sharedmetrics "knowfoolery/backend/shared/infra/observability/metrics"
|
|
"knowfoolery/backend/shared/infra/utils/httputil"
|
|
"knowfoolery/backend/shared/infra/utils/validation"
|
|
)
|
|
|
|
// Handler implements HTTP endpoint handlers.
|
|
type Handler struct {
|
|
service *appq.Service
|
|
validator *validation.Validator
|
|
logger *logging.Logger
|
|
metrics *sharedmetrics.Metrics
|
|
bulkMaxItems int
|
|
}
|
|
|
|
// NewHandler creates a new HTTP handler set.
|
|
func NewHandler(service *appq.Service, validator *validation.Validator, logger *logging.Logger,
|
|
metrics *sharedmetrics.Metrics, bulkMaxItems int) *Handler {
|
|
return &Handler{service: service, validator: validator, logger: logger, metrics: metrics, bulkMaxItems: bulkMaxItems}
|
|
}
|
|
|
|
// PostRandomQuestion handles POST /questions/random.
|
|
func (h *Handler) PostRandomQuestion(c fiber.Ctx) error {
|
|
var req RandomQuestionRequest
|
|
if err := c.Bind().Body(&req); err != nil {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInvalidInput, "invalid request body", err))
|
|
}
|
|
|
|
if req.Difficulty != "" && !domain.IsValidDifficulty(req.Difficulty) {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeValidationFailed, "invalid difficulty", nil))
|
|
}
|
|
|
|
result, err := h.service.GetRandomQuestion(c.Context(), appq.RandomQuestionRequest{
|
|
ExcludeQuestionIDs: req.ExcludeQuestionIDs,
|
|
Theme: req.Theme,
|
|
Difficulty: req.Difficulty,
|
|
})
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("POST", "/questions/random", fiber.StatusOK)
|
|
if h.logger != nil {
|
|
h.logger.Info("question selected")
|
|
}
|
|
return httputil.OK(c, toQuestionResponse(result, false))
|
|
}
|
|
|
|
// GetQuestionByID handles GET /questions/:id.
|
|
func (h *Handler) GetQuestionByID(c fiber.Ctx) error {
|
|
id := c.Params("id")
|
|
result, err := h.service.GetQuestionByID(c.Context(), id)
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("GET", "/questions/{id}", fiber.StatusOK)
|
|
return httputil.OK(c, toQuestionResponse(result, false))
|
|
}
|
|
|
|
// PostValidateAnswer handles POST /questions/:id/validate-answer.
|
|
func (h *Handler) PostValidateAnswer(c fiber.Ctx) error {
|
|
var req ValidateAnswerRequest
|
|
if err := c.Bind().Body(&req); err != nil {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInvalidInput, "invalid request body", err))
|
|
}
|
|
if err := h.validator.Validate(req); err != nil {
|
|
return httputil.SendError(c, err)
|
|
}
|
|
|
|
result, err := h.service.ValidateAnswerByQuestionID(c.Context(), c.Params("id"), req.Answer)
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("POST", "/questions/{id}/validate-answer", fiber.StatusOK)
|
|
return httputil.OK(c, fiber.Map{
|
|
"matched": result.Matched,
|
|
"score": result.Score,
|
|
"threshold": result.Threshold,
|
|
})
|
|
}
|
|
|
|
// AdminCreateQuestion handles POST /admin/questions.
|
|
func (h *Handler) AdminCreateQuestion(c fiber.Ctx) error {
|
|
var req CreateQuestionRequest
|
|
if err := c.Bind().Body(&req); err != nil {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInvalidInput, "invalid request body", err))
|
|
}
|
|
if err := h.validator.Validate(req); err != nil {
|
|
return httputil.SendError(c, err)
|
|
}
|
|
|
|
result, err := h.service.CreateQuestion(c.Context(), appq.CreateQuestionInput(req))
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("POST", "/admin/questions", fiber.StatusCreated)
|
|
return c.Status(fiber.StatusCreated).JSON(httputil.NewResponse(toQuestionResponse(result, true)))
|
|
}
|
|
|
|
// AdminUpdateQuestion handles PUT /admin/questions/:id.
|
|
func (h *Handler) AdminUpdateQuestion(c fiber.Ctx) error {
|
|
var req UpdateQuestionRequest
|
|
if err := c.Bind().Body(&req); err != nil {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInvalidInput, "invalid request body", err))
|
|
}
|
|
if err := h.validator.Validate(req); err != nil {
|
|
return httputil.SendError(c, err)
|
|
}
|
|
|
|
result, err := h.service.UpdateQuestion(c.Context(), c.Params("id"), appq.UpdateQuestionInput(req))
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("PUT", "/admin/questions/{id}", fiber.StatusOK)
|
|
return httputil.OK(c, toQuestionResponse(result, true))
|
|
}
|
|
|
|
// AdminDeleteQuestion handles DELETE /admin/questions/:id.
|
|
func (h *Handler) AdminDeleteQuestion(c fiber.Ctx) error {
|
|
if err := h.service.DeleteQuestion(c.Context(), c.Params("id")); err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("DELETE", "/admin/questions/{id}", fiber.StatusNoContent)
|
|
return httputil.NoContent(c)
|
|
}
|
|
|
|
// AdminListThemes handles GET /admin/themes.
|
|
func (h *Handler) AdminListThemes(c fiber.Ctx) error {
|
|
themes, err := h.service.ListThemes(c.Context())
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("GET", "/admin/themes", fiber.StatusOK)
|
|
return httputil.OK(c, fiber.Map{"themes": themes})
|
|
}
|
|
|
|
// AdminBulkImport handles POST /admin/questions/bulk.
|
|
func (h *Handler) AdminBulkImport(c fiber.Ctx) error {
|
|
var req BulkImportRequest
|
|
if err := c.Bind().Body(&req); err != nil {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInvalidInput, "invalid request body", err))
|
|
}
|
|
if len(req.Questions) == 0 {
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeValidationFailed, "questions is required", nil))
|
|
}
|
|
|
|
items := make([]appq.BulkImportItem, 0, len(req.Questions))
|
|
for _, q := range req.Questions {
|
|
items = append(items, appq.BulkImportItem{
|
|
Theme: q.Theme,
|
|
Text: q.Text,
|
|
Answer: q.Answer,
|
|
Hint: q.Hint,
|
|
Difficulty: q.Difficulty,
|
|
})
|
|
}
|
|
|
|
result, err := h.service.BulkImport(c.Context(), items, h.bulkMaxItems)
|
|
if err != nil {
|
|
return h.sendMappedError(c, err)
|
|
}
|
|
|
|
h.recordRequestMetric("POST", "/admin/questions/bulk", fiber.StatusOK)
|
|
return httputil.OK(c, result)
|
|
}
|
|
|
|
func (h *Handler) sendMappedError(c fiber.Ctx, err error) error {
|
|
var domainErr *sharederrors.DomainError
|
|
if errors.As(err, &domainErr) {
|
|
return httputil.SendError(c, domainErr)
|
|
}
|
|
return httputil.SendError(c, sharederrors.Wrap(sharederrors.CodeInternal, "internal error", err))
|
|
}
|
|
|
|
func (h *Handler) recordRequestMetric(method, endpoint string, status int) {
|
|
if h.metrics == nil {
|
|
return
|
|
}
|
|
h.metrics.HTTPRequestsTotal.WithLabelValues(method, endpoint, strconv.Itoa(status), h.metricsConfigService()).Inc()
|
|
}
|
|
|
|
func (h *Handler) metricsConfigService() string {
|
|
return "question-bank-service"
|
|
}
|