// Copyright 2022 Brian J. Downs // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package openweathermap import ( "errors" "net/http" "strings" ) var ( errUnitUnavailable = errors.New("unit unavailable") errLangUnavailable = errors.New("language unavailable") errInvalidKey = errors.New("invalid api key") errInvalidOption = errors.New("invalid option") errInvalidHttpClient = errors.New("invalid http client") errForecastUnavailable = errors.New("forecast unavailable") errExcludesUnavailable = errors.New("onecall excludes unavailable") errCountOfCityIDs = errors.New("count of ids should not be more than 20 per request") ) // DataUnits represents the character chosen to represent the temperature notation var DataUnits = map[string]string{"C": "metric", "F": "imperial", "K": "internal"} var ( baseURL = "https://api.openweathermap.org/data/2.5/weather?%s" onecallURL = "https://api.openweathermap.org/data/3.0/onecall%s" iconURL = "https://openweathermap.org/img/w/%s" groupURL = "http://api.openweathermap.org/data/2.5/group?%s" stationURL = "https://api.openweathermap.org/data/2.5/station?id=%d" forecast5Base = "https://api.openweathermap.org/data/2.5/forecast?appid=%s&%s&mode=json&units=%s&lang=%s&cnt=%d" forecast16Base = "https://api.openweathermap.org/data/2.5/forecast/daily?appid=%s&%s&mode=json&units=%s&lang=%s&cnt=%d" historyURL = "https://api.openweathermap.org/data/2.5/history/%s" pollutionURL = "https://api.openweathermap.org/data/2.5/air_pollution?appid=%s&lat=%s&lon=%s" uvURL = "https://api.openweathermap.org/data/2.5/" dataPostURL = "https://openweathermap.org/data/post" ) // LangCodes holds all supported languages to be used // inspried and sourced from @bambocher (github.com/bambocher) var LangCodes = map[string]string{ "AF": "Afrikaans", "AL": "Albanian", "AR": "Arabic", "AZ": "Azerbaijani", "BG": "Bulgarian", "CA": "Catalan", "CZ": "Czech", "DA": "Danish", "DE": "German", "EL": "Greek", "EN": "English", "ES": "Spanish", "EU": "Basque", "FA": "Persian (Farsi)", "FI": "Finnish", "FR": "French", "GL": "Galician", "HE": "Hebrew", "HI": "Hindi", "HR": "Croatian", "HU": "Hungarian", "ID": "Indonesian", "IT": "Italian", "JA": "Japanese", "KR": "Korean", "LA": "Latvian", "LT": "Lithuanian", "MK": "Macedonian", "NL": "Dutch", "NO": "Norwegian", "PL": "Polish", "PT": "Portuguese", "PT_BR": "Português Brasil", "RO": "Romanian", "RU": "Russian", "SE": "Swedish", "SK": "Slovak", "SL": "Slovenian", "SP": "Spanish", "SR": "Serbian", "SV": "Swedish", "TH": "Thai", "TR": "Turkish", "UA": "Ukrainian", "UK": "Ukrainian", "VI": "Vietnamese", "ZH_CN": "Chinese Simplified", "ZH_TW": "Chinese Traditional", "ZU": "Zulu", } // Exclude holds all supported excludes option to be used const ( ExcludeCurrent = "current" ExcludeMinutely = "minutely" ExcludeHourly = "hourly" ExcludeDaily = "daily" ExcludeAlerts = "alerts" ) var Excludes []string = []string{ ExcludeCurrent, ExcludeMinutely, ExcludeHourly, ExcludeDaily, ExcludeAlerts, } // Config will hold default settings to be passed into the // "NewCurrent, NewForecast, etc}" functions. type Config struct { Mode string // user choice of JSON or XML Unit string // measurement for results to be displayed. F, C, or K Lang string // should reference a key in the LangCodes map APIKey string // API Key for connecting to the OWM Username string // Username for posting data Password string // Pasword for posting data } // APIError returned on failed API calls. type APIError struct { Message string `json:"message"` COD string `json:"cod"` } // Coordinates struct holds longitude and latitude data in returned // JSON or as parameter data for requests using longitude and latitude. type Coordinates struct { Longitude float64 `json:"lon"` Latitude float64 `json:"lat"` } // Sys struct contains general information about the request // and the surrounding area for where the request was made. type Sys struct { Type int `json:"type"` ID int `json:"id"` Message float64 `json:"message"` Country string `json:"country"` Sunrise int `json:"sunrise"` Sunset int `json:"sunset"` } // Wind struct contains the speed and degree of the wind. type Wind struct { Speed float64 `json:"speed"` Deg float64 `json:"deg"` } // Weather struct holds high-level, basic info on the returned // data. type Weather struct { ID int `json:"id"` Main string `json:"main"` Description string `json:"description"` Icon string `json:"icon"` } // Main struct contains the temperates, humidity, pressure for the request. type Main struct { Temp float64 `json:"temp"` TempMin float64 `json:"temp_min"` TempMax float64 `json:"temp_max"` FeelsLike float64 `json:"feels_like"` Pressure float64 `json:"pressure"` SeaLevel float64 `json:"sea_level"` GrndLevel float64 `json:"grnd_level"` Humidity int `json:"humidity"` } // Clouds struct holds data regarding cloud cover. type Clouds struct { All int `json:"all"` } // return key // } func setKey(key string) (string, error) { if err := ValidAPIKey(key); err != nil { return "", err } return key, nil } // ValidDataUnit makes sure the string passed in is an accepted // unit of measure to be used for the return data. func ValidDataUnit(u string) bool { for d := range DataUnits { if u == d { return true } } return false } // ValidLangCode makes sure the string passed in is an // acceptable lang code. func ValidLangCode(c string) bool { for d := range LangCodes { if c == d { return true } } return false } // ValidDataUnitSymbol makes sure the string passed in is an // acceptable data unit symbol. func ValidDataUnitSymbol(u string) bool { for _, d := range DataUnits { if u == d { return true } } return false } // ValidExcludes makes sure the string passed in is an // acceptable excludes options. func ValidExcludes(e []string) (string, error) { list := make([]string, 0) for _, v := range e { vl := strings.ToLower(v) notFound := true for _, d := range Excludes { if d == vl { list = append(list, v) notFound = false break } } if notFound { return "", errExcludesUnavailable } } return strings.Join(list, ","), nil } // ValidAPIKey makes sure that the key given is a valid one func ValidAPIKey(key string) error { if len(key) > 64 { return errInvalidKey } return nil } // CheckAPIKeyExists will see if an API key has been set. func (c *Config) CheckAPIKeyExists() bool { return len(c.APIKey) > 1 } // Settings holds the client settings type Settings struct { client *http.Client } // NewSettings returns a new Setting pointer with default http client. func NewSettings() *Settings { return &Settings{ client: http.DefaultClient, } } // Optional client settings type Option func(s *Settings) error // WithHttpClient sets custom http client when creating a new Client. func WithHttpClient(c *http.Client) Option { return func(s *Settings) error { if c == nil { return errInvalidHttpClient } s.client = c return nil } } // setOptions sets Optional client settings to the Settings pointer func setOptions(settings *Settings, options []Option) error { for _, option := range options { if option == nil { return errInvalidOption } err := option(settings) if err != nil { return err } } return nil }