refactor: restructure internal/ into adapters/, infra/, and app layers
- internal/gemini/ → internal/adapters/openai/ (renamed package to openai) - internal/pexels/ → internal/adapters/pexels/ - internal/config/ → internal/infra/config/ - internal/database/ → internal/infra/database/ - internal/locale/ → internal/infra/locale/ - internal/middleware/ → internal/infra/middleware/ - internal/server/ → internal/infra/server/ All import paths and call sites updated accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/config"
|
"github.com/food-ai/backend/internal/infra/config"
|
||||||
"github.com/food-ai/backend/internal/database"
|
"github.com/food-ai/backend/internal/infra/database"
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -5,22 +5,22 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"github.com/food-ai/backend/internal/auth"
|
||||||
"github.com/food-ai/backend/internal/config"
|
"github.com/food-ai/backend/internal/infra/config"
|
||||||
"github.com/food-ai/backend/internal/diary"
|
"github.com/food-ai/backend/internal/diary"
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
"github.com/food-ai/backend/internal/gemini"
|
"github.com/food-ai/backend/internal/adapters/openai"
|
||||||
"github.com/food-ai/backend/internal/home"
|
"github.com/food-ai/backend/internal/home"
|
||||||
"github.com/food-ai/backend/internal/ingredient"
|
"github.com/food-ai/backend/internal/ingredient"
|
||||||
"github.com/food-ai/backend/internal/menu"
|
"github.com/food-ai/backend/internal/menu"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/pexels"
|
"github.com/food-ai/backend/internal/adapters/pexels"
|
||||||
"github.com/food-ai/backend/internal/product"
|
"github.com/food-ai/backend/internal/product"
|
||||||
"github.com/food-ai/backend/internal/recipe"
|
"github.com/food-ai/backend/internal/recipe"
|
||||||
"github.com/food-ai/backend/internal/recognition"
|
"github.com/food-ai/backend/internal/recognition"
|
||||||
"github.com/food-ai/backend/internal/recommendation"
|
"github.com/food-ai/backend/internal/recommendation"
|
||||||
"github.com/food-ai/backend/internal/savedrecipe"
|
"github.com/food-ai/backend/internal/savedrecipe"
|
||||||
"github.com/food-ai/backend/internal/cuisine"
|
"github.com/food-ai/backend/internal/cuisine"
|
||||||
"github.com/food-ai/backend/internal/server"
|
"github.com/food-ai/backend/internal/infra/server"
|
||||||
"github.com/food-ai/backend/internal/tag"
|
"github.com/food-ai/backend/internal/tag"
|
||||||
"github.com/food-ai/backend/internal/units"
|
"github.com/food-ai/backend/internal/units"
|
||||||
"github.com/food-ai/backend/internal/user"
|
"github.com/food-ai/backend/internal/user"
|
||||||
@@ -38,7 +38,7 @@ type tagListHandler http.HandlerFunc
|
|||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type geminiAPIKey string
|
type openaiAPIKey string
|
||||||
type pexelsAPIKey string
|
type pexelsAPIKey string
|
||||||
type jwtSecret string
|
type jwtSecret string
|
||||||
type jwtAccessDuration time.Duration
|
type jwtAccessDuration time.Duration
|
||||||
@@ -49,8 +49,8 @@ type allowedOrigins []string
|
|||||||
// Config extractors
|
// Config extractors
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func newGeminiAPIKey(appConfig *config.Config) geminiAPIKey {
|
func newOpenAIAPIKey(appConfig *config.Config) openaiAPIKey {
|
||||||
return geminiAPIKey(appConfig.OpenAIAPIKey)
|
return openaiAPIKey(appConfig.OpenAIAPIKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPexelsAPIKey(appConfig *config.Config) pexelsAPIKey {
|
func newPexelsAPIKey(appConfig *config.Config) pexelsAPIKey {
|
||||||
@@ -83,8 +83,8 @@ func newFirebaseCredentialsFile(appConfig *config.Config) string {
|
|||||||
// Constructor wrappers for functions that accept primitive newtypes
|
// Constructor wrappers for functions that accept primitive newtypes
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
func newGeminiClient(key geminiAPIKey) *gemini.Client {
|
func newOpenAIClient(key openaiAPIKey) *openai.Client {
|
||||||
return gemini.NewClient(string(key))
|
return openai.NewClient(string(key))
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPexelsClient(key pexelsAPIKey) *pexels.Client {
|
func newPexelsClient(key pexelsAPIKey) *pexels.Client {
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"github.com/food-ai/backend/internal/auth"
|
||||||
"github.com/food-ai/backend/internal/config"
|
"github.com/food-ai/backend/internal/infra/config"
|
||||||
"github.com/food-ai/backend/internal/diary"
|
"github.com/food-ai/backend/internal/diary"
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
"github.com/food-ai/backend/internal/home"
|
"github.com/food-ai/backend/internal/home"
|
||||||
"github.com/food-ai/backend/internal/ingredient"
|
"github.com/food-ai/backend/internal/ingredient"
|
||||||
"github.com/food-ai/backend/internal/menu"
|
"github.com/food-ai/backend/internal/menu"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/pexels"
|
"github.com/food-ai/backend/internal/adapters/pexels"
|
||||||
"github.com/food-ai/backend/internal/product"
|
"github.com/food-ai/backend/internal/product"
|
||||||
"github.com/food-ai/backend/internal/recipe"
|
"github.com/food-ai/backend/internal/recipe"
|
||||||
"github.com/food-ai/backend/internal/recognition"
|
"github.com/food-ai/backend/internal/recognition"
|
||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, error) {
|
func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, error) {
|
||||||
wire.Build(
|
wire.Build(
|
||||||
// Config extractors
|
// Config extractors
|
||||||
newGeminiAPIKey,
|
newOpenAIAPIKey,
|
||||||
newPexelsAPIKey,
|
newPexelsAPIKey,
|
||||||
newJWTSecret,
|
newJWTSecret,
|
||||||
newJWTAccessDuration,
|
newJWTAccessDuration,
|
||||||
@@ -49,7 +49,7 @@ func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, err
|
|||||||
user.NewHandler,
|
user.NewHandler,
|
||||||
|
|
||||||
// External clients
|
// External clients
|
||||||
newGeminiClient,
|
newOpenAIClient,
|
||||||
newPexelsClient,
|
newPexelsClient,
|
||||||
|
|
||||||
// Ingredient
|
// Ingredient
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"github.com/food-ai/backend/internal/auth"
|
||||||
"github.com/food-ai/backend/internal/config"
|
"github.com/food-ai/backend/internal/infra/config"
|
||||||
"github.com/food-ai/backend/internal/diary"
|
"github.com/food-ai/backend/internal/diary"
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
"github.com/food-ai/backend/internal/home"
|
"github.com/food-ai/backend/internal/home"
|
||||||
@@ -41,8 +41,8 @@ func initRouter(appConfig *config.Config, pool *pgxpool.Pool) (http.Handler, err
|
|||||||
handler := auth.NewHandler(service)
|
handler := auth.NewHandler(service)
|
||||||
userService := user.NewService(repository)
|
userService := user.NewService(repository)
|
||||||
userHandler := user.NewHandler(userService)
|
userHandler := user.NewHandler(userService)
|
||||||
mainGeminiAPIKey := newGeminiAPIKey(appConfig)
|
mainGeminiAPIKey := newOpenAIAPIKey(appConfig)
|
||||||
client := newGeminiClient(mainGeminiAPIKey)
|
client := newOpenAIClient(mainGeminiAPIKey)
|
||||||
mainPexelsAPIKey := newPexelsAPIKey(appConfig)
|
mainPexelsAPIKey := newPexelsAPIKey(appConfig)
|
||||||
pexelsClient := newPexelsClient(mainPexelsAPIKey)
|
pexelsClient := newPexelsClient(mainPexelsAPIKey)
|
||||||
productRepository := product.NewRepository(pool)
|
productRepository := product.NewRepository(pool)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package gemini
|
package openai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gemini
|
package openai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gemini
|
package openai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RecipeGenerator generates recipes using the Gemini AI.
|
// RecipeGenerator generates recipes using the Gemini AI.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gemini
|
package openai
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRequestBodySize = 1 << 20 // 1 MB
|
const maxRequestBodySize = 1 << 20 // 1 MB
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Language reads the Accept-Language request header, resolves the best
|
// Language reads the Accept-Language request header, resolves the best
|
||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/food-ai/backend/internal/ingredient"
|
"github.com/food-ai/backend/internal/ingredient"
|
||||||
"github.com/food-ai/backend/internal/language"
|
"github.com/food-ai/backend/internal/language"
|
||||||
"github.com/food-ai/backend/internal/menu"
|
"github.com/food-ai/backend/internal/menu"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/recipe"
|
"github.com/food-ai/backend/internal/recipe"
|
||||||
"github.com/food-ai/backend/internal/product"
|
"github.com/food-ai/backend/internal/product"
|
||||||
"github.com/food-ai/backend/internal/recognition"
|
"github.com/food-ai/backend/internal/recognition"
|
||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
type languageItem struct {
|
type languageItem struct {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
"github.com/food-ai/backend/internal/gemini"
|
"github.com/food-ai/backend/internal/adapters/openai"
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/user"
|
"github.com/food-ai/backend/internal/user"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
@@ -41,7 +41,7 @@ type RecipeSaver interface {
|
|||||||
// Handler handles menu and shopping-list endpoints.
|
// Handler handles menu and shopping-list endpoints.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
repo *Repository
|
repo *Repository
|
||||||
gemini *gemini.Client
|
openaiClient *openai.Client
|
||||||
pexels PhotoSearcher
|
pexels PhotoSearcher
|
||||||
userLoader UserLoader
|
userLoader UserLoader
|
||||||
productLister ProductLister
|
productLister ProductLister
|
||||||
@@ -51,7 +51,7 @@ type Handler struct {
|
|||||||
// NewHandler creates a new Handler.
|
// NewHandler creates a new Handler.
|
||||||
func NewHandler(
|
func NewHandler(
|
||||||
repo *Repository,
|
repo *Repository,
|
||||||
geminiClient *gemini.Client,
|
openaiClient *openai.Client,
|
||||||
pexels PhotoSearcher,
|
pexels PhotoSearcher,
|
||||||
userLoader UserLoader,
|
userLoader UserLoader,
|
||||||
productLister ProductLister,
|
productLister ProductLister,
|
||||||
@@ -59,7 +59,7 @@ func NewHandler(
|
|||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
gemini: geminiClient,
|
openaiClient: openaiClient,
|
||||||
pexels: pexels,
|
pexels: pexels,
|
||||||
userLoader: userLoader,
|
userLoader: userLoader,
|
||||||
productLister: productLister,
|
productLister: productLister,
|
||||||
@@ -137,7 +137,7 @@ func (h *Handler) GenerateMenu(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate 7-day plan via Gemini.
|
// Generate 7-day plan via Gemini.
|
||||||
days, err := h.gemini.GenerateMenu(r.Context(), menuReq)
|
days, err := h.openaiClient.GenerateMenu(r.Context(), menuReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("generate menu", "user_id", userID, "err", err)
|
slog.Error("generate menu", "user_id", userID, "err", err)
|
||||||
writeError(w, http.StatusServiceUnavailable, "menu generation failed, please try again")
|
writeError(w, http.StatusServiceUnavailable, "menu generation failed, please try again")
|
||||||
@@ -449,8 +449,8 @@ type userPreferences struct {
|
|||||||
Restrictions []string `json:"restrictions"`
|
Restrictions []string `json:"restrictions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMenuRequest(u *user.User, lang string) gemini.MenuRequest {
|
func buildMenuRequest(u *user.User, lang string) openai.MenuRequest {
|
||||||
req := gemini.MenuRequest{DailyCalories: 2000, Lang: lang}
|
req := openai.MenuRequest{DailyCalories: 2000, Lang: lang}
|
||||||
if u.Goal != nil {
|
if u.Goal != nil {
|
||||||
req.UserGoal = *u.Goal
|
req.UserGoal = *u.Goal
|
||||||
}
|
}
|
||||||
@@ -468,7 +468,7 @@ func buildMenuRequest(u *user.User, lang string) gemini.MenuRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// recipeToCreateRequest converts a Gemini recipe to a dish.CreateRequest.
|
// recipeToCreateRequest converts a Gemini recipe to a dish.CreateRequest.
|
||||||
func recipeToCreateRequest(r gemini.Recipe) dish.CreateRequest {
|
func recipeToCreateRequest(r openai.Recipe) dish.CreateRequest {
|
||||||
cr := dish.CreateRequest{
|
cr := dish.CreateRequest{
|
||||||
Name: r.Title,
|
Name: r.Title,
|
||||||
Description: r.Description,
|
Description: r.Description,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/gemini"
|
"github.com/food-ai/backend/internal/adapters/openai"
|
||||||
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/ingredient"
|
"github.com/food-ai/backend/internal/ingredient"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IngredientRepository is the subset of ingredient.Repository used by this handler.
|
// IngredientRepository is the subset of ingredient.Repository used by this handler.
|
||||||
@@ -23,13 +23,13 @@ type IngredientRepository interface {
|
|||||||
|
|
||||||
// Handler handles POST /ai/* recognition endpoints.
|
// Handler handles POST /ai/* recognition endpoints.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
gemini *gemini.Client
|
openaiClient *openai.Client
|
||||||
ingredientRepo IngredientRepository
|
ingredientRepo IngredientRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new Handler.
|
// NewHandler creates a new Handler.
|
||||||
func NewHandler(geminiClient *gemini.Client, repo IngredientRepository) *Handler {
|
func NewHandler(openaiClient *openai.Client, repo IngredientRepository) *Handler {
|
||||||
return &Handler{gemini: geminiClient, ingredientRepo: repo}
|
return &Handler{openaiClient: openaiClient, ingredientRepo: repo}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -60,12 +60,12 @@ type EnrichedItem struct {
|
|||||||
|
|
||||||
// ReceiptResponse is the response for POST /ai/recognize-receipt.
|
// ReceiptResponse is the response for POST /ai/recognize-receipt.
|
||||||
type ReceiptResponse struct {
|
type ReceiptResponse struct {
|
||||||
Items []EnrichedItem `json:"items"`
|
Items []EnrichedItem `json:"items"`
|
||||||
Unrecognized []gemini.UnrecognizedItem `json:"unrecognized"`
|
Unrecognized []openai.UnrecognizedItem `json:"unrecognized"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DishResponse is the response for POST /ai/recognize-dish.
|
// DishResponse is the response for POST /ai/recognize-dish.
|
||||||
type DishResponse = gemini.DishResult
|
type DishResponse = openai.DishResult
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Handlers
|
// Handlers
|
||||||
@@ -83,7 +83,7 @@ func (h *Handler) RecognizeReceipt(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := h.gemini.RecognizeReceipt(r.Context(), req.ImageBase64, req.MimeType)
|
result, err := h.openaiClient.RecognizeReceipt(r.Context(), req.ImageBase64, req.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("recognize receipt", "err", err)
|
slog.Error("recognize receipt", "err", err)
|
||||||
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")
|
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")
|
||||||
@@ -110,13 +110,13 @@ func (h *Handler) RecognizeProducts(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process each image in parallel.
|
// Process each image in parallel.
|
||||||
allItems := make([][]gemini.RecognizedItem, len(req.Images))
|
allItems := make([][]openai.RecognizedItem, len(req.Images))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i, img := range req.Images {
|
for i, img := range req.Images {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(i int, img imageRequest) {
|
go func(i int, img imageRequest) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
items, err := h.gemini.RecognizeProducts(r.Context(), img.ImageBase64, img.MimeType)
|
items, err := h.openaiClient.RecognizeProducts(r.Context(), img.ImageBase64, img.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("recognize products from image", "index", i, "err", err)
|
slog.Warn("recognize products from image", "index", i, "err", err)
|
||||||
return
|
return
|
||||||
@@ -140,7 +140,7 @@ func (h *Handler) RecognizeDish(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := h.gemini.RecognizeDish(r.Context(), req.ImageBase64, req.MimeType)
|
result, err := h.openaiClient.RecognizeDish(r.Context(), req.ImageBase64, req.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("recognize dish", "err", err)
|
slog.Error("recognize dish", "err", err)
|
||||||
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")
|
writeErrorJSON(w, http.StatusServiceUnavailable, "recognition failed, please try again")
|
||||||
@@ -156,7 +156,7 @@ func (h *Handler) RecognizeDish(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// enrichItems matches each recognized item against ingredient_mappings.
|
// enrichItems matches each recognized item against ingredient_mappings.
|
||||||
// Items without a match trigger a Gemini classification call and upsert into the DB.
|
// Items without a match trigger a Gemini classification call and upsert into the DB.
|
||||||
func (h *Handler) enrichItems(ctx context.Context, items []gemini.RecognizedItem) []EnrichedItem {
|
func (h *Handler) enrichItems(ctx context.Context, items []openai.RecognizedItem) []EnrichedItem {
|
||||||
result := make([]EnrichedItem, 0, len(items))
|
result := make([]EnrichedItem, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
enriched := EnrichedItem{
|
enriched := EnrichedItem{
|
||||||
@@ -188,7 +188,7 @@ func (h *Handler) enrichItems(ctx context.Context, items []gemini.RecognizedItem
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No mapping — ask AI to classify and save for future reuse.
|
// No mapping — ask AI to classify and save for future reuse.
|
||||||
classification, err := h.gemini.ClassifyIngredient(ctx, item.Name)
|
classification, err := h.openaiClient.ClassifyIngredient(ctx, item.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("classify unknown ingredient", "name", item.Name, "err", err)
|
slog.Warn("classify unknown ingredient", "name", item.Name, "err", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -208,7 +208,7 @@ func (h *Handler) enrichItems(ctx context.Context, items []gemini.RecognizedItem
|
|||||||
}
|
}
|
||||||
|
|
||||||
// saveClassification upserts an AI-produced ingredient classification into the DB.
|
// saveClassification upserts an AI-produced ingredient classification into the DB.
|
||||||
func (h *Handler) saveClassification(ctx context.Context, c *gemini.IngredientClassification) *ingredient.IngredientMapping {
|
func (h *Handler) saveClassification(ctx context.Context, c *openai.IngredientClassification) *ingredient.IngredientMapping {
|
||||||
if c == nil || c.CanonicalName == "" {
|
if c == nil || c.CanonicalName == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -252,8 +252,8 @@ func (h *Handler) saveClassification(ctx context.Context, c *gemini.IngredientCl
|
|||||||
|
|
||||||
// mergeAndDeduplicate combines results from multiple images.
|
// mergeAndDeduplicate combines results from multiple images.
|
||||||
// Items sharing the same name (case-insensitive) have their quantities summed.
|
// Items sharing the same name (case-insensitive) have their quantities summed.
|
||||||
func mergeAndDeduplicate(batches [][]gemini.RecognizedItem) []gemini.RecognizedItem {
|
func mergeAndDeduplicate(batches [][]openai.RecognizedItem) []openai.RecognizedItem {
|
||||||
seen := make(map[string]*gemini.RecognizedItem)
|
seen := make(map[string]*openai.RecognizedItem)
|
||||||
var order []string
|
var order []string
|
||||||
|
|
||||||
for _, batch := range batches {
|
for _, batch := range batches {
|
||||||
@@ -273,7 +273,7 @@ func mergeAndDeduplicate(batches [][]gemini.RecognizedItem) []gemini.RecognizedI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]gemini.RecognizedItem, 0, len(order))
|
result := make([]openai.RecognizedItem, 0, len(order))
|
||||||
for _, key := range order {
|
for _, key := range order {
|
||||||
result = append(result, *seen[key])
|
result = append(result, *seen[key])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/gemini"
|
"github.com/food-ai/backend/internal/adapters/openai"
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/user"
|
"github.com/food-ai/backend/internal/user"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,16 +37,16 @@ type userPreferences struct {
|
|||||||
|
|
||||||
// Handler handles GET /recommendations.
|
// Handler handles GET /recommendations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
gemini *gemini.Client
|
openaiClient *openai.Client
|
||||||
pexels PhotoSearcher
|
pexels PhotoSearcher
|
||||||
userLoader UserLoader
|
userLoader UserLoader
|
||||||
productLister ProductLister
|
productLister ProductLister
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a new Handler.
|
// NewHandler creates a new Handler.
|
||||||
func NewHandler(geminiClient *gemini.Client, pexels PhotoSearcher, userLoader UserLoader, productLister ProductLister) *Handler {
|
func NewHandler(openaiClient *openai.Client, pexels PhotoSearcher, userLoader UserLoader, productLister ProductLister) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
gemini: geminiClient,
|
openaiClient: openaiClient,
|
||||||
pexels: pexels,
|
pexels: pexels,
|
||||||
userLoader: userLoader,
|
userLoader: userLoader,
|
||||||
productLister: productLister,
|
productLister: productLister,
|
||||||
@@ -84,7 +84,7 @@ func (h *Handler) GetRecommendations(w http.ResponseWriter, r *http.Request) {
|
|||||||
slog.Warn("load products for recommendations", "user_id", userID, "err", err)
|
slog.Warn("load products for recommendations", "user_id", userID, "err", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
recipes, err := h.gemini.GenerateRecipes(r.Context(), req)
|
recipes, err := h.openaiClient.GenerateRecipes(r.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("generate recipes", "user_id", userID, "err", err)
|
slog.Error("generate recipes", "user_id", userID, "err", err)
|
||||||
writeErrorJSON(w, http.StatusServiceUnavailable, "recipe generation failed, please try again")
|
writeErrorJSON(w, http.StatusServiceUnavailable, "recipe generation failed, please try again")
|
||||||
@@ -109,8 +109,8 @@ func (h *Handler) GetRecommendations(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, recipes)
|
writeJSON(w, http.StatusOK, recipes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildRecipeRequest(u *user.User, count int, lang string) gemini.RecipeRequest {
|
func buildRecipeRequest(u *user.User, count int, lang string) openai.RecipeRequest {
|
||||||
req := gemini.RecipeRequest{
|
req := openai.RecipeRequest{
|
||||||
Count: count,
|
Count: count,
|
||||||
DailyCalories: 2000, // sensible default
|
DailyCalories: 2000, // sensible default
|
||||||
Lang: lang,
|
Lang: lang,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/dish"
|
"github.com/food-ai/backend/internal/dish"
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/jackc/pgx/v5/pgxpool"
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxRequestBodySize = 1 << 20 // 1 MB
|
const maxRequestBodySize = 1 << 20 // 1 MB
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/food-ai/backend/internal/auth"
|
"github.com/food-ai/backend/internal/auth"
|
||||||
"github.com/food-ai/backend/internal/auth/mocks"
|
"github.com/food-ai/backend/internal/auth/mocks"
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/food-ai/backend/internal/testutil"
|
"github.com/food-ai/backend/internal/testutil"
|
||||||
"github.com/food-ai/backend/internal/user"
|
"github.com/food-ai/backend/internal/user"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/ingredient"
|
"github.com/food-ai/backend/internal/ingredient"
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
"github.com/food-ai/backend/internal/testutil"
|
"github.com/food-ai/backend/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/locale"
|
"github.com/food-ai/backend/internal/infra/locale"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
func TestParse(t *testing.T) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRecovery_NoPanic(t *testing.T) {
|
func TestRecovery_NoPanic(t *testing.T) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/food-ai/backend/internal/middleware"
|
"github.com/food-ai/backend/internal/infra/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRequestID_GeneratesNew(t *testing.T) {
|
func TestRequestID_GeneratesNew(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user