From bd40b6d2f93d1b7fd99bdccd19dd0d6921b05f45 Mon Sep 17 00:00:00 2001 From: Michael Whatcott Date: Thu, 16 May 2024 16:32:52 -0600 Subject: [PATCH 1/3] Sample application demonstrating the core behaviors and features. --- README.md | 13 +++---- sample/app/processor.go | 39 ++++++++++++++++++++ sample/inputs/addition.go | 68 +++++++++++++++++++++++++++++++++++ sample/inputs/subtraction.go | 49 +++++++++++++++++++++++++ sample/main.go | 29 +++++++++++++++ sample/outputs/addition.go | 7 ++++ sample/outputs/subtraction.go | 7 ++++ 7 files changed, 206 insertions(+), 6 deletions(-) create mode 100644 sample/app/processor.go create mode 100644 sample/inputs/addition.go create mode 100644 sample/inputs/subtraction.go create mode 100644 sample/main.go create mode 100644 sample/outputs/addition.go create mode 100644 sample/outputs/subtraction.go diff --git a/README.md b/README.md index e1ed6a5..b9dd04d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ #### SMARTY DISCLAIMER: Subject to the terms of the associated license agreement, this software is freely available for your use. This software is FREE, AS IN PUPPIES, and is a gift. Enjoy your new responsibility. This means that while we may consider enhancement requests, we may or may not choose to entertain requests at our sole and absolute discretion. -Purpose ------------------------ +# Shuttle -How to Use: ------------------------ +## Purpose -Naming `shuttle` ------------------------ +Shuttle transforms HTTP requests into intention-revealing, user instructions to be processed by the application. After processing the given operation, shuttle then renders the results of that operation back to the underlying HTTP response. + +## How to Use: + +See the code in the `/sample` folder. \ No newline at end of file diff --git a/sample/app/processor.go b/sample/app/processor.go new file mode 100644 index 0000000..3424736 --- /dev/null +++ b/sample/app/processor.go @@ -0,0 +1,39 @@ +package app + +import ( + "context" + "net/http" + + "github.com/smarty/shuttle" + "github.com/smarty/shuttle/sample/inputs" + "github.com/smarty/shuttle/sample/outputs" +) + +type Processor struct{} + +func NewProcessor() *Processor { + return &Processor{} +} + +func (this *Processor) Process(_ context.Context, v any) any { + switch input := v.(type) { + case *inputs.Addition: + return outputs.Addition{ + A: input.A, + B: input.B, + C: input.A + input.B, + } + case *inputs.Subtraction: + return outputs.Subtraction{ + A: input.A, + B: input.B, + C: input.A - input.B, + } + default: + return shuttle.SerializeResult{ + StatusCode: http.StatusInternalServerError, + ContentType: "text/plain; charset=utf-8", + Content: http.StatusText(http.StatusInternalServerError), + } + } +} diff --git a/sample/inputs/addition.go b/sample/inputs/addition.go new file mode 100644 index 0000000..e86c4f1 --- /dev/null +++ b/sample/inputs/addition.go @@ -0,0 +1,68 @@ +package inputs + +import ( + "net/http" + "strconv" + + "github.com/smarty/shuttle" +) + +type Addition struct { + A int + B int +} + +func NewAddition() *Addition { + return &Addition{ + A: -1, + B: -1, + } +} + +func (this *Addition) Reset() { + this.A = 0 + this.B = 0 +} + +func (this *Addition) Bind(request *http.Request) error { + rawA := request.URL.Query().Get("a") + a, err := strconv.Atoi(rawA) + if err != nil { + return shuttle.InputError{ + Fields: []string{"query:a"}, + Name: "bind:integer", + Message: "failed to convert parameter to integer", + } + } + this.A = a + rawB := request.URL.Query().Get("b") + b, err := strconv.Atoi(rawB) + if err != nil { + return shuttle.InputError{ + Fields: []string{"query:b"}, + Name: "bind:integer", + Message: "failed to convert parameter to integer", + } + } + this.B = b + return nil +} +func (this *Addition) Validate(errors []error) (count int) { + if this.A <= 0 { + errors[count] = shuttle.InputError{ + Fields: []string{"query:a"}, + Name: "validate:positive", + Message: "parameter must be a positive integer", + } + count++ + } + if this.B <= 0 { + errors[count] = shuttle.InputError{ + Fields: []string{"query:b"}, + Name: "validate:positive", + Message: "parameter must be a positive integer", + } + count++ + } + return count +} diff --git a/sample/inputs/subtraction.go b/sample/inputs/subtraction.go new file mode 100644 index 0000000..8787e9a --- /dev/null +++ b/sample/inputs/subtraction.go @@ -0,0 +1,49 @@ +package inputs + +import "github.com/smarty/shuttle" + +type Subtraction struct { + shuttle.BaseInputModel + A int `json:"a"` + B int `json:"b"` +} + +func NewSubtraction() *Subtraction { + return &Subtraction{ + A: -1, + B: -1, + } +} + +func (this *Subtraction) Reset() { + this.A = 0 + this.B = 0 +} + +func (this *Subtraction) Validate(errors []error) (count int) { + if this.A <= 0 { + errors[count] = shuttle.InputError{ + Fields: []string{"query:a"}, + Name: "validate:positive", + Message: "parameter must be a positive integer", + } + count++ + } + if this.B <= 0 { + errors[count] = shuttle.InputError{ + Fields: []string{"query:b"}, + Name: "validate:positive", + Message: "parameter must be a positive integer", + } + count++ + } + if this.B > this.A { + errors[count] = shuttle.InputError{ + Fields: []string{"query:a", "query:b"}, + Name: "validate:a>b", + Message: "a must be greater than or equal to b", + } + count++ + } + return count +} diff --git a/sample/main.go b/sample/main.go new file mode 100644 index 0000000..cfefe44 --- /dev/null +++ b/sample/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + "net/http" + + "github.com/smarty/shuttle" + "github.com/smarty/shuttle/sample/app" + "github.com/smarty/shuttle/sample/inputs" +) + +func main() { + router := http.NewServeMux() + router.Handle("/add", shuttle.NewHandler( + shuttle.Options.InputModel(func() shuttle.InputModel { return inputs.NewAddition() }), + shuttle.Options.Processor(func() shuttle.Processor { return app.NewProcessor() }), + )) + router.Handle("/sub", shuttle.NewHandler( + shuttle.Options.InputModel(func() shuttle.InputModel { return inputs.NewSubtraction() }), + shuttle.Options.Processor(func() shuttle.Processor { return app.NewProcessor() }), + shuttle.Options.DeserializeJSON(true), + )) + address := "localhost:8080" + log.Printf("Listening on %s", address) + err := http.ListenAndServe(address, router) + if err != nil { + log.Fatal(err) + } +} diff --git a/sample/outputs/addition.go b/sample/outputs/addition.go new file mode 100644 index 0000000..f177f3d --- /dev/null +++ b/sample/outputs/addition.go @@ -0,0 +1,7 @@ +package outputs + +type Addition struct { + A int `json:"a"` + B int `json:"b"` + C int `json:"c"` +} diff --git a/sample/outputs/subtraction.go b/sample/outputs/subtraction.go new file mode 100644 index 0000000..2078615 --- /dev/null +++ b/sample/outputs/subtraction.go @@ -0,0 +1,7 @@ +package outputs + +type Subtraction struct { + A int `json:"a"` + B int `json:"b"` + C int `json:"c"` +} From 2e0c5e1c6222bff170d55f38a7d7108f87f73a19 Mon Sep 17 00:00:00 2001 From: Michael Whatcott Date: Thu, 16 May 2024 16:47:29 -0600 Subject: [PATCH 2/3] Provide commentary for sample. --- sample/app/processor.go | 3 +++ sample/inputs/addition.go | 11 +++++++++++ sample/main.go | 7 +++++++ sample/outputs/addition.go | 1 + 4 files changed, 22 insertions(+) diff --git a/sample/app/processor.go b/sample/app/processor.go index 3424736..78944b4 100644 --- a/sample/app/processor.go +++ b/sample/app/processor.go @@ -9,6 +9,9 @@ import ( "github.com/smarty/shuttle/sample/outputs" ) +// Processor receives the InputModel, invokes application behavior, and returns the results to be rendered. +// Generally, the processor will receive some sort of application component which handles the real work +// of the application, but for this simple example, the domain work happens right here. type Processor struct{} func NewProcessor() *Processor { diff --git a/sample/inputs/addition.go b/sample/inputs/addition.go index e86c4f1..ea7c85e 100644 --- a/sample/inputs/addition.go +++ b/sample/inputs/addition.go @@ -7,11 +7,13 @@ import ( "github.com/smarty/shuttle" ) +// Addition represents the data from the client's request. type Addition struct { A int B int } +// NewAddition is the constructor and, by convention, sets the data to garbage values. func NewAddition() *Addition { return &Addition{ A: -1, @@ -19,11 +21,16 @@ func NewAddition() *Addition { } } +// Reset is called by shuttle to prepare the instance for use with the current request. +// This instance will be re-used over the lifetime of the application. func (this *Addition) Reset() { this.A = 0 this.B = 0 } +// Bind is your only opportunity to get data from the request. +// Returning an error indicates that the request data is completely inscrutable. +// In such a case, processing will be short-circuited resulting in HTTP 400 Bad Request. func (this *Addition) Bind(request *http.Request) error { rawA := request.URL.Query().Get("a") a, err := strconv.Atoi(rawA) @@ -47,6 +54,10 @@ func (this *Addition) Bind(request *http.Request) error { this.B = b return nil } + +// Validate is an opportunity to ensure that the values gathered during Bind are usable. +// The errors slice provided is pre-initialized and can be directly assigned to, beginning at index 0. +// The presence of any errors short-circuits processing and results in HTTP 422 Unprocessable Entity. func (this *Addition) Validate(errors []error) (count int) { if this.A <= 0 { errors[count] = shuttle.InputError{ diff --git a/sample/main.go b/sample/main.go index cfefe44..f6e2cd9 100644 --- a/sample/main.go +++ b/sample/main.go @@ -10,16 +10,23 @@ import ( ) func main() { + // You can use any routing mechanism you'd like. For this sample, we'll be using net/http.ServeMux. router := http.NewServeMux() + + // About as simple as a route definition gets: router.Handle("/add", shuttle.NewHandler( shuttle.Options.InputModel(func() shuttle.InputModel { return inputs.NewAddition() }), shuttle.Options.Processor(func() shuttle.Processor { return app.NewProcessor() }), )) + + // This route expects JSON in the request body: router.Handle("/sub", shuttle.NewHandler( shuttle.Options.InputModel(func() shuttle.InputModel { return inputs.NewSubtraction() }), shuttle.Options.Processor(func() shuttle.Processor { return app.NewProcessor() }), shuttle.Options.DeserializeJSON(true), )) + + // Nothing interesting to see here... address := "localhost:8080" log.Printf("Listening on %s", address) err := http.ListenAndServe(address, router) diff --git a/sample/outputs/addition.go b/sample/outputs/addition.go index f177f3d..ad2aac0 100644 --- a/sample/outputs/addition.go +++ b/sample/outputs/addition.go @@ -1,5 +1,6 @@ package outputs +// Addition represents the http response, which by default will be serialized to JSON. type Addition struct { A int `json:"a"` B int `json:"b"` From 02a4c5e106e27d4bcdc37bf2307d9e36d12f25bf Mon Sep 17 00:00:00 2001 From: Michael Whatcott Date: Wed, 22 May 2024 13:27:11 -0600 Subject: [PATCH 3/3] Go 1.22 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 9a9c48a..63c2311 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/smarty/shuttle -go 1.20 +go 1.22