Initial commit

This commit is contained in:
EricNeid 2019-12-09 15:09:21 +01:00
commit 084da1a39f
13 changed files with 600 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*.key
debug.test
.vscode
*.exe

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Eric Neidhardt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
build:
cd cmd/openweatherclient && go build
test:
go test ./...

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# go - openweatherapi
This Repo contains golang library to query OpenWetherMaps (<http://openweathermap.org/>) for weather information.
* current weather: http://openweathermap.org/current
* 5 days forecast: http://openweathermap.org/forecast5
## Install
```bash
go get github.com/EricNeid/openweather
```
## Documentation
Is available on ``godoc``:
<https://godoc.org/github.com/EricNeid/openweather>
## Examples
Consuming the library:
```go
import "github.com/EricNeid/openweather"
// create a query
q := openweather.NewQueryForCity(readAPIKey(), "Berlin,de")
// obtain data
resp, err := q.Weather()
// enjoy
fmt.Println(resp.Name) // Berlin
fmt.Println(resp.Weather[0].Description) // misc
fmt.Println(resp.Main.Temp) // 1
```
See the test files for more example.
A simple client for testing is also included:
```bash
go build cmd/openweatherclient
openweatherclient -key <OpenWeather API Key> -city Berlin,de
```

131
api.go Normal file
View File

@ -0,0 +1,131 @@
// Package openweather contains helper functions to query
// OpenWeatherMaps (http://openweathermap.org/) for weather information.
// Currently the current weather API (http://openweathermap.org/current) and the
// 5 days forecast API (http://openweathermap.org/forecast5) are supported.
package openweather
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
)
// WeatherRaw downloads current weather data from openweathermap and return them as string.
func (query Query) WeatherRaw() (json string, err error) {
bytes, err := download(WeatherURL(query))
if err != nil {
return "", err
}
return string(bytes), nil
}
// Weather downloads current weather data from openweathermap and return them as WeatherData.
func (query Query) Weather() (*CurrentWeather, error) {
bytes, err := download(WeatherURL(query))
if err != nil {
return nil, err
}
dataPtr := &CurrentWeather{}
err = json.Unmarshal(bytes, dataPtr)
return dataPtr, err
}
// DailyForecast5Raw downloads 5 days forecast data from openweathermap and return them as string.
func (query Query) DailyForecast5Raw() (json string, err error) {
bytes, err := download(DailyForecast5URL(query))
if err != nil {
return "", err
}
return string(bytes), nil
}
// DailyForecast5 downloads 5 days forecast data from openweathermap and return them as DailyForecast5.
func (query Query) DailyForecast5() (*DailyForecast5, error) {
bytes, err := download(DailyForecast5URL(query))
if err != nil {
return nil, err
}
dataPtr := &DailyForecast5{}
err = json.Unmarshal(bytes, dataPtr)
return dataPtr, err
}
// DailyForecast16Raw downloads 16 days forecast data from openweathermap and return them as string.
// Warning: the 16 days forecast requires a paid account.
func (query Query) DailyForecast16Raw() (json string, err error) {
bytes, err := download(DailyForecast16URL(query))
if err != nil {
return "", err
}
return string(bytes), nil
}
// DailyForecast16 downloads 16 days forecast data from openweathermap and return them as DailyForecast16.
// Warning: the 16 days forecast requires a paid account.
func (query Query) DailyForecast16() (*DailyForecast16, error) {
bytes, err := download(DailyForecast16URL(query))
if err != nil {
return nil, err
}
dataPtr := &DailyForecast16{}
err = json.Unmarshal(bytes, dataPtr)
return dataPtr, err
}
func download(url string) (res []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
// WeatherIconURL returns an url to download matching icon for
// given weather id.
func WeatherIconURL(iconID string) (url string) {
return "http://openweathermap.org/img/w/" + iconID + ".png"
}
// DailyForecast5URL returns a matching url for the given query which can be used to obtain the 5 days forecast
// from openweathermap.org.
func DailyForecast5URL(q Query) string {
return "http://api.openweathermap.org/data/2.5/forecast/daily" + formatURLQuery(q)
}
// DailyForecast16URL returns a matching url for the given query which can be used to obtain the 16 days forecast
// from openweathermap.org.
func DailyForecast16URL(q Query) string {
return "http://api.openweathermap.org/data/2.5/forecast/daily" + formatURLQuery(q) + "&cnt=16"
}
// WeatherURL returns a matching url for the given query which can be used to obtain the current weather information
// from openweathermap.org.
func WeatherURL(q Query) string {
return "http://api.openweathermap.org/data/2.5/weather" + formatURLQuery(q)
}
func formatURLQuery(q Query) string {
queryType := q.queryType
queryValue := q.Query
var query string
if queryType == queryTypeGeo {
params := strings.Split(queryValue, "|") // expected format is lat|long
lat := params[0]
lon := params[1]
query = fmt.Sprintf("?lat=%s&lon=%s", lat, lon)
} else {
query = fmt.Sprintf("?%s=%s", queryType, queryValue)
}
query = query + fmt.Sprintf("&appid=%s&units=%s", q.APIKey, q.Unit)
return query
}

64
api_test.go Normal file
View File

@ -0,0 +1,64 @@
package openweather
import (
"io/ioutil"
"testing"
"github.com/EricNeid/openweather/internal/test"
)
const apiKeyFile = "testdata/api.key"
const cityBerlin = "Berlin,de"
func readAPIKey() string {
key, err := ioutil.ReadFile(apiKeyFile)
if err != nil {
panic(`
Cannot run test, you must provide openweathermap api key.
Expected: testdata/api.key
See https://home.openweathermap.org/users/sign_up
for information how to obtain a key`)
}
return string(key)
}
func TestForecastRaw(t *testing.T) {
// arrange
q := NewQueryForCity(readAPIKey(), cityBerlin)
// action
resp, err := q.DailyForecast5Raw()
// verify
test.Ok(t, err)
test.Assert(t, len(resp) > 0, "Received empty response")
}
func TestWeatherRaw(t *testing.T) {
// arrange
q := NewQueryForCity(readAPIKey(), cityBerlin)
// action
resp, err := q.WeatherRaw()
// verify
test.Ok(t, err)
test.Assert(t, len(resp) > 0, "Received empty response")
}
func TestWeather(t *testing.T) {
// arrange
q := NewQueryForCity(readAPIKey(), cityBerlin)
// action
data, err := q.Weather()
// verify
test.Ok(t, err)
test.Equals(t, "Berlin", data.Name)
}
func TestDailyForecast(t *testing.T) {
// arrange
q := NewQueryForCity(readAPIKey(), cityBerlin)
// action
data, err := q.DailyForecast5()
// verify
test.Ok(t, err)
test.Equals(t, "Berlin", data.City.Name)
}

View File

@ -0,0 +1,29 @@
package main
import (
"flag"
"fmt"
"github.com/EricNeid/openweather"
)
func main() {
keyPtr := flag.String("key", "", "Supply valid api key, see https://home.openweathermap.org/users/sign_up for details.")
cityPtr := flag.String("city", "", "City to look up")
flag.Parse()
if len(*keyPtr) == 0 || len(*cityPtr) == 0 {
fmt.Println("Usage: simpleclient.exe -key <api-key> -city <city-to-query>")
return
}
query := openweather.NewQueryForCity(*keyPtr, *cityPtr, "metric")
weather, err := query.WeatherRaw()
if err != nil {
fmt.Println("Error while retrieving data: " + err.Error())
return
}
fmt.Println(weather)
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module github.com/EricNeid/openweather
go 1.13

0
go.sum Normal file
View File

View File

@ -0,0 +1,36 @@
package test
import (
"fmt"
"path/filepath"
"reflect"
"runtime"
"testing"
)
// Assert fails the test if the condition is false.
func Assert(t *testing.T, condition bool, msg string, v ...interface{}) {
if !condition {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...)
t.FailNow()
}
}
// Ok fails the test if an err is not nil.
func Ok(t *testing.T, err error) {
if err != nil {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error())
t.FailNow()
}
}
// Equals fails the test if exp is not equal to act.
func Equals(t *testing.T, exp, act interface{}) {
if !reflect.DeepEqual(exp, act) {
_, file, line, _ := runtime.Caller(1)
fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act)
t.FailNow()
}
}

