commit 840c649b3e72092171af2ef679e343a1d04b9c84 Author: Levi Olson Date: Thu Jan 3 12:00:19 2019 -0600 Initial working API diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b64402 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +weather-api diff --git a/darksky.go b/darksky.go new file mode 100644 index 0000000..d0c1727 --- /dev/null +++ b/darksky.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9dbb1a1 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/rest.go b/rest.go new file mode 100644 index 0000000..2017218 --- /dev/null +++ b/rest.go @@ -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) +} diff --git a/structs.go b/structs.go new file mode 100644 index 0000000..0815e72 --- /dev/null +++ b/structs.go @@ -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"` +}