first pass at assessment
This commit is contained in:
36
README.md
36
README.md
@ -19,3 +19,39 @@ From recruiter:
|
|||||||
> * We do not expect a production-ready service, but you might want to comment on your shortcuts.
|
> * We do not expect a production-ready service, but you might want to comment on your shortcuts.
|
||||||
> * The submitted project should build and have brief instructions so we can verify that it works.
|
> * The submitted project should build and have brief instructions so we can verify that it works.
|
||||||
> * You may write in whatever language or stack you're most comfortable in
|
> * You may write in whatever language or stack you're most comfortable in
|
||||||
|
|
||||||
|
This HTTP service gets a short forecast (results in JSON) for a given latitude and longitude.
|
||||||
|
|
||||||
|
### Build requirements
|
||||||
|
A local installation of Go is needed. Instructions to install Go can be found here: https://go.dev/learn/. I've used Ubuntu, but any OS should work.
|
||||||
|
|
||||||
|
### Building and Running
|
||||||
|
In the top-level-directory, run the following command:
|
||||||
|
|
||||||
|
`go build main.go`
|
||||||
|
|
||||||
|
This will build a binary in that folder, which can be run without arguments.
|
||||||
|
|
||||||
|
`./main`
|
||||||
|
|
||||||
|
To call the endpoint, use an HTTP client to send a GET request to `localhost:8080/forecast`. The payload should look like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"latitude": 48.29944,
|
||||||
|
"longitude": -116.56
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The result should look like this:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"shortForecast": "Mostly Sunny",
|
||||||
|
"temperature": "hot"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shortcuts
|
||||||
|
* This should be containerized in something like Docker
|
||||||
|
* The code is all in main.go, but if this project was to grow, it should be broken down.
|
||||||
|
* There are no tests, but `httptest` should be used to test this. With rules, the temperature could be tested as well.
|
||||||
|
* Logging is sparse, but should be enough to test for this assessment.
|
||||||
|
2
go.mod
2
go.mod
@ -1,3 +1,5 @@
|
|||||||
module git.simplesystems.tech/jeff/current-weather
|
module git.simplesystems.tech/jeff/current-weather
|
||||||
|
|
||||||
go 1.24.3
|
go 1.24.3
|
||||||
|
|
||||||
|
require github.com/icodealot/noaa v0.0.2
|
||||||
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/icodealot/noaa v0.0.2 h1:nL21mFSxJUBog7/0/vakIfA109T2A8/JpowzEzL9O+w=
|
||||||
|
github.com/icodealot/noaa v0.0.2/go.mod h1:vPMSrP4zBvlbWC34qtUR5w64dCp0xSRjkLy9/Ky81go=
|
82
main.go
82
main.go
@ -1,5 +1,85 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
func main() {
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/icodealot/noaa"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForecastReq struct {
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForecastResp struct {
|
||||||
|
ShortForecast string `json:"shortForecast"`
|
||||||
|
Temperature string `json:"temperature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Using the default HTTP server
|
||||||
|
// SHORTCUT: no TLS
|
||||||
|
// SHORTCUT: The default HTTP server is good enough for small projects, but would need to be better configured for production
|
||||||
|
// SHORTCUT: no rate limiting
|
||||||
|
http.HandleFunc("GET /forecast", forecast)
|
||||||
|
|
||||||
|
// SHORTCUT: This server is only stopped when the process stops. This should have a graceful shutdown.
|
||||||
|
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||||
|
slog.Error("stopping server", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forecast(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
// SHORTCUT: not checking the body length to see if it is unreasonably large
|
||||||
|
|
||||||
|
// Decode request
|
||||||
|
var foreReq ForecastReq
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&foreReq); err != nil {
|
||||||
|
slog.Error("decode forecast request error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that request is in range
|
||||||
|
if foreReq.Longitude > 180 || foreReq.Longitude < -180 {
|
||||||
|
slog.Error("invalid request", "latitude", foreReq.Latitude, "longitude", foreReq.Longitude)
|
||||||
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call out to NOAA for forecast
|
||||||
|
resp, err := noaa.Forecast(fmt.Sprintf("%f", foreReq.Latitude), fmt.Sprintf("%f", foreReq.Longitude))
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("noaa forecast request error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find today's forecast
|
||||||
|
var result ForecastResp
|
||||||
|
// SHORTCUT: assuming [0] is the closest to "today", and Periods has elements
|
||||||
|
p := resp.Periods[0]
|
||||||
|
result.ShortForecast = p.Summary
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case p.Temperature < 32:
|
||||||
|
result.Temperature = "cold"
|
||||||
|
case p.Temperature < 70:
|
||||||
|
result.Temperature = "moderate"
|
||||||
|
default:
|
||||||
|
result.Temperature = "hot"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
if err := json.NewEncoder(w).Encode(result); err != nil {
|
||||||
|
slog.Error("encode forecast response error", err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user