package handlers import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "strings" "fred-api/config" "fred-api/models" "github.com/gin-gonic/gin" ) type FREDHandler struct { baseURL string apiKey string } func NewFREDHandler(cfg *config.Config) *FREDHandler { return &FREDHandler{ baseURL: cfg.FREDBaseURL, apiKey: cfg.FREDAPIKey, } } func (h *FREDHandler) buildURL(endpoint string, params map[string]string) string { u, _ := url.Parse(h.baseURL + endpoint) query := u.Query() query.Set("api_key", h.apiKey) query.Set("file_type", "json") for k, v := range params { if v != "" { query.Set(k, v) } } u.RawQuery = query.Encode() return u.String() } func (h *FREDHandler) fetchData(target interface{}, c *gin.Context, endpoint string, params map[string]string) { url := h.buildURL(endpoint, params) resp, err := http.Get(url) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{ ErrorCode: "NetworkError", ErrorMessage: err.Error(), }) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{ ErrorCode: "ReadError", ErrorMessage: err.Error(), }) return } if resp.StatusCode != http.StatusOK { var errResp models.ErrorResponse json.Unmarshal(body, &errResp) c.JSON(http.StatusBadGateway, models.ErrorResponse{ ErrorCode: "APIError", ErrorMessage: fmt.Sprintf("Status: %d, Error: %s - %s", resp.StatusCode, errResp.ErrorCode, errResp.ErrorMessage), }) return } if err := json.Unmarshal(body, target); err != nil { c.JSON(http.StatusInternalServerError, models.ErrorResponse{ ErrorCode: "ParseError", ErrorMessage: err.Error(), }) return } c.JSON(http.StatusOK, target) } func (h *FREDHandler) HealthCheck(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "status": "healthy", "service": "FRED API", }) } func (h *FREDHandler) GetCategory(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } h.fetchData(&models.CategoryResponse{}, c, "/category", map[string]string{"category_id": categoryID}) } func (h *FREDHandler) GetCategoryChildren(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } params := map[string]string{"category_id": categoryID} h.fetchData(&models.CategoriesResponse{}, c, "/category/children", params) } func (h *FREDHandler) GetCategoryRelated(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } h.fetchData(&models.RelatedTagsResponse{}, c, "/category/related", map[string]string{"category_id": categoryID}) } func (h *FREDHandler) GetCategorySeries(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } params := map[string]string{"category_id": categoryID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.CategorySeriesResponse{}, c, "/category/series", params) } func (h *FREDHandler) GetCategoryTags(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } params := map[string]string{"category_id": categoryID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.CategoryTagsResponse{}, c, "/category/tags", params) } func (h *FREDHandler) GetCategoryRelatedTags(c *gin.Context) { categoryID := c.Query("category_id") if categoryID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "category_id is required", }) return } tagNames := c.Query("tag_names") if tagNames == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "tag_names is required", }) return } params := map[string]string{"category_id": categoryID, "tag_names": tagNames} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.RelatedTagsResponse{}, c, "/category/related_tags", params) } func (h *FREDHandler) GetSeries(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.SeriesResponse{}, c, "/series", params) } func (h *FREDHandler) GetSeriesCategories(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.SeriesCategoriesResponse{}, c, "/series/categories", params) } func (h *FREDHandler) GetObservations(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "sort_order", "observation_start", "observation_end", "units", "frequency", "aggregation_method", "output_type", "vintage_dates", }) h.fetchData(&models.ObservationsResponse{}, c, "/series/observations", params) } func (h *FREDHandler) GetSeriesRelease(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.ReleaseResponse{}, c, "/series/release", params) } func (h *FREDHandler) SearchSeries(c *gin.Context) { searchText := c.Query("search_text") if searchText == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "search_text is required", }) return } params := map[string]string{"search_text": searchText} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "sort_order", "order_by", "filter", "" + "" + "" + "" + "" + "", }) h.fetchData(&models.SearchResponse{}, c, "/series/search", params) } func (h *FREDHandler) SearchSeriesTags(c *gin.Context) { searchText := c.Query("search_text") if searchText == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "search_text is required", }) return } params := map[string]string{"search_text": searchText} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.TagsResponse{}, c, "/series/search/tags", params) } func (h *FREDHandler) SearchSeriesRelatedTags(c *gin.Context) { searchText := c.Query("search_text") if searchText == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "search_text is required", }) return } tagNames := c.Query("tag_names") if tagNames == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "tag_names is required", }) return } params := map[string]string{"search_text": searchText, "tag_names": tagNames} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.RelatedTagsResponse{}, c, "/series/search/related_tags", params) } func (h *FREDHandler) GetSeriesTags(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.SeriesTagsResponse{}, c, "/series/tags", params) } func (h *FREDHandler) GetSeriesUpdates(c *gin.Context) { params := map[string]string{} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "sort_order", "filter", "observation_start", "observation_end", }) h.fetchData(&models.SeriesUpdatesResponse{}, c, "/series/updates", params) } func (h *FREDHandler) GetSeriesVintageDates(c *gin.Context) { seriesID := c.Query("series_id") if seriesID == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "series_id is required", }) return } params := map[string]string{"series_id": seriesID} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.VintageDatesResponse{}, c, "/series/vintagedates", params) } func (h *FREDHandler) GetReleases(c *gin.Context) { params := map[string]string{} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"}) h.fetchData(&models.ReleasesResponse{}, c, "/releases", params) } func (h *FREDHandler) GetReleaseDates(c *gin.Context) { params := map[string]string{} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "sort_order", "release_id", "date", "start_date", "end_date", }) h.fetchData(&models.ReleaseDatesResponse{}, c, "/releases/dates", params) } func (h *FREDHandler) GetRelease(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.ReleaseResponse{}, c, "/release", params) } func (h *FREDHandler) GetReleaseDates2(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order", "date", "start_date", "end_date"}) h.fetchData(&models.ReleaseDatesResponse{}, c, "/release/dates", params) } func (h *FREDHandler) GetReleaseSeries(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "sort_order", "order_by", "filter", "observation_start", "observation_end", "units", "frequency", "aggregation_method", "output_type", }) h.fetchData(&models.SearchResponse{}, c, "/release/series", params) } func (h *FREDHandler) GetReleaseSources(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.ReleaseSourcesResponse{}, c, "/release/sources", params) } func (h *FREDHandler) GetReleaseTags(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.ReleaseTagsResponse{}, c, "/release/tags", params) } func (h *FREDHandler) GetReleaseRelatedTags(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } tagNames := c.Query("tag_names") if tagNames == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "tag_names is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID), "tag_names": tagNames} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order"}) h.fetchData(&models.RelatedTagsResponse{}, c, "/release/related_tags", params) } func (h *FREDHandler) GetReleaseTables(c *gin.Context) { releaseIDStr := c.Query("release_id") if releaseIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "release_id is required", }) return } releaseID, _ := strconv.Atoi(releaseIDStr) params := map[string]string{"release_id": strconv.Itoa(releaseID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.ReleaseTablesResponse{}, c, "/release/tables", params) } func (h *FREDHandler) GetSources(c *gin.Context) { params := map[string]string{} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"}) h.fetchData(&models.SourcesResponse{}, c, "/sources", params) } func (h *FREDHandler) GetSource(c *gin.Context) { sourceIDStr := c.Query("source_id") if sourceIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "source_id is required", }) return } sourceID, _ := strconv.Atoi(sourceIDStr) params := map[string]string{"source_id": strconv.Itoa(sourceID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end"}) h.fetchData(&models.SourceResponse{}, c, "/source", params) } func (h *FREDHandler) GetSourceReleases(c *gin.Context) { sourceIDStr := c.Query("source_id") if sourceIDStr == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "source_id is required", }) return } sourceID, _ := strconv.Atoi(sourceIDStr) params := map[string]string{"source_id": strconv.Itoa(sourceID)} h.addOptionalParams(c, params, []string{"realtime_start", "realtime_end", "limit", "offset", "sort_order"}) h.fetchData(&models.ReleasesResponse{}, c, "/source/releases", params) } func (h *FREDHandler) GetTags(c *gin.Context) { params := map[string]string{} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order", "tag_name", "tag_group_id", "filter", }) h.fetchData(&models.TagsResponse{}, c, "/tags", params) } func (h *FREDHandler) GetRelatedTags(c *gin.Context) { tagNames := c.Query("tag_names") if tagNames == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "tag_names is required", }) return } params := map[string]string{"tag_names": tagNames} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order", }) h.fetchData(&models.RelatedTagsResponse{}, c, "/related_tags", params) } func (h *FREDHandler) GetTagsSeries(c *gin.Context) { tagNames := c.Query("tag_names") if tagNames == "" { c.JSON(http.StatusBadRequest, models.ErrorResponse{ ErrorCode: "BadRequest", ErrorMessage: "tag_names is required", }) return } params := map[string]string{"tag_names": tagNames} h.addOptionalParams(c, params, []string{ "realtime_start", "realtime_end", "limit", "offset", "order_by", "sort_order", "filter", "" + "" + "" + "" + "" + "", }) h.fetchData(&models.SearchResponse{}, c, "/tags/series", params) } func (h *FREDHandler) addOptionalParams(c *gin.Context, params map[string]string, optionals []string) { for _, param := range optionals { if v := c.Query(param); v != "" { params[param] = strings.TrimSpace(v) } } }