diff --git a/examples/geo_basic/main.go b/examples/geo_basic/main.go index 8f69fad..bb08165 100644 --- a/examples/geo_basic/main.go +++ b/examples/geo_basic/main.go @@ -10,10 +10,7 @@ import ( ) func main() { - client, err := geo.NewClient(nil) - if err != nil { - log.Fatalf("new client: %v", err) - } + client := geo.NewClient(nil) resp, err := client.Direct("Stockholm,SE", nil) if err != nil { diff --git a/examples/onecall_advanced/main.go b/examples/onecall_advanced/main.go index b628542..4a98be2 100644 --- a/examples/onecall_advanced/main.go +++ b/examples/onecall_advanced/main.go @@ -26,7 +26,7 @@ func main() { Transport: newRateLimitedTransport(rate.Every(time.Second), 1, nil), } - client, err := onecall.NewClient(&onecall.ClientOptions{ + client := onecall.NewClient(&onecall.ClientOptions{ HttpClient: httpClient, // Either pass AppID like below, @@ -37,9 +37,7 @@ func main() { // which is not very common for everyday applications. Units: onecall.Units.METRIC, }) - if err != nil { - panic(err) - } + resp, err := client.OneCall(59.3327, 18.0656, &onecall.OneCallOptions{ // If we only want CURRENT and DAILY for our location we can exclude the other forecasts. Exclude: []onecall.Exclude{onecall.Excludes.HOURLY, onecall.Excludes.MINUTELY, onecall.Excludes.ALERTS}, diff --git a/examples/onecall_basic/main.go b/examples/onecall_basic/main.go index cc6eb76..1779248 100644 --- a/examples/onecall_basic/main.go +++ b/examples/onecall_basic/main.go @@ -10,14 +10,11 @@ import ( ) func main() { - client, err := onecall.NewClient(&onecall.ClientOptions{ + client := onecall.NewClient(&onecall.ClientOptions{ // By default, OpenWeatherMap API returns Kelvin for temperature, // which is not very common for everyday applications. Units: onecall.Units.METRIC, }) - if err != nil { - panic(err) - } resp, err := client.OneCall(59.3327, 18.0656, nil) if err != nil { diff --git a/geo/client.go b/geo/client.go index 7f60574..5fd5985 100644 --- a/geo/client.go +++ b/geo/client.go @@ -33,7 +33,7 @@ type ClientOptions struct { Logger *slog.Logger } -func NewClient(opts *ClientOptions) (*Client, error) { +func NewClient(opts *ClientOptions) *Client { // Defaults if opts are not provided if opts == nil { @@ -60,7 +60,7 @@ func NewClient(opts *ClientOptions) (*Client, error) { appID: opts.AppID, httpClient: opts.HttpClient, logger: opts.Logger, - }, nil + } } type GeoOptions struct { diff --git a/go.mod b/go.mod index 8b746df..317e66d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.24.3 require ( github.com/joho/godotenv v1.5.1 + github.com/martinlindhe/unit v0.0.0-20230420213220-4adfd7d0a0d6 github.com/stretchr/testify v1.10.0 golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 golang.org/x/time v0.12.0 diff --git a/go.sum b/go.sum index 9c47523..400297a 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,14 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/martinlindhe/unit v0.0.0-20230420213220-4adfd7d0a0d6 h1:muzoir7BEy+lDPqdROr57IjJBP7OydzCg0VDhZtdG+w= +github.com/martinlindhe/unit v0.0.0-20230420213220-4adfd7d0a0d6/go.mod h1:8QbxAolnDKw/JhUJMU80MRjHjEs0tLwkjZAPrTn+xLA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4= @@ -12,5 +17,6 @@ golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/onecall/client.go b/onecall/client.go index 7ede0e6..f5fdacb 100644 --- a/onecall/client.go +++ b/onecall/client.go @@ -37,7 +37,7 @@ type ClientOptions struct { Units Unit // Units to use for the client. Overruled by unit option explicitly passed to client calls. } -func NewClient(opts *ClientOptions) (*Client, error) { +func NewClient(opts *ClientOptions) *Client { if opts == nil { opts = &ClientOptions{} } @@ -66,7 +66,7 @@ func NewClient(opts *ClientOptions) (*Client, error) { if opts.Units.IsValid() { client.unit = opts.Units } - return client, nil + return client } type OneCallOptions struct { @@ -75,17 +75,17 @@ type OneCallOptions struct { Lang Lang } -func (c *Client) OneCallRaw(lat, lon float64, opts *OneCallOptions) (*OneCallResponseRaw, error) { +func (c *Client) OneCallRaw(lat, lon float64, opts *OneCallOptions) (*OneCallResponseRaw, *Unit, error) { if lat < -90 || lat > 90 { - return nil, fmt.Errorf("lat argument must be in range (-90; 90), is %v", lat) + return nil, nil, fmt.Errorf("lat argument must be in range (-90; 90), is %v", lat) } if lon < -180 || lon > 180 { - return nil, fmt.Errorf("lon argument must be in range (-180; 180), is %v", lon) + return nil, nil, fmt.Errorf("lon argument must be in range (-180; 180), is %v", lon) } u, err := url.Parse(c.baseURL) if err != nil { - return nil, fmt.Errorf("parse url: %w", err) + return nil, nil, fmt.Errorf("parse url: %w", err) } q := u.Query() @@ -97,9 +97,12 @@ func (c *Client) OneCallRaw(lat, lon float64, opts *OneCallOptions) (*OneCallRes q.Set(excludeParam, ExcludeList(opts.Exclude).String()) } + var units *Unit if opts != nil && opts.Units.IsValid() { + units = &opts.Units q.Set(unitsParam, opts.Units.String()) } else if c.unit.IsValid() { + units = &c.unit q.Set(unitsParam, c.unit.String()) } @@ -111,44 +114,44 @@ func (c *Client) OneCallRaw(lat, lon float64, opts *OneCallOptions) (*OneCallRes resp, err := c.httpClient.Get(u.String()) if err != nil { - return nil, fmt.Errorf("get: %w", err) + return nil, nil, fmt.Errorf("get: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("unexpected status: %s", resp.Status) + return nil, nil, fmt.Errorf("unexpected status: %s", resp.Status) } bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) + return nil, nil, fmt.Errorf("failed to read response body: %w", err) } // Save response body to a file f, err := os.Create("response.json") if err != nil { - return nil, fmt.Errorf("failed to create file: %w", err) + return nil, nil, fmt.Errorf("failed to create file: %w", err) } defer f.Close() _, err = f.Write(bodyBytes) if err != nil { - return nil, fmt.Errorf("failed to write to file: %w", err) + return nil, nil, fmt.Errorf("failed to write to file: %w", err) } var oneCallResp OneCallResponseRaw if err := json.Unmarshal(bodyBytes, &oneCallResp); err != nil { - return nil, fmt.Errorf("failed to decode one call response JSON: %w", err) + return nil, nil, fmt.Errorf("failed to decode one call response JSON: %w", err) } - return &oneCallResp, nil + return &oneCallResp, units, nil } func (c *Client) OneCall(lat, lon float64, opts *OneCallOptions) (*OneCallResponse, error) { - raw, err := c.OneCallRaw(lat, lon, opts) + raw, units, err := c.OneCallRaw(lat, lon, opts) if err != nil { return nil, err } - return raw.Parse(), nil + return raw.Parse(units), nil } diff --git a/onecall/client_test.go b/onecall/client_test.go index eea2bfb..2155aea 100644 --- a/onecall/client_test.go +++ b/onecall/client_test.go @@ -7,11 +7,10 @@ import ( ) func TestNewClient(t *testing.T) { - client, err := NewClient(&ClientOptions{ + client := NewClient(&ClientOptions{ AppID: "TEST", Units: Units.METRIC, }) - require.NoError(t, err) - _, err = client.OneCall(0, 0, nil) + _, err := client.OneCall(0, 0, nil) require.Error(t, err) // 401 Unauthorized } diff --git a/onecall/model.go b/onecall/model.go index 062bf8b..c4dcb44 100644 --- a/onecall/model.go +++ b/onecall/model.go @@ -40,12 +40,12 @@ func (w weathersRaw) convert() []WeatherCondition { return out } -func (r OneCallResponseRaw) Parse() *OneCallResponse { +func (r OneCallResponseRaw) Parse(units *Unit) *OneCallResponse { return &OneCallResponse{ oneCallResponseCommon: r.oneCallResponseCommon, Current: *r.Current.Parse(), Minutely: minuteResponsesRaw(r.Minutely).Parse(), - Daily: dailyResponsesRaw(r.Daily).Parse(), + Daily: dailyResponsesRaw(r.Daily).Parse(units), } } @@ -54,6 +54,6 @@ func (p OneCallResponse) convert() *OneCallResponseRaw { oneCallResponseCommon: p.oneCallResponseCommon, Current: p.Current.Parse(), Minutely: minuteResponses(p.Minutely).convert(), - Daily: dailyResponses(p.Daily).parse(), + Daily: dailyResponses(p.Daily).convert(), } } diff --git a/onecall/model_daily.go b/onecall/model_daily.go index 4afb8fd..9b06045 100644 --- a/onecall/model_daily.go +++ b/onecall/model_daily.go @@ -1,43 +1,51 @@ package onecall -import "time" +import ( + "time" + + "github.com/martinlindhe/unit" +) type dailyResponseCommons struct { - MoonPhase float64 `json:"moon_phase"` // Moon phase. 0 and 1 are 'new moon', 0.25 is 'first quarter moon', 0.5 is 'full moon' and 0.75 is 'last quarter moon'. The periods in between are called 'waxing crescent', 'waxing gibbous', 'waning gibbous', and 'waning crescent', respectively. Moon phase calculation algorithm: if the moon phase values between the start of the day and the end of the day have a round value (0, 0.25, 0.5, 0.75, 1.0), then this round value is taken, otherwise the average of moon phases for the start of the day and the end of the day is taken - Summary string `json:"summary"` // Human-readable description of the weather conditions for the day - Temp Temp `json:"temp"` // Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. - FeelsLike FeelsLike `json:"feels_like"` // This accounts for the human perception of weather. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. - Pressure int `json:"pressure"` // Atmospheric pressure on the sea level, hPa - Humidity int `json:"humidity"` // Humidity, % - DewPoint float64 `json:"dew_point"` // Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. - WindSpeed float64 `json:"wind_speed"` // Wind speed. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. - WindGust *float64 `json:"wind_gust"` // (where available) Wind gust. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. - WindDeg int `json:"wind_deg"` // Wind direction, degrees (meteorological) - Clouds int `json:"clouds"` // Cloudiness, % - UVI float64 `json:"uvi"` // The maximum value of UV index for the day - Pop float64 `json:"pop"` // Probability of precipitation. The values of the parameter vary between 0 and 1, where 0 is equal to 0%, 1 is equal to 100% + MoonPhase float64 `json:"moon_phase"` // Moon phase. 0 and 1 are 'new moon', 0.25 is 'first quarter moon', 0.5 is 'full moon' and 0.75 is 'last quarter moon'. The periods in between are called 'waxing crescent', 'waxing gibbous', 'waning gibbous', and 'waning crescent', respectively. Moon phase calculation algorithm: if the moon phase values between the start of the day and the end of the day have a round value (0, 0.25, 0.5, 0.75, 1.0), then this round value is taken, otherwise the average of moon phases for the start of the day and the end of the day is taken + Summary string `json:"summary"` // Human-readable description of the weather conditions for the day + Temp Temp `json:"temp"` // Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. + Pressure int `json:"pressure"` // Atmospheric pressure on the sea level, hPa + Humidity int `json:"humidity"` // Humidity, % + DewPoint float64 `json:"dew_point"` // Atmospheric temperature (varying according to pressure and humidity) below which water droplets begin to condense and dew can form. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. + WindSpeed float64 `json:"wind_speed"` // Wind speed. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. + WindGust *float64 `json:"wind_gust"` // (where available) Wind gust. Units – default: metre/sec, metric: metre/sec, imperial: miles/hour. + WindDeg int `json:"wind_deg"` // Wind direction, degrees (meteorological) + Clouds int `json:"clouds"` // Cloudiness, % + UVI float64 `json:"uvi"` // The maximum value of UV index for the day + Pop float64 `json:"pop"` // Probability of precipitation. The values of the parameter vary between 0 and 1, where 0 is equal to 0%, 1 is equal to 100% } type DailyResponseRaw struct { dailyResponseCommons - Dt int64 // Time of the forecasted data, Unix, UTC - Sunrise int64 // Sunrise time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response - Sunset int64 // Sunset time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response - Moonrise int64 // The time of when the moon rises for this day, Unix, UTC - Moonset int64 // The time of when the moon rises for this day, Unix, UTC - Weather []WeatherRaw + Dt int64 // Time of the forecasted data, Unix, UTC + Sunrise int64 // Sunrise time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response + Sunset int64 // Sunset time, Unix, UTC. For polar areas in midnight sun and polar night periods this parameter is not returned in the response + Moonrise int64 // The time of when the moon rises for this day, Unix, UTC + Moonset int64 // The time of when the moon rises for this day, Unix, UTC + FeelsLike FeelsLikeRaw `json:"feels_like"` // This accounts for the human perception of weather. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. + Weather []WeatherRaw } type DailyResponse struct { dailyResponseCommons - Dt time.Time - Sunrise time.Time - Sunset time.Time - Moonrise time.Time - Moonset time.Time - Weather []WeatherCondition + Dt time.Time + Sunrise time.Time + Sunset time.Time + Moonrise time.Time + Moonset time.Time + FeelsLike FeelsLike `json:"feels_like"` // This accounts for the human perception of weather. Units – default: kelvin, metric: Celsius, imperial: Fahrenheit. + Weather []WeatherCondition + + // internal fields + units *Unit } type Temp struct { @@ -49,13 +57,20 @@ type Temp struct { Max float64 `json:"max"` // Max daily temperature. } -type FeelsLike struct { +type FeelsLikeRaw struct { Morn float64 `json:"morn"` // Morning temperature. Day float64 `json:"day"` // Day temperature. Eve float64 `json:"eve"` // Evening temperature. Night float64 `json:"night"` // Night temperature. } +type FeelsLike struct { + Morn unit.Temperature `json:"morn"` // Morning temperature. + Day unit.Temperature `json:"day"` // Day temperature. + Eve unit.Temperature `json:"eve"` // Evening temperature. + Night unit.Temperature `json:"night"` // Night temperature. +} + // daily.rain (where available) Precipitation volume, mm. Please note that only mm as units of measurement are available for this parameter // daily.snow (where available) Snow volume, mm. Please note that only mm as units of measurement are available for this parameter // daily.weather @@ -67,7 +82,7 @@ type FeelsLike struct { type dailyResponsesRaw []DailyResponseRaw type dailyResponses []DailyResponse -func (r dailyResponsesRaw) Parse() []DailyResponse { +func (r dailyResponsesRaw) Parse(units *Unit) []DailyResponse { var out []DailyResponse for _, d := range r { out = append(out, DailyResponse{ @@ -77,13 +92,21 @@ func (r dailyResponsesRaw) Parse() []DailyResponse { Sunset: time.Unix(d.Sunset, 0), Moonrise: time.Unix(d.Moonrise, 0), Moonset: time.Unix(d.Moonset, 0), - Weather: weathersRaw(d.Weather).convert(), + FeelsLike: FeelsLike{ + Morn: convertValueToTemp(d.FeelsLike.Morn, units), + Day: convertValueToTemp(d.FeelsLike.Day, units), + Eve: convertValueToTemp(d.FeelsLike.Eve, units), + Night: convertValueToTemp(d.FeelsLike.Night, units), + }, + Weather: weathersRaw(d.Weather).convert(), + + units: units, }) } return out } -func (r dailyResponses) parse() []DailyResponseRaw { +func (r dailyResponses) convert() []DailyResponseRaw { var out []DailyResponseRaw for _, d := range r { out = append(out, DailyResponseRaw{ @@ -93,8 +116,36 @@ func (r dailyResponses) parse() []DailyResponseRaw { Sunset: d.Sunset.Unix(), Moonrise: d.Moonrise.Unix(), Moonset: d.Moonset.Unix(), - Weather: weatherConditions(d.Weather).convert(), + FeelsLike: FeelsLikeRaw{ + Morn: convertTempToValue(d.FeelsLike.Morn, d.units), + Day: convertTempToValue(d.FeelsLike.Day, d.units), + Eve: convertTempToValue(d.FeelsLike.Eve, d.units), + Night: convertTempToValue(d.FeelsLike.Night, d.units), + }, + Weather: weatherConditions(d.Weather).convert(), }) } return out } + +func convertValueToTemp(value float64, owmUnits *Unit) unit.Temperature { + switch owmUnits { + case &Units.METRIC: + return unit.FromCelsius(value) + case &Units.IMPERIAL: + return unit.FromFahrenheit(value) + default: + return unit.FromKelvin(value) + } +} + +func convertTempToValue(value unit.Temperature, owmUnits *Unit) float64 { + switch owmUnits { + case &Units.METRIC: + return value.Celsius() + case &Units.IMPERIAL: + return value.Fahrenheit() + default: + return value.Kelvin() + } +} diff --git a/onecall/model_test.go b/onecall/model_test.go index d2e5df7..bb2b596 100644 --- a/onecall/model_test.go +++ b/onecall/model_test.go @@ -43,7 +43,7 @@ func TestParse(t *testing.T) { }, }, } - parsedAndConverted := raw.Parse().convert() + parsedAndConverted := raw.Parse(nil).convert() require.NotNil(t, parsedAndConverted) require.Equal(t, *parsedAndConverted, raw) } @@ -56,7 +56,7 @@ func TestParseTestData(t *testing.T) { var raw OneCallResponseRaw json.NewDecoder(bytes.NewBuffer(b)).Decode(&raw) - parsedAndConverted := raw.Parse() + parsedAndConverted := raw.Parse(nil) require.NotNil(t, parsedAndConverted) require.Equal(t, *parsedAndConverted.convert(), raw) diff --git a/onecall/unit.go b/onecall/unit.go deleted file mode 100644 index e9884a8..0000000 --- a/onecall/unit.go +++ /dev/null @@ -1,11 +0,0 @@ -package onecall - -type unit int - -//go:generate goenums unit.go -const ( - unknownUnit unit = iota // invalid - standard - metric - imperial -) diff --git a/onecall/units.go b/onecall/units.go new file mode 100644 index 0000000..ef4a67b --- /dev/null +++ b/onecall/units.go @@ -0,0 +1,11 @@ +package onecall + +type units int + +//go:generate goenums units.go +const ( + unknownUnits units = iota // invalid + standard + metric + imperial +) diff --git a/onecall/units_enums.go b/onecall/units_enums.go index 73de413..2b22c98 100644 --- a/onecall/units_enums.go +++ b/onecall/units_enums.go @@ -1,10 +1,10 @@ // DO NOT EDIT. -// code generated by goenums v0.4.3 at Aug 2 14:44:08. +// code generated by goenums v0.4.3 at Aug 3 11:19:00. // // github.com/zarldev/goenums // // using the command: -// goenums unit.go +// goenums units.go package onecall @@ -21,39 +21,39 @@ import ( // Unit is a type that represents a single enum value. // It combines the core information about the enum constant and it's defined fields. type Unit struct { - unit + units } // unitsContainer is the container for all enum values. // It is private and should not be used directly use the public methods on the Unit type. type unitsContainer struct { - UNKNOWNUNIT Unit - STANDARD Unit - METRIC Unit - IMPERIAL Unit + UNKNOWNUNITS Unit + STANDARD Unit + METRIC Unit + IMPERIAL Unit } // Units is a main entry point using the Unit type. // It it a container for all enum values and provides a convenient way to access all enum values and perform // operations, with convenience methods for common use cases. var Units = unitsContainer{ - UNKNOWNUNIT: Unit{ - unit: unknownUnit, + UNKNOWNUNITS: Unit{ + units: unknownUnits, }, STANDARD: Unit{ - unit: standard, + units: standard, }, METRIC: Unit{ - unit: metric, + units: metric, }, IMPERIAL: Unit{ - unit: imperial, + units: imperial, }, } // invalidUnit is an invalid sentinel value for Unit var invalidUnit = Unit{ - unit: -1, + units: -1, } // allSlice returns a slice of all enum values. @@ -155,10 +155,10 @@ func ParseUnit(input any) (Unit, error) { // unitsNameMap is a map of enum values to their Unit representation // It is used to convert string representations of enum values into their Unit representation. var unitsNameMap = map[string]Unit{ - "unknownUnit": Units.UNKNOWNUNIT, - "standard": Units.STANDARD, - "metric": Units.METRIC, - "imperial": Units.IMPERIAL, + "unknownUnits": Units.UNKNOWNUNITS, + "standard": Units.STANDARD, + "metric": Units.METRIC, + "imperial": Units.IMPERIAL, } // stringToUnit converts a string representation of an enum value into its Unit representation @@ -200,10 +200,10 @@ func ExhaustiveUnits(f func(Unit)) { // validUnits is a map of enum values to their validity var validUnits = map[Unit]bool{ - Units.UNKNOWNUNIT: false, - Units.STANDARD: true, - Units.METRIC: true, - Units.IMPERIAL: true, + Units.UNKNOWNUNITS: false, + Units.STANDARD: true, + Units.METRIC: true, + Units.IMPERIAL: true, } // IsValid checks whether the Units value is valid. @@ -303,25 +303,25 @@ func (u *Unit) UnmarshalYAML(b []byte) error { return nil } -// unitNames is a constant string slice containing all enum values cononical absolute names -const unitNames = "unknownUnitstandardmetricimperial" +// unitsNames is a constant string slice containing all enum values cononical absolute names +const unitsNames = "unknownUnitsstandardmetricimperial" -// unitNamesMap is a map of enum values to their canonical absolute -// name positions within the unitNames string slice -var unitNamesMap = map[Unit]string{ - Units.UNKNOWNUNIT: unitNames[0:11], - Units.STANDARD: unitNames[11:19], - Units.METRIC: unitNames[19:25], - Units.IMPERIAL: unitNames[25:33], +// unitsNamesMap is a map of enum values to their canonical absolute +// name positions within the unitsNames string slice +var unitsNamesMap = map[Unit]string{ + Units.UNKNOWNUNITS: unitsNames[0:12], + Units.STANDARD: unitsNames[12:20], + Units.METRIC: unitsNames[20:26], + Units.IMPERIAL: unitsNames[26:34], } // String implements the Stringer interface. // It returns the canonical absolute name of the enum value. func (u Unit) String() string { - if str, ok := unitNamesMap[u]; ok { + if str, ok := unitsNamesMap[u]; ok { return str } - return fmt.Sprintf("unit(%d)", u.unit) + return fmt.Sprintf("units(%d)", u.units) } // Compile-time check that all enum values are valid. @@ -332,7 +332,7 @@ func _() { // Re-run the goenums command to generate them again. // Does not identify newly added constant values unless order changes var x [4]struct{} - _ = x[unknownUnit] + _ = x[unknownUnits] _ = x[standard-1] _ = x[metric-2] _ = x[imperial-3] diff --git a/pkg/owm/owm.go b/pkg/owm/owm.go index 24f08ba..e50ad05 100644 --- a/pkg/owm/owm.go +++ b/pkg/owm/owm.go @@ -1,6 +1,7 @@ package owm import ( + "fmt" "log/slog" "net/http" @@ -69,12 +70,7 @@ func (c *Client) WithOneCall(opts *onecall.ClientOptions) *Client { opts.Logger = c.logger } - oc, err := onecall.NewClient(opts) - if err != nil { - c.logger.Error("unable to create onecall client", "error", err) - panic(err) - } - c.OneCall = oc + c.OneCall = onecall.NewClient(opts) return c } @@ -94,11 +90,37 @@ func (c *Client) WithGeo(opts *geo.ClientOptions) *Client { opts.Logger = c.logger } - geo, err := geo.NewClient(opts) + c.Geo = geo.NewClient(opts) + return c +} + +type GeoDirectOneCallResponse struct { + GeoDirect *geo.DirectData + OneCall *onecall.OneCallResponse +} + +// GetWeather is an opinionated convenience function that first performs geocoding and then gets the One Call forecast. +func (c *Client) GetWeather(query string, opts *onecall.OneCallOptions) (*GeoDirectOneCallResponse, error) { + if c.OneCall == nil || c.Geo == nil { + return nil, fmt.Errorf("both onecall and geo clients are needed") + } + + geo, err := c.Geo.Direct(query, &geo.GeoOptions{Limit: 1}) if err != nil { - c.logger.Error("unable to create geo client", "error", err) - panic(err) + return nil, fmt.Errorf("geo: %w", err) } - c.Geo = geo - return c + + if len(geo.Data) == 0 { + return nil, fmt.Errorf("no result matching '%v'", query) + } + + onecall, err := c.OneCall.OneCall(geo.Data[0].Lat, geo.Data[0].Lon, opts) + if err != nil { + return nil, fmt.Errorf("onecall: %w", err) + } + + return &GeoDirectOneCallResponse{ + GeoDirect: &geo.Data[0], + OneCall: onecall, + }, nil }