Skip to content

Sample application demonstrating the core behaviors and features #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/smarty/shuttle

go 1.20
go 1.22
42 changes: 42 additions & 0 deletions sample/app/processor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package app

import (
"context"
"net/http"

"github.com/smarty/shuttle"
"github.com/smarty/shuttle/sample/inputs"
"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 {
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),
}
}
}
79 changes: 79 additions & 0 deletions sample/inputs/addition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package inputs

import (
"net/http"
"strconv"

"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,
B: -1,
}
}

// 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)
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
}

// 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{
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
}
49 changes: 49 additions & 0 deletions sample/inputs/subtraction.go
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 36 additions & 0 deletions sample/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package main

import (
"log"
"net/http"

"github.com/smarty/shuttle"
"github.com/smarty/shuttle/sample/app"
"github.com/smarty/shuttle/sample/inputs"
)

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)
if err != nil {
log.Fatal(err)
}
}
8 changes: 8 additions & 0 deletions sample/outputs/addition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
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"`
C int `json:"c"`
}
7 changes: 7 additions & 0 deletions sample/outputs/subtraction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package outputs

type Subtraction struct {
A int `json:"a"`
B int `json:"b"`
C int `json:"c"`
}
Loading