123
model.go Normal file
View File

@ -0,0 +1,123 @@
package openweather
// CurrentWeather represents unmarshalled data from openweathermap
// for the current weather API (http://openweathermap.org/current).
type CurrentWeather struct {
Coord struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
} `json:"coord"`
Weather []struct {
ID int `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
Icon string `json:"icon"`
} `json:"weather"`
Base string `json:"base"`
Main struct {
Temp float64 `json:"temp"`
Pressure int `json:"pressure"`
Humidity int `json:"humidity"`
TempMin float64 `json:"temp_min"`
TempMax float64 `json:"temp_max"`
} `json:"main"`
Wind struct {
Speed float64 `json:"speed"`
Deg int `json:"deg"`
} `json:"wind"`
Clouds struct {
All int `json:"all"`
} `json:"clouds"`
Rain struct {
ThreeH int `json:"3h"`
} `json:"rain"`
Dt int `json:"dt"`
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"`
} `json:"sys"`
ID int `json:"id"`
Name string `json:"name"`
Cod int `json:"cod"`
}
// DailyForecast5 represents unmarshalled data from openweathermap
// for the 5 days forecast weather API (http://openweathermap.org/forecast5).
type DailyForecast5 struct {
Cod string `json:"cod"`
Message float64 `json:"message"`
City struct {
GeonameID int `json:"geoname_id"`
Name string `json:"name"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
Country string `json:"country"`
Iso2 string `json:"iso2"`
Type string `json:"type"`
Population int `json:"population"`
} `json:"city"`
Cnt int `json:"cnt"`
List []struct {
Dt int `json:"dt"`
Temp struct {
Day float64 `json:"day"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Night float64 `json:"night"`
Eve float64 `json:"eve"`
Morn float64 `json:"morn"`
} `json:"temp"`
Pressure float64 `json:"pressure"`
Humidity int `json:"humidity"`
Weather []struct {
ID int `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
Icon string `json:"icon"`
} `json:"weather"`
Speed float64 `json:"speed"`
Deg int `json:"deg"`
Clouds int `json:"clouds"`
Snow float64 `json:"snow,omitempty"`
} `json:"list"`
}
// DailyForecast16 represents unmarshalled data from openweathermap
// for the 16 days forecast weather API (http://openweathermap.org/forecast16).
type DailyForecast16 struct {
Cod string `json:"cod"`
Message float64 `json:"message"`
City struct {
ID int `json:"id"`
Name string `json:"name"`
Coord struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
} `json:"coord"`
Country string `json:"country"`
} `json:"city"`
Cnt int `json:"cnt"`
List []struct {
Dt int `json:"dt"`
Temp struct {
Day float64 `json:"day"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Night float64 `json:"night"`
Eve float64 `json:"eve"`
Morn float64 `json:"morn"`
} `json:"temp"`
Pressure float64 `json:"pressure"`
Humidity int `json:"humidity"`
Weather []struct {
ID int `json:"id"`
Main string `json:"main"`
Description string `json:"description"`
Icon string `json:"icon"`
} `json:"weather"`
} `json:"list"`
}

