From 6125a1279ccceeb9665fe40cf0da02f4b67c6cb0 Mon Sep 17 00:00:00 2001 From: tgs266 <siegel.tucker@gmail.com> Date: Tue, 21 Sep 2021 18:44:46 -0400 Subject: [PATCH] major update, includes testing, context wrapper for logger, seed data for corn, and begin process of enforcing the passing of context to downstream functions for logging purposes --- .gitignore | 4 +- README.md | 4 + config/routes.go | 3 + context/context.go | 23 ++++ controllers/data_download_controller.go | 4 +- controllers/gdd_controller.go | 10 +- controllers/misc_controller.go | 4 +- controllers/nomads_controller.go | 22 ++- controllers/seed_controllers.go | 47 +++++++ docs/docs.go | 170 +++++++++++++++++++++--- docs/swagger.json | 156 ++++++++++++++++++++-- docs/swagger.yaml | 105 ++++++++++++++- errors/dawn_errors.go | 41 +++--- go.mod | 10 +- go.sum | 17 +++ logger/logger.go | 6 +- main.go | 20 +-- models/csv.go | 6 +- models/enums/product_type.go | 4 +- models/freezing_dates.go | 4 +- models/gdd.go | 14 +- models/seeds.go | 66 +++++++++ persistence/entities/seed.go | 10 ++ persistence/mongodb.go | 66 +++++++-- services/data_download_service.go | 19 +-- services/freezing_date_service.go | 4 +- services/gdd_service.go | 24 +++- services/seed_service.go | 72 ++++++++++ utils/clip_test.go | 37 ++++++ utils/date_converter.go | 38 ++++++ utils/gdd_calculations.go | 27 +--- utils/gdd_calculations_test.go | 97 ++++++++++++++ 32 files changed, 1006 insertions(+), 128 deletions(-) create mode 100644 context/context.go create mode 100644 controllers/seed_controllers.go create mode 100644 models/seeds.go create mode 100644 persistence/entities/seed.go create mode 100644 services/seed_service.go create mode 100644 utils/clip_test.go create mode 100644 utils/date_converter.go create mode 100644 utils/gdd_calculations_test.go diff --git a/.gitignore b/.gitignore index 965dfc0..b861460 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.exe .idea -docs/ \ No newline at end of file +docs/ + +coverage.out \ No newline at end of file diff --git a/README.md b/README.md index 9662a36..c490275 100644 --- a/README.md +++ b/README.md @@ -11,5 +11,9 @@ To run: `go run main.go` Build to executable: `go build` To rebuild swagger: `swag init` +## Testing + +Call `go test -covermode=count -coverprofile=coverage.out ./...` +To generate code coverage html page, call `go tool cover -html=coverage.out` Swagger [Here](http://localhost:8080/api/weather/gdd-swagger/index.html#/) \ No newline at end of file diff --git a/config/routes.go b/config/routes.go index 7a89f6e..3fdcfac 100644 --- a/config/routes.go +++ b/config/routes.go @@ -17,4 +17,7 @@ func GddRoutes(route fiber.Router) { route.Get("freezing-dates", controllers.GetFreezingDates) route.Get("gdd/csv", controllers.GetCSVFile) + + route.Get("gdd/seeds/corn", controllers.GetCornSeedMaturityDate) + route.Get("gdd/seeds", controllers.GetSeedList) } diff --git a/context/context.go b/context/context.go new file mode 100644 index 0000000..d56374c --- /dev/null +++ b/context/context.go @@ -0,0 +1,23 @@ +package context + +import ( + "dawn-weather/logger" + + "github.com/gofiber/fiber/v2" +) + +type DawnCtx struct { + FiberCtx *fiber.Ctx +} + +func (ctx DawnCtx) INFO(message string) { + logger.INFO(ctx.FiberCtx, message) +} + +func (ctx DawnCtx) DEBUG(message string) { + logger.DEBUG(ctx.FiberCtx, message) +} + +func (ctx DawnCtx) TRACE(message string) { + logger.TRACE(ctx.FiberCtx, message) +} diff --git a/controllers/data_download_controller.go b/controllers/data_download_controller.go index c1a289b..46a58f1 100644 --- a/controllers/data_download_controller.go +++ b/controllers/data_download_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "dawn-weather/context" "dawn-weather/models" "dawn-weather/services" @@ -36,7 +37,8 @@ import ( // @Router /api/weather/gdd/csv [get] func GetCSVFile(c *fiber.Ctx) error { request := models.CSVRequest{}.Build(c) - fileText := services.GetDataDownload(c, request) + ctx := context.DawnCtx{FiberCtx: c} + fileText := services.GetDataDownload(ctx, request) e := c.Status(fiber.StatusOK).SendString(fileText) return e } diff --git a/controllers/gdd_controller.go b/controllers/gdd_controller.go index 3d40f8a..67818ab 100644 --- a/controllers/gdd_controller.go +++ b/controllers/gdd_controller.go @@ -1,12 +1,15 @@ package controllers import ( + "dawn-weather/context" "dawn-weather/models" "dawn-weather/services" "github.com/gofiber/fiber/v2" ) +var GetGddValues = services.GetGddValues + // GetDailyGdd godoc // @Summary Get gdd values // @Tags Gdd @@ -22,8 +25,9 @@ import ( // @Param accumulate query boolean true "Accumulate gdd values" // @Router /api/weather/gdd/daily [get] func GetDailyGdd(c *fiber.Ctx) error { - request := models.GddRequest{}.Build(c) - return c.Status(fiber.StatusOK).JSON(services.GetGddValues(c, request)) + ctx := context.DawnCtx{FiberCtx: c} + request := models.BuildGddRequest(ctx.FiberCtx) + return c.Status(fiber.StatusOK).JSON(GetGddValues(ctx, request)) } // GetNormalGdd godoc @@ -40,6 +44,6 @@ func GetDailyGdd(c *fiber.Ctx) error { // @Param accumulate query boolean true "Accumulate gdd values" // @Router /api/weather/gdd/normals [get] func GetNormalGdd(c *fiber.Ctx) error { - request := models.GddRequest{}.Build(c) + request := models.BuildGddRequest(c) return c.Status(fiber.StatusOK).JSON(services.GetNormalValues(request)) } diff --git a/controllers/misc_controller.go b/controllers/misc_controller.go index b8b3e9b..b93933d 100644 --- a/controllers/misc_controller.go +++ b/controllers/misc_controller.go @@ -1,7 +1,7 @@ package controllers import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "dawn-weather/models" "dawn-weather/persistence" "dawn-weather/persistence/entities" @@ -27,7 +27,7 @@ func GetAnalogYear(c *fiber.Ctx) error { lon, _ := strconv.ParseFloat(c.Query("longitude", "-10000.0"), 64) err := models.ValidateAnalogRequest(lat, lon) if err != nil { - panic(errors.BAD_REQUEST) + panic(DawnErrors.BAD_REQUEST) } location := entities.Location{Type: "Point", Coordinates: []float64{lon, lat}} diff --git a/controllers/nomads_controller.go b/controllers/nomads_controller.go index eb5cec1..3a4b1a1 100644 --- a/controllers/nomads_controller.go +++ b/controllers/nomads_controller.go @@ -21,7 +21,7 @@ import ( // @Param accumulate query boolean true "Accumulate gdd values" // @Router /api/weather/gdd/gefs [get] func GetGefsGDD(c *fiber.Ctx) error { - request := models.GddRequest{}.Build(c) + request := models.BuildGddRequest(c) return c.Status(fiber.StatusOK).JSON(services.GetGefsGddValues(request)) } @@ -39,6 +39,24 @@ func GetGefsGDD(c *fiber.Ctx) error { // @Param accumulate query boolean true "Accumulate gdd values" // @Router /api/weather/gdd/cfs [get] func GetCfsGDD(c *fiber.Ctx) error { - request := models.GddRequest{}.Build(c) + request := models.BuildGddRequest(c) return c.Status(fiber.StatusOK).JSON(services.GetCfsGddValues(request)) } + +// // GetCfsFreezingDate godoc +// // @Summary Get predicted freezing date from CFS +// // @Tags Gdd +// // @Description Get GDD values calculated from CFS +// // @Accept json +// // @Produce json +// // @Success 200 {object} models.CfsGddResponse +// // @Failure 400 {object} errors.StandardError +// // @Param product query string true "Crop to calculate gdd for" Enums(corn, soybean, sunflower, tomato, sugar_beet, peanut, cotton, potato, wheat, pea, oat, spring_wheat, rice, sorghum) +// // @Param latitude query number true "Latitude to search for" +// // @Param longitude query number true "Longitude to search for" +// // @Param accumulate query boolean true "Accumulate gdd values" +// // @Router /api/weather/gdd/cfs [get] +// func GetCfsFreezingDate(c *fiber.Ctx) error { +// request := models.GddRequest{}.Build(c) +// return c.Status(fiber.StatusOK).JSON(services.GetCfsGddValues(request)) +// } diff --git a/controllers/seed_controllers.go b/controllers/seed_controllers.go new file mode 100644 index 0000000..583274e --- /dev/null +++ b/controllers/seed_controllers.go @@ -0,0 +1,47 @@ +package controllers + +import ( + "dawn-weather/models" + "dawn-weather/services" + + "github.com/gofiber/fiber/v2" +) + +// GetSeedList godoc +// @Summary Get list of seeds in database +// @Tags GDD Seed Data +// @Description Get list of seeds in database +// @Accept json +// @Produce json +// @Success 200 {object} models.SeedListResponse +// @Failure 400 {object} errors.StandardError +// @Param product query string true "Crop type to use" Enums(corn, soybean) +// @Router /api/weather/gdd/seeds [get] +func GetSeedList(c *fiber.Ctx) error { + request := models.SeedListRequest{Product: c.Query("product")} + return c.Status(fiber.StatusOK).JSON( + services.GetSeedList(c, request), + ) +} + +// GetCornSeedMaturityDate godoc +// @Summary Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity) +// @Tags GDD Seed Data +// @Description Get estimated maturity date from corn seed +// @Accept json +// @Produce json +// @Success 200 {object} models.CornMaturityResponse +// @Failure 400 {object} errors.StandardError +// @Param latitude query number true "Latitude to search for" +// @Param longitude query number true "Longitude to search for" +// @Param seed query string true "Corn seed to use" +// @Param month query number true "month planted" +// @Param date query number true "date planted" +// @Router /api/weather/gdd/seeds/corn [get] +func GetCornSeedMaturityDate(c *fiber.Ctx) error { + + request := models.BuildCornMaturityRequest(c) + return c.Status(fiber.StatusOK).JSON( + services.GetCornMaturityDate(c, request), + ) +} diff --git a/docs/docs.go b/docs/docs.go index 442c8e0..6adc5d3 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,14 +1,13 @@ -// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// Package docs GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // This file was generated by swaggo/swag - package docs import ( "bytes" "encoding/json" "strings" + "text/template" - "github.com/alecthomas/template" "github.com/swaggo/swag" ) @@ -16,7 +15,7 @@ var doc = `{ "schemes": {{ marshal .Schemes }}, "swagger": "2.0", "info": { - "description": "{{.Description}}", + "description": "{{escape .Description}}", "title": "{{.Title}}", "contact": { "name": "API Support", @@ -656,29 +655,132 @@ var doc = `{ } } } - } - }, - "definitions": { - "errors.ErrorDetails": { - "type": "object", - "properties": { - "correlationId": { - "type": "string" + }, + "/api/weather/gdd/seeds": { + "get": { + "description": "Get list of seeds in database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GDD Seed Data" + ], + "summary": "Get list of seeds in database", + "parameters": [ + { + "enum": [ + "corn", + "soybean" + ], + "type": "string", + "description": "Crop type to use", + "name": "product", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SeedListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } } } }, + "/api/weather/gdd/seeds/corn": { + "get": { + "description": "Get estimated maturity date from corn seed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GDD Seed Data" + ], + "summary": "Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity)", + "parameters": [ + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Corn seed to use", + "name": "seed", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "month planted", + "name": "month", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "date planted", + "name": "date", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CornMaturityResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + } + }, + "definitions": { "errors.StandardError": { "type": "object", "properties": { "description": { "type": "string" }, + "details": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "errorCode": { "type": "string" }, - "errorDetails": { - "$ref": "#/definitions/errors.ErrorDetails" - }, "source": { "type": "string" } @@ -756,6 +858,26 @@ var doc = `{ } } }, + "models.CornMaturityResponse": { + "type": "object", + "properties": { + "closest_gdd": { + "type": "number" + }, + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "date": { + "type": "string" + }, + "gdd": { + "type": "number" + } + } + }, "models.FreezingDateResponse": { "type": "object", "properties": { @@ -839,6 +961,17 @@ var doc = `{ } } } + }, + "models.SeedListResponse": { + "type": "object", + "properties": { + "seeds": { + "type": "array", + "items": { + "type": "string" + } + } + } } } }` @@ -873,6 +1006,13 @@ func (s *s) ReadDoc() string { a, _ := json.Marshal(v) return string(a) }, + "escape": func(v interface{}) string { + // escape tabs + str := strings.Replace(v.(string), "\t", "\\t", -1) + // replace " with \", and if that results in \\", replace that with \\\" + str = strings.Replace(str, "\"", "\\\"", -1) + return strings.Replace(str, "\\\\\"", "\\\\\\\"", -1) + }, }).Parse(doc) if err != nil { return doc diff --git a/docs/swagger.json b/docs/swagger.json index d024431..77fea7a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -641,29 +641,132 @@ } } } - } - }, - "definitions": { - "errors.ErrorDetails": { - "type": "object", - "properties": { - "correlationId": { - "type": "string" + }, + "/api/weather/gdd/seeds": { + "get": { + "description": "Get list of seeds in database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GDD Seed Data" + ], + "summary": "Get list of seeds in database", + "parameters": [ + { + "enum": [ + "corn", + "soybean" + ], + "type": "string", + "description": "Crop type to use", + "name": "product", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SeedListResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } } } }, + "/api/weather/gdd/seeds/corn": { + "get": { + "description": "Get estimated maturity date from corn seed", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "GDD Seed Data" + ], + "summary": "Get estimated maturity date from corn seed (uses CFS data if current GDUs are less than the crop maturity)", + "parameters": [ + { + "type": "number", + "description": "Latitude to search for", + "name": "latitude", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "Longitude to search for", + "name": "longitude", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Corn seed to use", + "name": "seed", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "month planted", + "name": "month", + "in": "query", + "required": true + }, + { + "type": "number", + "description": "date planted", + "name": "date", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CornMaturityResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/errors.StandardError" + } + } + } + } + } + }, + "definitions": { "errors.StandardError": { "type": "object", "properties": { "description": { "type": "string" }, + "details": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, "errorCode": { "type": "string" }, - "errorDetails": { - "$ref": "#/definitions/errors.ErrorDetails" - }, "source": { "type": "string" } @@ -741,6 +844,26 @@ } } }, + "models.CornMaturityResponse": { + "type": "object", + "properties": { + "closest_gdd": { + "type": "number" + }, + "closest_latitude": { + "type": "number" + }, + "closest_longitude": { + "type": "number" + }, + "date": { + "type": "string" + }, + "gdd": { + "type": "number" + } + } + }, "models.FreezingDateResponse": { "type": "object", "properties": { @@ -824,6 +947,17 @@ } } } + }, + "models.SeedListResponse": { + "type": "object", + "properties": { + "seeds": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 6ed4fc3..9ad8465 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,18 +1,15 @@ basePath: / definitions: - errors.ErrorDetails: - properties: - correlationId: - type: string - type: object errors.StandardError: properties: description: type: string + details: + additionalProperties: + type: string + type: object errorCode: type: string - errorDetails: - $ref: '#/definitions/errors.ErrorDetails' source: type: string type: object @@ -63,6 +60,19 @@ definitions: type: number type: array type: object + models.CornMaturityResponse: + properties: + closest_gdd: + type: number + closest_latitude: + type: number + closest_longitude: + type: number + date: + type: string + gdd: + type: number + type: object models.FreezingDateResponse: properties: closest_latitude: @@ -118,6 +128,13 @@ definitions: type: number type: array type: object + models.SeedListResponse: + properties: + seeds: + items: + type: string + type: array + type: object host: localhost:8080 info: contact: @@ -577,4 +594,78 @@ paths: summary: Get gdd normals tags: - Gdd + /api/weather/gdd/seeds: + get: + consumes: + - application/json + description: Get list of seeds in database + parameters: + - description: Crop type to use + enum: + - corn + - soybean + in: query + name: product + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.SeedListResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.StandardError' + summary: Get list of seeds in database + tags: + - GDD Seed Data + /api/weather/gdd/seeds/corn: + get: + consumes: + - application/json + description: Get estimated maturity date from corn seed + parameters: + - description: Latitude to search for + in: query + name: latitude + required: true + type: number + - description: Longitude to search for + in: query + name: longitude + required: true + type: number + - description: Corn seed to use + in: query + name: seed + required: true + type: string + - description: month planted + in: query + name: month + required: true + type: number + - description: date planted + in: query + name: date + required: true + type: number + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.CornMaturityResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/errors.StandardError' + summary: Get estimated maturity date from corn seed (uses CFS data if current + GDUs are less than the crop maturity) + tags: + - GDD Seed Data swagger: "2.0" diff --git a/errors/dawn_errors.go b/errors/dawn_errors.go index adbb0c3..45289c9 100644 --- a/errors/dawn_errors.go +++ b/errors/dawn_errors.go @@ -15,21 +15,18 @@ type BaseError interface { } type DawnError struct { - Name string `json:"name"` - Description string `json:"description"` - LogDetails string `json:"log_details"` - Code int `json:"code"` -} - -type ErrorDetails struct { - RequestId string `json:"correlationId"` + Name string `json:"name"` + Description string `json:"description"` + LogDetails string `json:"log_details"` + Code int `json:"code"` + Details map[string]string `json:"details"` } type StandardError struct { - Source string `json:"source"` - ErrorCode string `json:"errorCode"` - Description string `json:"description"` - Details ErrorDetails `json:"errorDetails"` + Source string `json:"source"` + ErrorCode string `json:"errorCode"` + Description string `json:"description"` + Details map[string]string `json:"details"` } func (err *DawnError) Error() string { @@ -42,7 +39,10 @@ func (err *DawnError) Error() string { func (err *DawnError) BuildStandardError(ctx *fiber.Ctx) StandardError { requestId := ctx.Locals("requestId") - details := ErrorDetails{RequestId: fmt.Sprintf("%s", requestId)} + details := map[string]string{"RequestId": fmt.Sprintf("%s", requestId)} + for k, v := range err.Details { + details[k] = v + } return StandardError{Source: viper.GetString("app.name"), ErrorCode: err.Name, Description: err.Description, Details: details} } @@ -51,6 +51,11 @@ func (err *DawnError) AddLogDetails(logDetails string) *DawnError { return err } +func (err *DawnError) PutDetail(key string, value string) *DawnError { + err.Details[key] = value + return err +} + func Build(err error) *DawnError { return &DawnError{ Name: "INTERNAL_SERVER_ERROR", @@ -66,8 +71,6 @@ func (err *DawnError) LogJson(c *fiber.Ctx) { func (err *DawnError) LogString(c *fiber.Ctx) { requestId := c.Locals("requestId") - // requestBody := c.Request() - // requestBodyStr, _ := json.Marshal(requestBody) output := strconv.Itoa(os.Getpid()) + " " + fmt.Sprintf("%s", requestId) + " " + strconv.Itoa(err.Code) + " - " + c.Method() + " " + c.Route().Path + " - " + err.Error() if err.LogDetails != "" { output += " - " + err.LogDetails @@ -89,6 +92,14 @@ var BAD_REQUEST = &DawnError{ Code: 400, } +func SEED_TYPE_MISMATCH(passedSeed string, expectedSeed string) *DawnError { + return &DawnError{ + Name: "SEED_TYPE_MISMATCH", + Description: "Seed '" + passedSeed + "' was not of expected seed type '" + expectedSeed + "'", + Code: 400, + } +} + var NO_DATA_FOUND = &DawnError{ Name: "NO_DATA_FOUND", Description: "No data found for request", diff --git a/go.mod b/go.mod index 17e4177..1b382c5 100644 --- a/go.mod +++ b/go.mod @@ -25,14 +25,16 @@ require ( github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/procfs v0.7.2 // indirect github.com/spf13/viper v1.8.1 // indirect - github.com/swaggo/swag v1.7.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect + github.com/swaggo/swag v1.7.1 // indirect github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect github.com/valyala/fasthttp v1.28.0 // indirect github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect go.mongodb.org/mongo-driver v1.7.1 // indirect go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect - golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect - golang.org/x/tools v0.1.5 // indirect + golang.org/x/net v0.0.0-20210917221730-978cfadd31cf // indirect + golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.6 // indirect google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index 67138a7..8b10826 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -423,6 +424,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -504,6 +506,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= @@ -511,10 +514,13 @@ github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01S github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= github.com/swaggo/swag v1.7.0 h1:5bCA/MTLQoIqDXXyHfOpMeDvL9j68OY/udlK4pQoo4E= github.com/swaggo/swag v1.7.0/go.mod h1:BdPIL73gvS9NBsdi7M1JOxLvlbfvNRaBP8m6WT6Aajo= +github.com/swaggo/swag v1.7.1 h1:gY9ZakXlNWg/i/v5bQBic7VMZ4teq4m89lpiao74p/s= +github.com/swaggo/swag v1.7.1/go.mod h1:gAiHxNTb9cIpNmA/VEGUP+CyZMCP/EW7mdtc8Bny+p8= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= @@ -544,6 +550,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= @@ -669,6 +676,8 @@ golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf h1:R150MpwJIv1MpS0N/pc+NhTM8ajzvlmxlY5OYsrevXQ= +golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -762,6 +771,9 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678 h1:J27LZFQBFoihqXoegpscI10HpjZ7B5WQLLKL2FZXQKw= +golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -772,6 +784,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -839,6 +853,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4= +golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -981,6 +997,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/logger/logger.go b/logger/logger.go index 4a41cef..cbda320 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -36,6 +36,8 @@ type MessageLog struct { Message string } +var LEVEL_FORMAT_STRING string = "%-5s" + func buildMessageLog(c *fiber.Ctx, message string) MessageLog { const layout = "2006-01-02 03:04:05" requestId := c.Locals("requestId") @@ -83,7 +85,7 @@ func LogRequest(message RequestLog) { tempLogString, _ := json.MarshalIndent(message, "", " ") logString = string(tempLogString) } else { - logString = fmt.Sprintf("[%s] %s %s %s %s - %s %s", message.Level, message.Date, message.PID, message.RequestId, message.StatusCode, message.Method, message.Path) + logString = fmt.Sprintf("[%s] %s %s %s %s - %s %s", fmt.Sprintf(LEVEL_FORMAT_STRING, message.Level), message.Date, message.PID, message.RequestId, message.StatusCode, message.Method, message.Path) if message.Error != nil { logString += " - " + message.Error.Error() } @@ -165,7 +167,7 @@ func _log(c *fiber.Ctx, level, message string) { tempLogString, _ := json.MarshalIndent(lg, "", " ") logString = string(tempLogString) } else { - logString = fmt.Sprintf("[%s] %s %s %s %s", lg.Level, lg.Date, lg.PID, lg.RequestId, lg.Message) + logString = fmt.Sprintf("[%s] %s %s %s %s", fmt.Sprintf(LEVEL_FORMAT_STRING, lg.Level), lg.Date, lg.PID, lg.RequestId, lg.Message) } fmt.Println(logString) } diff --git a/main.go b/main.go index 9da86f9..7903083 100644 --- a/main.go +++ b/main.go @@ -25,7 +25,7 @@ func registerRoutes(app *fiber.App) { } func registerSwagger(app *fiber.App) { - if viper.GetBool("swagger") { + if viper.GetBool("app.swagger") { app.Get(viper.GetString("server.context-path")+"/gdd-swagger/*", swagger.Handler) app.Get(viper.GetString("server.context-path")+"/gdd-swagger/*", swagger.New()) } @@ -33,7 +33,7 @@ func registerSwagger(app *fiber.App) { func registerCors(app *fiber.App) { app.Use(cors.New(cors.Config{ - AllowOrigins: "http://localhost:3000, http://localhost:4200, http://localhost:5000", + AllowOrigins: "http://localhost:3000, http://localhost:4200, http://localhost:5000, http://localhost:12321", AllowHeaders: "Origin, Content-Type, Accept", })) } @@ -63,7 +63,7 @@ func createFiberConfig() fiber.Config { code := fiber.StatusInternalServerError message := errors.StandardError{Source: viper.GetString("app.name"), ErrorCode: "INTERNAL_SERVER", - Description: "Internal Server Error Occurred", Details: errors.ErrorDetails{RequestId: ""}} + Description: "Internal Server Error Occurred", Details: map[string]string{"RequestId": ""}} if e, ok := err.(*errors.DawnError); ok { code = e.Code @@ -89,9 +89,14 @@ func createFiberConfig() fiber.Config { } } -func createFiberApp() *fiber.App { +func CreateFiberApp() *fiber.App { app := fiber.New(createFiberConfig()) app.Use(recover.New()) + registerCors(app) + registerSwagger(app) + registerPrometheus(app) + registerLogging(app) + registerRoutes(app) return app } @@ -105,12 +110,7 @@ func createFiberApp() *fiber.App { func main() { config.GetConfig() persistence.CreateDBSession() - app := createFiberApp() - registerCors(app) - registerSwagger(app) - registerPrometheus(app) - registerLogging(app) - registerRoutes(app) + app := CreateFiberApp() err := app.Listen(":" + viper.GetString("server.port")) diff --git a/models/csv.go b/models/csv.go index 6ab7f10..5885c3e 100644 --- a/models/csv.go +++ b/models/csv.go @@ -1,7 +1,7 @@ package models import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "strconv" "time" @@ -85,8 +85,8 @@ func (r CSVRequest) Build(c *fiber.Ctx) CSVRequest { Longitude: longitude, } - if newRequest.Validate() != nil { - panic(errors.BAD_REQUEST) + if e := newRequest.Validate(); e != nil { + panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error())) } return newRequest diff --git a/models/enums/product_type.go b/models/enums/product_type.go index 9619f11..76bd38e 100644 --- a/models/enums/product_type.go +++ b/models/enums/product_type.go @@ -1,7 +1,7 @@ package enums import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "strings" ) @@ -89,5 +89,5 @@ func GetProductFromString(productString string) Product { case strings.ToLower(ProductType.SORGHUM.Name): return ProductType.SORGHUM } - panic(errors.INVALID_PRODUCT(productString)) + panic(DawnErrors.INVALID_PRODUCT(productString)) } diff --git a/models/freezing_dates.go b/models/freezing_dates.go index 5d67b84..1458d30 100644 --- a/models/freezing_dates.go +++ b/models/freezing_dates.go @@ -1,7 +1,7 @@ package models import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "dawn-weather/persistence/entities" "strconv" "time" @@ -48,7 +48,7 @@ func (r FreezingDateRequest) Build(c *fiber.Ctx) FreezingDateRequest { } if rNew.Validate() != nil { - panic(errors.BAD_REQUEST) + panic(DawnErrors.BAD_REQUEST) } return rNew diff --git a/models/gdd.go b/models/gdd.go index 5ff846f..8c818db 100644 --- a/models/gdd.go +++ b/models/gdd.go @@ -1,7 +1,7 @@ package models import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "dawn-weather/persistence/entities" "strconv" "time" @@ -43,7 +43,7 @@ func (r GddRequest) ValidateNoYear() error { ) } -func (r GddRequest) Build(c *fiber.Ctx) GddRequest { +var BuildGddRequest = func(c *fiber.Ctx) GddRequest { year, errYear := strconv.Atoi(c.Query("year", "0")) product := c.Query("product") latitude, errLat := strconv.ParseFloat(c.Query("latitude"), 64) @@ -59,16 +59,16 @@ func (r GddRequest) Build(c *fiber.Ctx) GddRequest { } if errYear != nil || errLat != nil || errLon != nil || errBool != nil { - panic(errors.BAD_REQUEST) + panic(DawnErrors.BAD_REQUEST) } if rNew.Year != 0 { - if rNew.Validate() != nil { - panic(errors.BAD_REQUEST) + if e := rNew.Validate(); e != nil { + panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error())) } } else { - if rNew.ValidateNoYear() != nil { - panic(errors.BAD_REQUEST) + if e := rNew.ValidateNoYear(); e != nil { + panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error())) } } return rNew diff --git a/models/seeds.go b/models/seeds.go new file mode 100644 index 0000000..f370bf0 --- /dev/null +++ b/models/seeds.go @@ -0,0 +1,66 @@ +package models + +import ( + DawnErrors "dawn-weather/errors" + "strconv" + "time" + + validation "github.com/go-ozzo/ozzo-validation" + "github.com/gofiber/fiber/v2" +) + +type SeedListRequest struct { + Product string `json:"product"` +} + +type SeedListResponse struct { + Seeds []string `json:"seeds"` +} + +type CornMaturityRequest struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Seed string `json:"seed"` + Month int `json:"month"` + Date int `json:"date"` +} + +type CornMaturityResponse struct { + Date time.Time `json:"date"` + GDD float64 `json:"gdd"` + ClosestGDD float64 `json:"closest_gdd"` + ClosestLatitude float64 `json:"closest_latitude"` + ClosestLongitude float64 `json:"closest_longitude"` +} + +func (r CornMaturityRequest) Validate() error { + return validation.ValidateStruct(&r, + validation.Field(&r.Latitude, validation.Required, validation.Min(-90.0), validation.Max(90.0)), + validation.Field(&r.Longitude, validation.Required, validation.Min(-180.0), validation.Max(180.0)), + validation.Field(&r.Month, validation.Required, validation.Min(1), validation.Max(12)), + validation.Field(&r.Date, validation.Required, validation.Min(1), validation.Max(31)), + ) +} + +func BuildCornMaturityRequest(c *fiber.Ctx) CornMaturityRequest { + + lat, _ := strconv.ParseFloat(c.Query("latitude", "-10000.0"), 64) + lon, _ := strconv.ParseFloat(c.Query("longitude", "-10000.0"), 64) + month, _ := strconv.Atoi(c.Query("month", "0")) + date, _ := strconv.Atoi(c.Query("date", "0")) + seed := c.Query("seed", "-10000.0") + + newRequest := CornMaturityRequest{ + Latitude: lat, + Longitude: lon, + Seed: seed, + Month: month, + Date: date, + } + + if e := newRequest.Validate(); e != nil { + panic(DawnErrors.BAD_REQUEST.AddLogDetails(e.Error())) + } + + return newRequest +} diff --git a/persistence/entities/seed.go b/persistence/entities/seed.go new file mode 100644 index 0000000..4b6e438 --- /dev/null +++ b/persistence/entities/seed.go @@ -0,0 +1,10 @@ +package entities + +type Seed struct { + Seed string `bson:"seed"` + Company string `bson:"company"` + SilkGdus float64 `bson:"silk_gdus"` + BlackLayerGdus float64 `bson:"black_layer_gdus"` + RelativeMaturity float64 `bson:"relative_maturity"` + Type string `bson:"type"` +} diff --git a/persistence/mongodb.go b/persistence/mongodb.go index 45c5336..e91aad4 100644 --- a/persistence/mongodb.go +++ b/persistence/mongodb.go @@ -2,9 +2,10 @@ package persistence import ( "context" - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "dawn-weather/models" "dawn-weather/persistence/entities" + "strings" "github.com/bradfitz/slice" "github.com/spf13/viper" @@ -67,7 +68,7 @@ func CurrentGddFindFirstByYearAndLocation(location entities.Location) entities.G var g entities.Gdd err := coll.FindOne(Ctx, filter).Decode(&g) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } return g @@ -81,7 +82,7 @@ func GddFindFirstByYearAndLocation(year int, location entities.Location) entitie var g entities.Gdd err := coll.FindOne(Ctx, filter).Decode(&g) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } return g @@ -95,7 +96,7 @@ func NormalsFindFirstByYearAndLocation(location entities.Location) entities.Norm var n entities.Normal err := coll.FindOne(Ctx, filter).Decode(&n) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } return n @@ -115,7 +116,7 @@ func GefsFindAllByLocation(location entities.Location) []entities.GefsGdd { cursor, err := coll.Find(Ctx, filter, options) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } for cursor.Next(context.TODO()) { @@ -123,7 +124,7 @@ func GefsFindAllByLocation(location entities.Location) []entities.GefsGdd { var elem entities.GefsGdd err := cursor.Decode(&elem) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } results = append(results, elem) @@ -144,7 +145,7 @@ func CfsFindAllByLocation(location entities.Location) entities.CfsGdd { var g entities.CfsGdd err := coll.FindOne(Ctx, filter).Decode(&g) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } return g @@ -158,7 +159,7 @@ func FindAnalogYear(location entities.Location) models.AnalogResponse { var g entities.Gdd err := coll.FindOne(Ctx, filter).Decode(&g) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } results := models.AnalogResponse{ @@ -178,8 +179,55 @@ func FindFreezingDates(location entities.Location) entities.FreezingDates { var g entities.FreezingDates err := coll.FindOne(Ctx, filter).Decode(&g) if err != nil { - panic(errors.NO_DATA_FOUND) + panic(DawnErrors.NO_DATA_FOUND) } return g } + +func FindSeed(seedName string) entities.Seed { + coll := Conn.Database(viper.GetString("db.database")).Collection("seeds") + + filter := bson.D{ + {Key: "seed", Value: seedName}, + } + + var g entities.Seed + err := coll.FindOne(Ctx, filter).Decode(&g) + if err != nil { + panic(DawnErrors.NO_DATA_FOUND) + } + + return g +} + +func FindSeeds(product string) []entities.Seed { + + coll := Conn.Database(viper.GetString("db.database")).Collection("seeds") + + filter := bson.D{ + {Key: "type", Value: strings.ToLower(product)}, + } + + var results []entities.Seed + + options := options.Find() + + cursor, err := coll.Find(Ctx, filter, options) + if err != nil { + panic(DawnErrors.NO_DATA_FOUND) + } + + for cursor.Next(context.TODO()) { + + var elem entities.Seed + err := cursor.Decode(&elem) + if err != nil { + panic(DawnErrors.NO_DATA_FOUND) + } + + results = append(results, elem) + } + + return results +} diff --git a/services/data_download_service.go b/services/data_download_service.go index 0bc4f0a..82b0de1 100644 --- a/services/data_download_service.go +++ b/services/data_download_service.go @@ -1,7 +1,8 @@ package services import ( - "dawn-weather/errors" + "dawn-weather/context" + DawnErrors "dawn-weather/errors" "dawn-weather/models" "dawn-weather/models/enums" "dawn-weather/persistence" @@ -92,7 +93,7 @@ func fillDates() []time.Time { return dates } -func getPrimary(c *fiber.Ctx, request models.CSVRequest, dates []time.Time) []float64 { +func getPrimary(c context.DawnCtx, request models.CSVRequest, dates []time.Time) []float64 { gddRequest := models.GddRequest{ Year: request.Year, Product: request.Product, @@ -152,7 +153,7 @@ func _parseDate(date string) time.Time { const layout = "2006-Jan-02" newDate, e := time.Parse(layout, date) if e != nil { - panic(errors.DATE_PARSE_FAILURE.AddLogDetails(e.Error())) + panic(DawnErrors.DATE_PARSE_FAILURE.AddLogDetails(e.Error())) } return newDate } @@ -187,7 +188,7 @@ func parseDate(date string) time.Time { case "12": return _parseDate("1981-Dev-" + day) } - panic(errors.DATE_PARSE_FAILURE.AddLogDetails("Failed converting " + date + " to proper format.")) + panic(DawnErrors.DATE_PARSE_FAILURE.AddLogDetails("Failed converting " + date + " to proper format.")) } func getFreezingDates(request models.CSVRequest, dates []time.Time) [][]int { @@ -230,7 +231,7 @@ func getFreezingDates(request models.CSVRequest, dates []time.Time) [][]int { return [][]int{firstFreezingValues, lastFreezingValues} } -func getComparisonYear(c *fiber.Ctx, request models.CSVRequest, dates []time.Time) []float64 { +func getComparisonYear(c context.DawnCtx, request models.CSVRequest, dates []time.Time) []float64 { gddRequest := models.GddRequest{ Year: request.ComparisonYear, Product: request.Product, @@ -346,13 +347,13 @@ func getGefsData(request models.CSVRequest, dates []time.Time) []float64 { return fullGddValues } -func pullData(c *fiber.Ctx, request models.CSVRequest) CSVData { +func pullData(c context.DawnCtx, request models.CSVRequest) CSVData { returnData := CSVData{} dates := fillDates() returnData.Date = dates if request.Analog { - returnData.Analog = getAnalogYear(c, request, dates) + returnData.Analog = getAnalogYear(c.FiberCtx, request, dates) } if request.Cfs || request.CfsLower || request.CfsUpper { t := getCfsData(request, dates) @@ -424,14 +425,14 @@ func createRecords(keys []string, data CSVData) [][]string { return records } -func GetDataDownload(c *fiber.Ctx, request models.CSVRequest) string { +func GetDataDownload(c context.DawnCtx, request models.CSVRequest) string { fileId := uuid.New() f, err := os.Create(fileId.String() + ".csv") if err != nil { - panic(errors.FILE_CREATION_ERROR.AddLogDetails("Could not create file")) + panic(DawnErrors.FILE_CREATION_ERROR.AddLogDetails("Could not create file")) } w := csv.NewWriter(f) diff --git a/services/freezing_date_service.go b/services/freezing_date_service.go index 5bb6c54..14990b3 100644 --- a/services/freezing_date_service.go +++ b/services/freezing_date_service.go @@ -1,7 +1,7 @@ package services import ( - "dawn-weather/errors" + DawnErrors "dawn-weather/errors" "dawn-weather/models" "dawn-weather/persistence" "dawn-weather/persistence/entities" @@ -45,7 +45,7 @@ func GetFreezingDate(request models.FreezingDateRequest) models.FreezingDateResp } if tempIdx == -1 { - panic(errors.BAD_REQUEST) + panic(DawnErrors.BAD_REQUEST) } firstDates := freezingDates.FirstDates[tempIdx] diff --git a/services/gdd_service.go b/services/gdd_service.go index 4f70be9..670e171 100644 --- a/services/gdd_service.go +++ b/services/gdd_service.go @@ -1,6 +1,7 @@ package services import ( + "dawn-weather/context" "dawn-weather/models" "dawn-weather/models/enums" "dawn-weather/persistence" @@ -11,7 +12,28 @@ import ( "github.com/gofiber/fiber/v2" ) -func GetGddValues(c *fiber.Ctx, request models.GddRequest) models.GddResponse { +func GetFullYearGddValues(c *fiber.Ctx, request models.GddRequest) models.GddResponse { + product := enums.GetProductFromString(request.Product) + var gdds entities.Gdd + if request.Year == time.Now().Year() { + gdds = persistence.CurrentGddFindFirstByYearAndLocation(request.BuildLocation()) + gdds2 := persistence.CfsFindAllByLocation(request.BuildLocation()) + gdds.MaxTemps = append(gdds.MaxTemps, gdds2.MaxTemps...) + gdds.MinTemps = append(gdds.MinTemps, gdds2.MinTemps...) + } else { + gdds = persistence.GddFindFirstByYearAndLocation(request.Year, request.BuildLocation()) + } + returnGdds := models.GddResponse{ + Product: product.Name, + ClosestLatitude: gdds.Location.Coordinates[1], + ClosestLongitude: gdds.Location.Coordinates[0], + GddValues: utils.CalculateGddValues(gdds.MinTemps, gdds.MaxTemps, product, request.Accumulate), + LastDate: gdds.LastDate.Time(), + } + return returnGdds +} + +func GetGddValues(ctx context.DawnCtx, request models.GddRequest) models.GddResponse { product := enums.GetProductFromString(request.Product) var gdds entities.Gdd if request.Year == time.Now().Year() { diff --git a/services/seed_service.go b/services/seed_service.go new file mode 100644 index 0000000..c64717e --- /dev/null +++ b/services/seed_service.go @@ -0,0 +1,72 @@ +package services + +import ( + DawnErrors "dawn-weather/errors" + "dawn-weather/models" + "dawn-weather/models/enums" + "dawn-weather/persistence" + "dawn-weather/utils" + "math" + "time" + + "github.com/gofiber/fiber/v2" +) + +func GetSeedList(c *fiber.Ctx, request models.SeedListRequest) models.SeedListResponse { + product := enums.GetProductFromString(request.Product) + if product.Name != enums.ProductType.CORN.Name && product.Name != enums.ProductType.SOYBEAN.Name { + panic(DawnErrors.INVALID_PRODUCT(request.Product)) + } + seeds := persistence.FindSeeds(product.Name) + var results []string + for i := 0; i < len(seeds); i++ { + results = append(results, seeds[i].Seed) + } + return models.SeedListResponse{Seeds: results} +} + +func GetCornMaturityDate(c *fiber.Ctx, request models.CornMaturityRequest) models.CornMaturityResponse { + + seed := persistence.FindSeed(request.Seed) + if seed.Type != "corn" { + panic(DawnErrors.SEED_TYPE_MISMATCH(request.Seed, "corn")) + } + + gddRequest := models.GddRequest{ + Year: time.Now().Year(), + Product: "corn", + Latitude: request.Latitude, + Longitude: request.Longitude, + Accumulate: false, + } + + gdds := GetFullYearGddValues(c, gddRequest) + closestValue := 0.0 + closestIdx := 0 + gdus := 0.0 + + startingDate := time.Date(time.Now().Year(), time.Month(request.Month), request.Date, 0, 0, 0, 0, time.UTC) + startingIdx := startingDate.Sub(time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.UTC)) + startingIdxVal := int(startingIdx.Hours() / 24) + if isLeapYear(time.Now().Year()) { + startingIdxVal -= 1 + } + + for i := startingIdxVal; i < len(gdds.GddValues); i++ { + value := gdds.GddValues[i] + gdus += value + if math.Abs(gdus-seed.BlackLayerGdus) < math.Abs(closestValue-seed.BlackLayerGdus) { + closestValue = gdus + closestIdx = i + } + } + response := models.CornMaturityResponse{ + Date: utils.ConvertDateIdxToDate(closestIdx), + GDD: seed.BlackLayerGdus, + ClosestGDD: closestValue, + ClosestLatitude: gdds.ClosestLatitude, + ClosestLongitude: gdds.ClosestLongitude, + } + return response + +} diff --git a/utils/clip_test.go b/utils/clip_test.go new file mode 100644 index 0000000..e6e6db4 --- /dev/null +++ b/utils/clip_test.go @@ -0,0 +1,37 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestClipInt(t *testing.T) { + value := 10 + assert.Equal(t, value, ClipInt(value, 0, 11)) +} + +func TestClipIntLow(t *testing.T) { + value := 4 + assert.Equal(t, 10, ClipInt(value, 10, 11)) +} + +func TestClipIntAbove(t *testing.T) { + value := 100 + assert.Equal(t, 11, ClipInt(value, 10, 11)) +} + +func TestClipFloat(t *testing.T) { + value := 10.0 + assert.Equal(t, value, ClipFloat(value, 0.0, 11.0)) +} + +func TestClipFloatLow(t *testing.T) { + value := 4.0 + assert.Equal(t, 10.0, ClipFloat(value, 10.0, 11.0)) +} + +func TestClipFloatAbove(t *testing.T) { + value := 100.0 + assert.Equal(t, 11.0, ClipFloat(value, 10.0, 11.0)) +} diff --git a/utils/date_converter.go b/utils/date_converter.go new file mode 100644 index 0000000..7e614cb --- /dev/null +++ b/utils/date_converter.go @@ -0,0 +1,38 @@ +package utils + +import "time" + +func isLeapYear(year int) bool { + if (year % 4) == 0 { + if (year % 100) == 0 { + if (year % 400) == 0 { + return true + } else { + return false + } + } else { + return true + } + } else { + return false + } +} + +func ConvertDateIdxToDate(idx int) time.Time { + idx -= 1 + date := time.Date(time.Now().Year(), time.January, 1, 0, 0, 0, 0, time.UTC) + date = date.Add(time.Duration(24*idx) * time.Hour) + if isLeapYear(time.Now().Year()) { + date = date.Add(time.Duration(-24) * time.Hour) + } + return date +} + +func ConvertDateIdxToDateWithPlantingDate(plantingDate time.Time, idx int) time.Time { + date := plantingDate + date = date.Add(time.Duration(24*idx) * time.Hour) + if isLeapYear(time.Now().Year()) && plantingDate.Before(time.Date(time.Now().Year(), time.March, 1, 0, 0, 0, 0, time.UTC)) { + date = date.Add(time.Duration(-24) * time.Hour) + } + return date +} diff --git a/utils/gdd_calculations.go b/utils/gdd_calculations.go index c3de23f..3d068a9 100644 --- a/utils/gdd_calculations.go +++ b/utils/gdd_calculations.go @@ -1,31 +1,20 @@ package utils -import "dawn-weather/models/enums" +import ( + "dawn-weather/models/enums" +) func CalculateSingleGdd(minTemp float64, maxTemp float64, product enums.Product) float64 { if product.Name == "CORN" { - if minTemp < 50 { - minTemp = 50 - } - if minTemp > 86 { - minTemp = 86 - } - - if maxTemp < 50 { - maxTemp = 50 - } - if maxTemp > 86 { - maxTemp = 86 - } + minTemp = ClipFloat(minTemp, 50, 86) + maxTemp = ClipFloat(maxTemp, 50, 86) } mean := (maxTemp + minTemp) / 2.0 value := mean - product.BaseTemp if product.Name == "CORN" { return value } - if value < 0 { - value = 0 - } + value = ClipMinFloat(value, 0) return value } @@ -33,9 +22,7 @@ func CalculateNormalGddValues(base []float64, product enums.Product, accumulate var returnList []float64 for i := 0; i < len(base); i++ { value := base[i] - product.BaseTemp - if value < 0 { - value = 0 - } + value = ClipMinFloat(value, 0) if accumulate && i > 0 { value += returnList[len(returnList)-1] } diff --git a/utils/gdd_calculations_test.go b/utils/gdd_calculations_test.go new file mode 100644 index 0000000..a3f5727 --- /dev/null +++ b/utils/gdd_calculations_test.go @@ -0,0 +1,97 @@ +package utils + +import ( + "dawn-weather/models/enums" + "testing" + + "github.com/stretchr/testify/assert" +) + +var minTempsArr = [...]float64{55.5, 62.4, 22.5, 96.5} +var maxTempsArr = [...]float64{78.4, 92.5, 55.8, 35.2} + +var normalBaseArr = [...]float64{50, 60, 70, 80} + +func Test_CalculateSingleGdd(t *testing.T) { + minTemp := minTempsArr[0] + maxTemp := maxTempsArr[0] + + expectedGdd := (minTemp + maxTemp) / 2 + expectedGdd -= enums.ProductType.SOYBEAN.BaseTemp + + assert.Equal(t, expectedGdd, CalculateSingleGdd(minTemp, maxTemp, enums.ProductType.SOYBEAN)) +} + +func Test_CalculateSingleGdd_Corn(t *testing.T) { + minTemp := minTempsArr[0] + maxTemp := maxTempsArr[0] + + expectedGdd := (minTemp + maxTemp) / 2 + expectedGdd -= enums.ProductType.CORN.BaseTemp + + assert.Equal(t, expectedGdd, CalculateSingleGdd(minTemp, maxTemp, enums.ProductType.CORN)) +} + +func Test_CalculateGddValues(t *testing.T) { + var expectedGdd []float64 + + for i := 0; i < 4; i++ { + val := (minTempsArr[i] + maxTempsArr[i]) / 2 + val -= enums.ProductType.OAT.BaseTemp + expectedGdd = append(expectedGdd, ClipMinFloat(val, 0)) + } + + assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.OAT, false)) +} + +func Test_CalculateGddValues_Corn(t *testing.T) { + var expectedGdd []float64 + + for i := 0; i < 4; i++ { + val := (ClipFloat(minTempsArr[i], 50, 86) + ClipFloat(maxTempsArr[i], 50, 86)) / 2 + val -= enums.ProductType.CORN.BaseTemp + expectedGdd = append(expectedGdd, val) + } + + assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.CORN, false)) +} + +func Test_CalculateGddValues_Accumulated(t *testing.T) { + var expectedGdd []float64 + for i := 0; i < 4; i++ { + val := (minTempsArr[i] + maxTempsArr[i]) / 2 + val -= enums.ProductType.WHEAT.BaseTemp + val = ClipMinFloat(val, 0) + if i > 0 { + val += expectedGdd[len(expectedGdd)-1] + } + expectedGdd = append(expectedGdd, val) + } + + assert.Equal(t, expectedGdd, CalculateGddValues(minTempsArr[:], maxTempsArr[:], enums.ProductType.WHEAT, true)) +} + +func Test_CalculateNormalGddValues(t *testing.T) { + var expectedGdd []float64 + for i := 0; i < 4; i++ { + val := normalBaseArr[i] - enums.ProductType.PEANUT.BaseTemp + val = ClipMinFloat(val, 0) + expectedGdd = append(expectedGdd, val) + } + + assert.Equal(t, expectedGdd, CalculateNormalGddValues(normalBaseArr[:], enums.ProductType.PEANUT, false)) +} + +func Test_CalculateNormalGddValues_Accumulated(t *testing.T) { + var expectedGdd []float64 + for i := 0; i < 4; i++ { + val := normalBaseArr[i] - enums.ProductType.PEANUT.BaseTemp + val = ClipMinFloat(val, 0) + if i > 0 { + val += expectedGdd[len(expectedGdd)-1] + } + expectedGdd = append(expectedGdd, val) + } + + assert.Equal(t, expectedGdd, CalculateNormalGddValues(normalBaseArr[:], enums.ProductType.PEANUT, true)) +} -- GitLab