diff --git a/internal/config/config.go b/internal/config/config.go index 60c8386..23c90d1 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,6 +91,10 @@ type Geo struct { type Nominatim struct { Name string + // Type describes the type of the location service. + // Supported types: iq. + Type string + URL string Token string @@ -112,9 +116,16 @@ func Default() *Config { Nominatim: []Nominatim{ { Name: "locationiq", + Type: "iq", URL: "https://eu1.locationiq.com/v1/search", Token: os.Getenv("NOMINATIM_LOCATIONIQ"), }, + { + Name: "opencage", + Type: "opencage", + URL: "https://api.opencagedata.com/geocode/v1/json", + Token: os.Getenv("NOMINATIM_OPENCAGE"), + }, }, }, Logging{ diff --git a/internal/geo/location/nominatim.go b/internal/geo/location/nominatim.go index 2927761..a1b214f 100644 --- a/internal/geo/location/nominatim.go +++ b/internal/geo/location/nominatim.go @@ -5,7 +5,6 @@ import ( "fmt" "io/ioutil" "net/http" - "net/url" "github.com/chubin/wttr.in/internal/types" log "github.com/sirupsen/logrus" @@ -15,69 +14,64 @@ type Nominatim struct { name string url string token string + typ string } -type NominatimLocation struct { - Name string `db:"name,key"` - Lat string `db:"lat"` - Lon string `db:"lon"` - //nolint:tagliatelle - Fullname string `db:"displayName" json:"display_name"` +type locationQuerier interface { + Query(*Nominatim, string) (*Location, error) } -func NewNominatim(name, url, token string) *Nominatim { +func NewNominatim(name, typ, url, token string) *Nominatim { return &Nominatim{ name: name, url: url, token: token, + typ: typ, } } func (n *Nominatim) Query(location string) (*Location, error) { - var ( - result []NominatimLocation + var data locationQuerier - errResponse struct { - Error string - } - ) + switch n.typ { + case "iq": + data = &locationIQ{} + case "opencage": + data = &locationOpenCage{} + default: + return nil, fmt.Errorf("%s: %w", n.name, types.ErrUnknownLocationService) + } + + return data.Query(n, location) +} - urlws := fmt.Sprintf( - "%s?q=%s&format=json&accept-language=native&limit=1&key=%s", - n.url, url.QueryEscape(location), n.token) +func makeQuery(url string, result interface{}) error { + var errResponse struct { + Error string + } - log.Debugln("nominatim:", urlws) - resp, err := http.Get(urlws) + log.Debugln("nominatim:", url) + resp, err := http.Get(url) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } err = json.Unmarshal(body, &errResponse) if err == nil && errResponse.Error != "" { - return nil, fmt.Errorf("%w: %s: %s", types.ErrUpstream, n.name, errResponse.Error) + return fmt.Errorf("%w: %s", types.ErrUpstream, errResponse.Error) } log.Debugln("nominatim: response: ", string(body)) err = json.Unmarshal(body, &result) if err != nil { - return nil, fmt.Errorf("%s: %w", n.name, err) + return err } - if len(result) != 1 { - return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) - } - - nl := &result[0] - - return &Location{ - Lat: nl.Lat, - Lon: nl.Lon, - Fullname: nl.Fullname, - }, nil + return nil } diff --git a/internal/geo/location/nominatim_locationiq.go b/internal/geo/location/nominatim_locationiq.go new file mode 100644 index 0000000..0a8d5fb --- /dev/null +++ b/internal/geo/location/nominatim_locationiq.go @@ -0,0 +1,39 @@ +package location + +import ( + "fmt" + "net/url" + + "github.com/chubin/wttr.in/internal/types" +) + +type locationIQ []struct { + Name string `db:"name,key"` + Lat string `db:"lat"` + Lon string `db:"lon"` + //nolint:tagliatelle + Fullname string `db:"displayName" json:"display_name"` +} + +func (data *locationIQ) Query(n *Nominatim, location string) (*Location, error) { + url := fmt.Sprintf( + "%s?q=%s&format=json&language=native&limit=1&key=%s", + n.url, url.QueryEscape(location), n.token) + + err := makeQuery(url, data) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + if len(*data) != 1 { + return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) + } + + nl := &(*data)[0] + + return &Location{ + Lat: nl.Lat, + Lon: nl.Lon, + Fullname: nl.Fullname, + }, nil +} diff --git a/internal/geo/location/nominatim_opencage.go b/internal/geo/location/nominatim_opencage.go new file mode 100644 index 0000000..577accd --- /dev/null +++ b/internal/geo/location/nominatim_opencage.go @@ -0,0 +1,42 @@ +package location + +import ( + "fmt" + "net/url" + + "github.com/chubin/wttr.in/internal/types" +) + +type locationOpenCage struct { + Results []struct { + Name string `db:"name,key"` + Geometry struct { + Lat float64 `db:"lat"` + Lng float64 `db:"lng"` + } + Fullname string `json:"formatted"` + } `json:"results"` +} + +func (data *locationOpenCage) Query(n *Nominatim, location string) (*Location, error) { + url := fmt.Sprintf( + "%s?q=%s&language=native&limit=1&key=%s", + n.url, url.QueryEscape(location), n.token) + + err := makeQuery(url, data) + if err != nil { + return nil, fmt.Errorf("%s: %w", n.name, err) + } + + if len(data.Results) != 1 { + return nil, fmt.Errorf("%w: %s: invalid response", types.ErrUpstream, n.name) + } + + nl := data.Results[0] + + return &Location{ + Lat: fmt.Sprint(nl.Geometry.Lat), + Lon: fmt.Sprint(nl.Geometry.Lng), + Fullname: nl.Fullname, + }, nil +} diff --git a/internal/geo/location/search.go b/internal/geo/location/search.go index c05cdb0..2efefeb 100644 --- a/internal/geo/location/search.go +++ b/internal/geo/location/search.go @@ -14,7 +14,7 @@ type Searcher struct { func NewSearcher(config *config.Config) *Searcher { providers := []Provider{} for _, p := range config.Geo.Nominatim { - providers = append(providers, NewNominatim(p.Name, p.URL, p.Token)) + providers = append(providers, NewNominatim(p.Name, p.Type, p.URL, p.Token)) } return &Searcher{ diff --git a/internal/types/errors.go b/internal/types/errors.go index 62c36f1..e081774 100644 --- a/internal/types/errors.go +++ b/internal/types/errors.go @@ -9,4 +9,6 @@ var ( // ErrNoServersConfigured means that there are no servers to run. ErrNoServersConfigured = errors.New("no servers configured") + + ErrUnknownLocationService = errors.New("unknown location service") )