diff --git a/README.md b/README.md index a17172e..c340cb4 100644 --- a/README.md +++ b/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. > * 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 + +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. diff --git a/go.mod b/go.mod index 1bf8fee..4e5fac1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module git.simplesystems.tech/jeff/current-weather go 1.24.3 + +require github.com/icodealot/noaa v0.0.2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b066e83 --- /dev/null +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 7905807..9bbfd7a 100644 --- a/main.go +++ b/main.go @@ -1,5 +1,85 @@ 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 + } }