package weather import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "github.com/paramah/ai_devs4/s01e03/internal/domain" ) // APIClient implements domain.WeatherClient using wttr.in type APIClient struct { baseURL string client *http.Client } // NewAPIClient creates a new weather API client // apiKey parameter is ignored as wttr.in doesn't require authentication func NewAPIClient(baseURL, apiKey string) *APIClient { if baseURL == "" { baseURL = "https://wttr.in" } return &APIClient{ baseURL: baseURL, client: &http.Client{}, } } type wttrResponse struct { CurrentCondition []struct { TempC string `json:"temp_C"` Humidity string `json:"humidity"` WeatherDesc []struct { Value string `json:"value"` } `json:"weatherDesc"` } `json:"current_condition"` NearestArea []struct { AreaName []struct { Value string `json:"value"` } `json:"areaName"` } `json:"nearest_area"` } // GetWeather gets weather information for a location using wttr.in func (c *APIClient) GetWeather(ctx context.Context, location string) (*domain.WeatherInfo, error) { // Build URL - wttr.in format: https://wttr.in/Location?format=j1 u, err := url.Parse(c.baseURL + "/" + url.PathEscape(location)) if err != nil { return nil, fmt.Errorf("parsing URL: %w", err) } q := u.Query() q.Set("format", "j1") u.RawQuery = q.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("creating request: %w", err) } // wttr.in prefers this header to return JSON req.Header.Set("User-Agent", "curl/7.0") resp, err := c.client.Do(req) if err != nil { return nil, fmt.Errorf("sending request: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) } body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("reading response: %w", err) } var apiResp wttrResponse if err := json.Unmarshal(body, &apiResp); err != nil { return nil, fmt.Errorf("unmarshaling response: %w", err) } if len(apiResp.CurrentCondition) == 0 { return nil, fmt.Errorf("no weather data available for location: %s", location) } current := apiResp.CurrentCondition[0] // Parse temperature temp, err := strconv.ParseFloat(current.TempC, 64) if err != nil { return nil, fmt.Errorf("parsing temperature: %w", err) } // Parse humidity humidity, err := strconv.Atoi(current.Humidity) if err != nil { return nil, fmt.Errorf("parsing humidity: %w", err) } // Get description description := "unknown" if len(current.WeatherDesc) > 0 { description = current.WeatherDesc[0].Value } // Get location name locationName := location if len(apiResp.NearestArea) > 0 && len(apiResp.NearestArea[0].AreaName) > 0 { locationName = apiResp.NearestArea[0].AreaName[0].Value } return &domain.WeatherInfo{ Location: locationName, Temperature: temp, Description: description, Humidity: humidity, }, nil }