// query_bind.go binds endpoint query parameters into typed request config. // Layer: adapters/inbound/httpapi request binding. package httpapi import ( "net/http" "strings" "time" "gitea.maximumdirect.net/ejr/feedapi/bind" apierrors "gitea.maximumdirect.net/ejr/feedapi/errors" "gitea.maximumdirect.net/ejr/weatherapi/internal/adapters/inbound/httpapi/presenter" "gitea.maximumdirect.net/ejr/weatherapi/internal/app" ) type queryRequest struct { Units presenter.Units } type precisionQueryRequest struct { Units presenter.Units Precision int Timezone *time.Location } type timezoneQueryRequest struct { Units presenter.Units Timezone *time.Location } type outlookQueryRequest struct { Units presenter.Units Timezone *time.Location Filter app.OutlookFilter } func bindQuery(r *http.Request) (queryRequest, error) { normalizeCommonQueryValue(r, "units") normalizeCommonQueryValue(r, "format") common, err := bind.CommonQueryParams(r, bind.QueryPolicy{ AllowUnits: true, AllowFormat: true, DefaultUnits: string(presenter.UnitsMetric), RejectUnknown: true, }) if err != nil { return queryRequest{}, err } units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units))) if units == "" { units = presenter.UnitsMetric } return queryRequest{Units: units}, nil } func bindPrecisionQuery(r *http.Request) (precisionQueryRequest, error) { return bindPrecisionQueryInternal(r, false) } func bindForecastPrecisionQuery(r *http.Request) (precisionQueryRequest, error) { return bindPrecisionQueryInternal(r, true) } func bindTimezoneQuery(r *http.Request) (timezoneQueryRequest, error) { normalizeCommonQueryValue(r, "units") normalizeCommonQueryValue(r, "format") common, err := bind.CommonQueryParams(r, bind.QueryPolicy{ AllowUnits: true, AllowFormat: true, DefaultUnits: string(presenter.UnitsMetric), RejectUnknown: true, }, "tz", "TZ") if err != nil { return timezoneQueryRequest{}, err } units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units))) if units == "" { units = presenter.UnitsMetric } tz, err := parseTimezoneQuery(r) if err != nil { return timezoneQueryRequest{}, err } return timezoneQueryRequest{ Units: units, Timezone: tz, }, nil } func bindPrecisionQueryInternal(r *http.Request, allowTimezone bool) (precisionQueryRequest, error) { normalizeCommonQueryValue(r, "units") normalizeCommonQueryValue(r, "format") normalizeCommonQueryValue(r, "precision") allowedExtra := []string{"precision"} if allowTimezone { allowedExtra = append(allowedExtra, "tz", "TZ") } common, err := bind.CommonQueryParams(r, bind.QueryPolicy{ AllowUnits: true, AllowFormat: true, DefaultUnits: string(presenter.UnitsMetric), RejectUnknown: true, }, allowedExtra...) if err != nil { return precisionQueryRequest{}, err } precision, err := bind.OptionalInt(r, "precision", 0) if err != nil { return precisionQueryRequest{}, err } if err := bind.MinInt(precision, 0, "precision"); err != nil { return precisionQueryRequest{}, err } if err := bind.MaxInt(precision, 2, "precision"); err != nil { return precisionQueryRequest{}, err } units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units))) if units == "" { units = presenter.UnitsMetric } var tz *time.Location if allowTimezone { tz, err = parseTimezoneQuery(r) if err != nil { return precisionQueryRequest{}, err } } return precisionQueryRequest{ Units: units, Precision: precision, Timezone: tz, }, nil } func bindOutlookQuery(r *http.Request) (outlookQueryRequest, error) { return bindOutlookQueryInternal(r, true) } func bindOutlookLocationQuery(r *http.Request) (outlookQueryRequest, error) { return bindOutlookQueryInternal(r, false) } func bindOutlookQueryInternal(r *http.Request, allowContainsLocation bool) (outlookQueryRequest, error) { normalizeCommonQueryValue(r, "units") normalizeCommonQueryValue(r, "format") normalizeCommonQueryValue(r, "outlookType") allowedExtra := []string{"tz", "TZ", "day", "outlookType"} if allowContainsLocation { allowedExtra = append(allowedExtra, "containsLocation") } common, err := bind.CommonQueryParams(r, bind.QueryPolicy{ AllowUnits: true, AllowFormat: true, DefaultUnits: string(presenter.UnitsMetric), RejectUnknown: true, }, allowedExtra...) if err != nil { return outlookQueryRequest{}, err } units := presenter.Units(strings.ToLower(strings.TrimSpace(common.Units))) if units == "" { units = presenter.UnitsMetric } tz, err := parseTimezoneQuery(r) if err != nil { return outlookQueryRequest{}, err } filter, err := bindOutlookFilter(r, allowContainsLocation) if err != nil { return outlookQueryRequest{}, err } return outlookQueryRequest{ Units: units, Timezone: tz, Filter: filter, }, nil } func bindOutlookFilter(r *http.Request, allowContainsLocation bool) (app.OutlookFilter, error) { var filter app.OutlookFilter if strings.TrimSpace(r.URL.Query().Get("day")) != "" { day, err := bind.OptionalInt(r, "day", 0) if err != nil { return app.OutlookFilter{}, err } if day < 1 || day > 3 { return app.OutlookFilter{}, apierrors.InvalidParameter("day must be one of [1, 2, 3]") } filter.Day = &day } outlookType := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("outlookType"))) if outlookType != "" { switch outlookType { case "categorical", "tornado", "hail", "wind": filter.OutlookType = outlookType default: return app.OutlookFilter{}, apierrors.InvalidParameter("outlookType must be one of [categorical, tornado, hail, wind]") } } if strings.TrimSpace(r.URL.Query().Get("containsLocation")) != "" { if !allowContainsLocation { return app.OutlookFilter{}, apierrors.InvalidParameter("containsLocation is not allowed on this endpoint") } containsLocation, err := bind.OptionalBool(r, "containsLocation", false) if err != nil { return app.OutlookFilter{}, err } filter.ContainsLocation = &containsLocation } return filter, nil }