Files

172 lines
4.7 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package handlers
import (
"context"
"fmt"
"net/http"
"strconv"
"strings"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/go-chi/chi/v5"
)
// RegionsRouter mounts all /regions routes onto a subrouter.
//
// GET /regions → listRegions
// GET /regions/{regionId} → getRegion
//
// Note: region creation and IATA assignment are managed via the server config
// file, not the API (v1). These endpoints are read-only.
func RegionsRouter(reader api.Reader) http.Handler {
r := chi.NewRouter()
r.Get("/", listRegions(reader))
r.Get("/{regionId}", getRegion(reader))
return r
}
// listRegions godoc
//
// @Summary List all regions
// @Tags Regions
// @Produce json
// @Success 200 {array} api.RegionSummary
// @Failure 404 {object} handlers.APIError
// @Router /regions [get]
func listRegions(reader api.Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
regions, err := reader.ListRegions(r.Context())
if err != nil {
respondError(w, http.StatusNotFound, "no regions found")
return
}
respond(w, http.StatusOK, regions)
}
}
// getRegion godoc
//
// @Summary Get a single region
// @Tags Regions
// @Produce json
// @Param regionId path int true "Region ID"
// @Success 200 {object} api.Region
// @Failure 400 {object} handlers.APIError
// @Failure 404 {object} handlers.APIError
// @Router /regions/{regionId} [get]
func getRegion(reader api.Reader) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
regionID := chi.URLParam(r, "regionId")
regionInt, err := strconv.ParseInt(regionID, 10, 32)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid region ID")
return
}
region, err := reader.GetRegion(r.Context(), int32(regionInt))
if err != nil {
respondError(w, http.StatusNotFound, "region not found")
return
}
respond(w, http.StatusOK, region)
}
}
// parseIATAs splits a comma-separated iatas query param and uppercases each value.
func parseIATAs(r *http.Request) []string {
raw := r.URL.Query().Get("iatas")
if raw == "" {
// fall back to single iata param for backwards compatibility
if single := r.URL.Query().Get("iata"); single != "" {
return []string{strings.ToUpper(strings.TrimSpace(single))}
}
return nil
}
parts := strings.Split(raw, ",")
iatas := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
iatas = append(iatas, strings.ToUpper(p))
}
}
return iatas
}
// parseInt16CSVOrSingle reads a comma-separated list from pluralParam, falling back to a
// single value from singularParam if pluralParam is absent -- same precedence as parseIATAs.
func parseInt16CSVOrSingle(r *http.Request, pluralParam, singularParam string) ([]int16, error) {
raw := r.URL.Query().Get(pluralParam)
if raw == "" {
single := r.URL.Query().Get(singularParam)
if single == "" {
return nil, nil
}
v, err := strconv.ParseInt(single, 10, 16)
if err != nil {
return nil, err
}
return []int16{int16(v)}, nil
}
parts := strings.Split(raw, ",")
out := make([]int16, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
v, err := strconv.ParseInt(p, 10, 16)
if err != nil {
return nil, err
}
out = append(out, int16(v))
}
return out, nil
}
// parseCSVOrSingle reads a comma-separated list from pluralParam, falling back to a single
// value from singularParam if pluralParam is absent -- same precedence as parseIATAs, but
// without the uppercasing (used for scope names, which are case-sensitive, e.g. "#bc").
func parseCSVOrSingle(r *http.Request, pluralParam, singularParam string) []string {
raw := r.URL.Query().Get(pluralParam)
if raw == "" {
if single := r.URL.Query().Get(singularParam); single != "" {
return []string{single}
}
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
// resolveRegionIATAs expands a regionId or region slug to a slice of IATA codes.
func resolveRegionIATAs(ctx context.Context, regionID, regionSlug string, reader api.Reader) ([]string, error) {
var region *api.Region
var err error
switch {
case regionID != "":
rid, e := strconv.ParseInt(regionID, 10, 32)
if e != nil {
return nil, fmt.Errorf("invalid regionId: %w", e)
}
region, err = reader.GetRegion(ctx, int32(rid))
case regionSlug != "":
region, err = reader.GetRegionBySlug(ctx, regionSlug)
default:
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("region not found: %w", err)
}
return region.IATAs, nil
}