@ -0,0 +1 @@ | |||
weather-api |
@ -0,0 +1,56 @@ | |||
package main | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"github.com/google/go-querystring/query" | |||
) | |||
// DarkSky API endpoint | |||
var ( | |||
BaseURL = "https://api.darksky.net/forecast" | |||
) | |||
// DarkSky Api client | |||
type DarkSky interface { | |||
Forecast(request ForecastRequest) (ForecastResponse, error) | |||
} | |||
type darkSky struct { | |||
APIKey string | |||
Client *http.Client | |||
} | |||
// NewDarkSkyAPI creates a new DarkSky client | |||
func NewDarkSkyAPI(apiKey string) DarkSky { | |||
return &darkSky{apiKey, &http.Client{}} | |||
} | |||
// Forecast request a forecast | |||
func (d *darkSky) Forecast(request ForecastRequest) (ForecastResponse, error) { | |||
response := ForecastResponse{} | |||
url := d.buildRequestURL(request) | |||
err := get(d.Client, url, &response) | |||
return response, err | |||
} | |||
func (d *darkSky) buildRequestURL(request ForecastRequest) string { | |||
url := fmt.Sprintf("%s/%s/%f,%f", BaseURL, d.APIKey, request.Latitude, request.Longitude) | |||
if request.Time > 0 { | |||
url = url + fmt.Sprintf(",%d", request.Time) | |||
} | |||
values, _ := query.Values(request.Options) | |||
queryString := values.Encode() | |||
if len(queryString) > 0 { | |||
url = url + "?" + queryString | |||
} | |||
return url | |||
} |
@ -0,0 +1,60 @@ | |||
package main | |||
import ( | |||
"log" | |||
"net/http" | |||
"os" | |||
"strconv" | |||
"github.com/fvbock/endless" | |||
"github.com/gin-gonic/gin" | |||
) | |||
func main() { | |||
apikey := os.Getenv("DARK_SKY_API_KEY") | |||
if len(apikey) == 0 { | |||
log.Fatalln("DARK_SKY_API_KEY environment variable must be set.") | |||
} | |||
router := gin.Default() | |||
router.GET("/ping", func(c *gin.Context) { | |||
c.String(http.StatusOK, "pong") | |||
}) | |||
router.GET("/current_weather/:lat/:long", func(c *gin.Context) { | |||
lat, err := strconv.ParseFloat(c.Params.ByName("lat"), 64) | |||
long, err := strconv.ParseFloat(c.Params.ByName("long"), 64) | |||
if err != nil { | |||
c.JSON(http.StatusBadRequest, gin.H{"error": "Latitude and Longitude are required"}) | |||
return | |||
} | |||
response, err := currentWeather(lat, long, apikey) | |||
if err != nil { | |||
c.JSON(http.StatusExpectationFailed, gin.H{"error": err.Error()}) | |||
return | |||
} | |||
c.Header("Access-Control-Allow-Origin", "*") | |||
c.JSON(http.StatusOK, gin.H{"weather": response}) | |||
}) | |||
err := endless.ListenAndServe("localhost:8080", router) | |||
if err != nil { | |||
log.Fatalf("Error: %s\n", err) | |||
} | |||
} | |||
func currentWeather(lat, long float64, apikey string) (ForecastResponse, error) { | |||
client := NewDarkSkyAPI(apikey) | |||
request := ForecastRequest{} | |||
request.Latitude = lat | |||
request.Longitude = long | |||
request.Options = ForecastRequestOptions{ | |||
Exclude: "minutely", | |||
Lang: "en", | |||
Units: "us", | |||
} | |||
return client.Forecast(request) | |||
} |
@ -0,0 +1,74 @@ | |||
package main | |||
import ( | |||
"compress/gzip" | |||
"encoding/json" | |||
"errors" | |||
"io" | |||
"io/ioutil" | |||
"net/http" | |||
) | |||
func get(client *http.Client, url string, output interface{}) error { | |||
req, err := http.NewRequest("GET", url, nil) | |||
if err != nil { | |||
return err | |||
} | |||
req.Header.Add("Content-Type", "application/json; charset=utf-8") | |||
req.Header.Add("Accept-Encoding", "gzip") | |||
response, err := client.Do(req) | |||
if err != nil { | |||
return err | |||
} | |||
defer response.Body.Close() | |||
err = checkErrors(response) | |||
if err != nil { | |||
return err | |||
} | |||
body, err := decompress(response) | |||
if err != nil { | |||
return err | |||
} | |||
return decodeJson(body, &output) | |||
} | |||
func checkErrors(response *http.Response) error { | |||
if response.StatusCode != 200 { | |||
body, _ := ioutil.ReadAll(response.Body) | |||
return errors.New("Bad response: " + string(body)) | |||
} | |||
return nil | |||
} | |||
func decompress(response *http.Response) (io.Reader, error) { | |||
header := response.Header.Get("Content-Encoding") | |||
if len(header) < 1 { | |||
return response.Body, nil | |||
} | |||
reader, err := gzip.NewReader(response.Body) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return reader, nil | |||
} | |||
func decodeJson(body io.Reader, into interface{}) error { | |||
jsonDecoder := json.NewDecoder(body) | |||
return jsonDecoder.Decode(&into) | |||
} |
@ -0,0 +1,81 @@ | |||
package main | |||
// Timestamp is an int64 timestamp | |||
type Timestamp int64 | |||
// ForecastRequest contains all available options for requesting a forecast | |||
type ForecastRequest struct { | |||
Latitude float64 | |||
Longitude float64 | |||
Time Timestamp | |||
Options ForecastRequestOptions | |||
} | |||
// ForecastRequestOptions are optional and passed as query parameters | |||
type ForecastRequestOptions struct { | |||
Exclude string `url:"exclude,omitempty"` | |||
Extend string `url:"extend,omitempty"` | |||
Lang string `url:"lang,omitempty"` | |||
Units string `url:"units,omitempty"` | |||
} | |||
// ForecastResponse is the response containing all requested properties | |||
type ForecastResponse struct { | |||
Latitude float64 `json:"latitude,omitempty"` | |||
Longitude float64 `json:"longitude,omitempty"` | |||
Timezone string `json:"timezone,omitempty"` | |||
Currently *DataPoint `json:"currently,omitempty"` | |||
Minutely *DataBlock `json:"minutely,omitempty"` | |||
Hourly *DataBlock `json:"hourly,omitempty"` | |||
Daily *DataBlock `json:"daily,omitempty"` | |||
Alerts []*Alert `json:"alerts,omitempty"` | |||
Flags *Flags `json:"flags,omitempty"` | |||
} | |||
// DataPoint contains various properties, each representing the average (unless otherwise specified) of a particular weather phenomenon occurring during a period of time. | |||
type DataPoint struct { | |||
ApparentTemperature float64 `json:"apparentTemperature,omitempty"` | |||
ApparentTemperatureHigh float64 `json:"apparentTemperatureHigh,omitempty"` | |||
ApparentTemperatureLow float64 `json:"apparentTemperatureLow,omitempty"` | |||
Humidity float64 `json:"humidity,omitempty"` | |||
Icon string `json:"icon"` | |||
MoonPhase float64 `json:"moonPhase,omitempty"` | |||
Summary string `json:"summary,omitempty"` | |||
SunriseTime Timestamp `json:"sunriseTime,omitempty"` | |||
SunsetTime Timestamp `json:"sunsetTime,omitempty"` | |||
Temperature float64 `json:"temperature,omitempty"` | |||
TemperatureHigh float64 `json:"temperatureHigh"` | |||
TemperatureLow float64 `json:"temperatureLow"` | |||
TemperatureMax float64 `json:"temperatureMax"` | |||
TemperatureMin float64 `json:"temperatureMin"` | |||
Time Timestamp `json:"time,omitempty"` | |||
Visibility float64 `json:"visibility,omitempty"` | |||
WindBearing float64 `json:"windBearing"` | |||
WindGust float64 `json:"windGust"` | |||
WindSpeed float64 `json:"windSpeed"` | |||
} | |||
// DataBlock represents the various weather phenomena occurring over a period of time | |||
type DataBlock struct { | |||
Summary string `json:"summary,omitempty"` | |||
Icon string `json:"icon,omitempty"` | |||
Data []DataPoint `json:"data,omitempty"` | |||
} | |||
// Alert contains objects representing the severe weather warnings issued for the requested location by a governmental authority | |||
type Alert struct { | |||
Title string `json:"title,omitempty"` | |||
Severity string `json:"severity,omitempty"` | |||
Description string `json:"description,omitempty"` | |||
Expires Timestamp `json:"expires,omitempty"` | |||
Regions []string `json:"regions,omitempty"` | |||
Time Timestamp `json:"time,omitempty"` | |||
URI string `json:"uri,omitempty"` | |||
} | |||
// Flags contains various metadata information related to the request | |||
type Flags struct { | |||
DarkSkyUnavailable string `json:"darksky-unavailable,omitempty"` | |||
Sources []string `json:"sources,omitempty"` | |||
Units string `json:"units,omitempty"` | |||
} |