package api import ( "context" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/favicon" "github.com/gofiber/fiber/v3/middleware/logger" recover "github.com/gofiber/fiber/v3/middleware/recover" "github.com/gofiber/fiber/v3/middleware/static" "prayertimes/pkg/prayer" "prayertimes/templates" ) type httpError struct { Message string `json:"message"` } type Services struct { Provider DiyanetProvider } type DiyanetProvider interface { Get(ctx context.Context, locationID string) ([]prayer.Times, error) GetByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Times, error) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) } func New(services Services) *fiber.App { app := fiber.New(fiber.Config{ Immutable: true, ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 30, ErrorHandler: func(ctx fiber.Ctx, err error) error { if errors.Is(err, prayer.ErrInvalidLocation) { return ctx.Status(http.StatusUnprocessableEntity).JSON(httpError{ Message: err.Error(), }) } return fiber.DefaultErrorHandler(ctx, err) }, }) app.Use( favicon.New(), recover.New(recover.Config{EnableStackTrace: true}), logger.New(), cors.New(), ) app.Get("/healthz", func(ctx fiber.Ctx) error { return ctx.SendString("OK") }) app.Get("/api/v1/diyanet/prayertimes", func(ctx fiber.Ctx) error { var query struct { LocationID string `query:"location_id"` Latitude string `query:"latitude"` Longitude string `query:"longitude"` } if err := ctx.Bind().Query(&query); err != nil { return fmt.Errorf("failed to bind prayer times query parameters: %w", errors.Join(fiber.ErrBadRequest, err)) } locationID := strings.TrimSpace(query.LocationID) latitude := strings.TrimSpace(query.Latitude) longitude := strings.TrimSpace(query.Longitude) var ( times []prayer.Times err error ) switch { case locationID != "": times, err = services.Provider.Get(ctx.Context(), locationID) case latitude != "" && longitude != "": lat, latErr := strconv.ParseFloat(latitude, 64) if latErr != nil { return fmt.Errorf("failed to parse latitude query parameter: %w", errors.Join(fiber.ErrBadRequest, latErr)) } lng, lngErr := strconv.ParseFloat(longitude, 64) if lngErr != nil { return fmt.Errorf("failed to parse longitude query parameter: %w", errors.Join(fiber.ErrBadRequest, lngErr)) } times, err = services.Provider.GetByCoords(ctx.Context(), prayer.Coordinates{ Latitude: lat, Longitude: lng, }) default: return fmt.Errorf("failed to validate prayer times query parameters: %w", fiber.ErrBadRequest) } if err != nil { return fmt.Errorf("failed to fetch prayer times: %w", err) } ctx.Response().Header.Set(fiber.HeaderCacheControl, "max-age=86400") return ctx.JSON(fiber.Map{ "prayertimes": times, }) }) app.Get("/api/v1/diyanet/location", func(ctx fiber.Ctx) error { var query struct { Text string `query:"query"` } if err := ctx.Bind().Query(&query); err != nil { return fmt.Errorf("failed to bind location query parameters: %w", errors.Join(fiber.ErrBadRequest, err)) } query.Text = strings.TrimSpace(query.Text) if query.Text == "" { return fmt.Errorf("failed to validate location query parameters: %w", fiber.ErrBadRequest) } locations, err := services.Provider.SearchLocations(ctx.Context(), query.Text) if err != nil { return fmt.Errorf("failed to search locations: %w", err) } ctx.Response().Header.Set(fiber.HeaderCacheControl, "max-age=86400") return ctx.JSON(fiber.Map{ "locations": locations, }) }) app.Use("/", static.New("", static.Config{FS: templates.FS()})) return app }