74
query.go Normal file
View File

@ -0,0 +1,74 @@
package openweather
// Query represents a pending request to openweathermap.
type Query struct {
APIKey string
Unit string
Query string
queryType string
}
const queryTypeCity = "q"
const queryTypeZip = "zip"
const queryTypeID = "id"
const queryTypeGeo = "lat|lon"
// NewQueryForCity creates a query for openweathermap from city name.
// The unit is optional and defaults to metric.
func NewQueryForCity(apiKey string, city string, unit ...string) Query {
u := "metric"
if len(unit) > 0 {
u = unit[0]
}
return Query{
APIKey: apiKey,
Query: city,
queryType: queryTypeCity,
Unit: u,
}
}
// NewQueryForZip creates a query for openweathermap from zip code.
// The unit is optional and defaults to metric.
func NewQueryForZip(apiKey string, zip string, unit ...string) Query {
u := "metric"
if len(unit) > 0 {
u = unit[0]
}
return Query{
APIKey: apiKey,
Query: zip,
queryType: queryTypeZip,
Unit: u,
}
}
// NewQueryForID creates a query for openweathermap from city id.
// The unit is optional and defaults to metric.
func NewQueryForID(apiKey string, id string, unit ...string) Query {
u := "metric"
if len(unit) > 0 {
u = unit[0]
}
return Query{
APIKey: apiKey,
Query: id,
queryType: queryTypeID,
Unit: u,
}
}
// NewQueryForLocation creates a query for openweathermap from latitude and longitude.
// The unit is optional and defaults to metric.
func NewQueryForLocation(apiKey string, lat string, lon string, unit ...string) Query {
u := "metric"
if len(unit) > 0 {
u = unit[0]
}
return Query{
APIKey: apiKey,
Query: lat + "|" + lon,
queryType: queryTypeGeo,
Unit: u,
}
}

64
query_test.go Normal file
View File

@ -0,0 +1,64 @@
package openweather
import (
"testing"
"github.com/EricNeid/openweather/internal/test"
)
func TestNewQueryForCity(t *testing.T) {
// arrange
apiKey := "testKey"
location := cityBerlin
// action
q := NewQueryForCity(apiKey, location)
// verify
test.Equals(t, apiKey, q.APIKey)
test.Equals(t, location, q.Query)
test.Equals(t, "metric", q.Unit)
test.Equals(t, queryTypeCity, q.queryType)
// arrange
unit := "imperial"
// action
q = NewQueryForCity(apiKey, location, unit)
// verify
test.Equals(t, unit, q.Unit)
}
func TestNewQueryForZip(t *testing.T) {
// arrange
apiKey := "testKey"
zip := "12345"
// action
q := NewQueryForZip(apiKey, zip)
// verify
test.Equals(t, apiKey, q.APIKey)
test.Equals(t, zip, q.Query)
test.Equals(t, queryTypeZip, q.queryType)
}
func TestNewQueryForID(t *testing.T) {
// arrange
apiKey := "testKey"
id := "42"
// action
q := NewQueryForID(apiKey, id)
// verify
test.Equals(t, apiKey, q.APIKey)
test.Equals(t, id, q.Query)
test.Equals(t, queryTypeID, q.queryType)
}
func TestNewQueryForLocation(t *testing.T) {
// arrange
apiKey := "testKey"
lat := "51"
lon := "13"
// action
q := NewQueryForLocation(apiKey, lat, lon)
// verify
test.Equals(t, apiKey, q.APIKey)
test.Equals(t, lat+"|"+lon, q.Query)
test.Equals(t, queryTypeGeo, q.queryType)
}