github.com/deadlysurgeon/weather@v0.0.0-20240402201029-3925d9f784b1/weather/impl.go (about)

     1  package weather
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/deadlysurgeon/weather/settings"
    10  )
    11  
    12  type impl struct {
    13  	endpoint string
    14  	apiKey   string
    15  	client   *http.Client
    16  }
    17  
    18  // New creates a weather service implementation
    19  func New(config settings.App) (Service, error) {
    20  	service := &impl{
    21  		endpoint: "https://api.openweathermap.org/data/3.0/onecall", // ?lat={lat}&lon={lon}&exclude={part}&appid={API key}
    22  		client: &http.Client{
    23  			Timeout: 5 * time.Second,
    24  		},
    25  	}
    26  
    27  	if service.apiKey = config.Weather.APIKey; service.apiKey == "" {
    28  		return nil, fmt.Errorf("no api key specified")
    29  	}
    30  
    31  	if config.Weather.Endpoint != "" {
    32  		service.endpoint = config.Weather.Endpoint
    33  	}
    34  
    35  	return service, nil
    36  }
    37  
    38  func (s *impl) At(lat, lon string) (Report, error) {
    39  	report := Report{
    40  		Temperature:    "unknown",
    41  		Condition:      "unknown",
    42  		TemperatureRaw: 0,
    43  	}
    44  
    45  	req, err := s.formRequest(lat, lon)
    46  	if err != nil {
    47  		return report, err
    48  	}
    49  
    50  	resp, err := s.client.Do(req)
    51  	if err != nil {
    52  		return report, err
    53  	}
    54  	defer resp.Body.Close()
    55  
    56  	// TODO:
    57  	// - Double check how they return errors such as overuse.
    58  
    59  	if resp.StatusCode != http.StatusOK {
    60  		return report, fmt.Errorf("bad status code: %d", resp.StatusCode)
    61  	}
    62  
    63  	var owmResp openweathermapResponse
    64  	if err = json.NewDecoder(resp.Body).Decode(&owmResp); err != nil {
    65  		return report, fmt.Errorf("failed to read response: %w", err)
    66  	}
    67  
    68  	report.TemperatureRaw = owmResp.Current.FeelsLike
    69  	report.Temperature = feelsLike(report.TemperatureRaw)
    70  	if len(owmResp.Current.Weather) > 0 {
    71  		report.Condition = owmResp.Current.Weather[0].Main
    72  	}
    73  
    74  	return report, nil
    75  }
    76  
    77  func feelsLike(temp float32) string {
    78  	switch {
    79  	case temp <= 0:
    80  		return "freezing"
    81  	case temp <= 10:
    82  		return "cold"
    83  	case temp <= 20:
    84  		return "moderate"
    85  	case temp <= 30:
    86  		return "hot"
    87  	default:
    88  		return "burning"
    89  	}
    90  }
    91  
    92  func (s *impl) formRequest(lat, lon string) (*http.Request, error) {
    93  	req, err := http.NewRequest(http.MethodGet, s.endpoint, nil)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  
    98  	q := req.URL.Query()
    99  	q.Add("appid", s.apiKey)
   100  	q.Add("units", "metric")
   101  	q.Add("lat", lat)
   102  	q.Add("lon", lon)
   103  	req.URL.RawQuery = q.Encode()
   104  
   105  	return req, nil
   106  }
   107  
   108  // Only grab the fields we care about. If we needed more from the service we
   109  // would need to break this out into its own models file and each sub struct be
   110  // exportable.
   111  type openweathermapResponse struct {
   112  	Current struct {
   113  		FeelsLike float32 `json:"feels_like"`
   114  		Weather   []struct {
   115  			Main string `json:"main"`
   116  		} `json:"weather"`
   117  	} `json:"current"`
   118  }