Skip to content

Commit

Permalink
chore(allsrv): add v2 Server create API
Browse files Browse the repository at this point in the history
Here we're leaning into a more structured API. This utilizes versioned URLs
so that our endpoints can evolve in a meaningful way. We also make use of
the [JSON-API spec](https://jsonapi.org/). This provides a common structure
for our consumers. JSON-API is more opinionated than other API specs, but
it has client libs that are widely available across most languages. We've
chosen not to implement the entire spec, but enough to show off the
core benefits of using a spec (not limited to JSON-API spec). The JSON-API
spec is VERY structured (for better or worse), and would make this a
[level 3 RMM](https://www.crummy.com/writing/speaking/2008-QCon/) compliant service,
when the links/relationships are included. That can be incredibly powerful.

As maintainers/developers we get the following from using an API Spec:

  * Standardized API shape, provides for strong abstractions
  * With a spec/standardization you can now remove the boilerplate altogether
    and potentially generate :allthetransportthings: with simple tooling
  * We eliminate some bike-shedding about API design. Kind of like `gofmt`,
    the API is no one's favorite, yet the API is everyone's favorite

Consumers benefit in the following ways:

  * A surprised consumer is an unhappy consumer, following a Spec (even a
    bad one), helps inform consumers and becomes simpler over time to
    reason about.
  * Consumers may not require any SDK/client lib and can traverse the API
    on their own. This is part of the salespitch for RMM lvl 3/JSON-API,
    though I'm not in 100% agreement that is worth the effort.

We've introduced a naive URI versioning scheme. There are a lot of ways
to slice this bread. The simplest is arguably the URI versioning scheme,
which is why we're using it here. However, there are a number of other
options available as well. Versioning is a tough pill to swallow for most
orgs. There are many strategies, and every strategy has 1000x opinions about
why THIS IS THE WAY. Explore the links below yourself, determine what's
important to your organization and go from there.

Take note, there are many conflicting opinions in the resources above :hidethepain:.
Another thing to take note of here is our use of middleware has increased to
include some additional checks. In this case we have some additional checks,
that all return the same response (via the API spec), and creates a one stop
shop for these orthogonal concerns.

For flavor, we've made use of generics to adhere to not only the JSON-API
spec, but also the reduce the boilerplate in dealing with handlers. We'll
expand on this in a bit.

Next we'll take a look at making our tests more flexible so that we can
extend our testcases without having to duplicate the entire test.

Refs: [Intro to Versioning a Rest API](https://www.freecodecamp.org/news/how-to-version-a-rest-api/)
Refs: [Versioning Rest Web API Best Practices - MSFT](https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design#versioning-a-restful-web-api)
Refs: [API Design Cheat Sheet](https://github.com/RestCheatSheet/api-cheat-sheet#api-design-cheat-sheet)
  • Loading branch information
jsteenb2 committed Jul 7, 2024
1 parent 30862b7 commit ac1360e
Show file tree
Hide file tree
Showing 4 changed files with 550 additions and 20 deletions.
9 changes: 6 additions & 3 deletions allsrv/errors.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package allsrv

const (
errTypeExists = "exists"
errTypeNotFound = "not found"
errTypeUnknown = iota
errTypeExists
errTypeInvalid
errTypeNotFound
errTypeUnAuthed
)

// Err provides a lightly structured error that we can attach behavior. Additionally,
// the use of fields makes it possible for us to enrich our logging infra without
// blowing up the message cardinality.
type Err struct {
Type string
Type int
Msg string
Fields []any
}
Expand Down
52 changes: 35 additions & 17 deletions allsrv/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"encoding/json"
"log"
"net/http"
"time"

"github.com/gofrs/uuid"
"github.com/hashicorp/go-metrics"
)

/*
Expand Down Expand Up @@ -59,33 +61,40 @@ type (
}
)

type Server struct {
db DB // 1)
mux *http.ServeMux // 4)
type serverOpts struct {
authFn func(http.Handler) http.Handler
idFn func() string
nowFn func() time.Time

authFn func(http.Handler) http.Handler // 3)
idFn func() string // 11)
met *metrics.Metrics
mux *http.ServeMux
}

// WithBasicAuth sets the authorization fn for the server to basic auth.
// 3)
func WithBasicAuth(user, pass string) func(*Server) {
return func(s *Server) {
func WithBasicAuth(user, pass string) func(*serverOpts) {
return func(s *serverOpts) {
s.authFn = basicAuth(user, pass)
}
}

// WithIDFn sets the id generation fn for the server.
func WithIDFn(fn func() string) func(*Server) {
return func(s *Server) {
func WithIDFn(fn func() string) func(*serverOpts) {
return func(s *serverOpts) {
s.idFn = fn
}
}

func NewServer(db DB, opts ...func(*Server)) *Server {
s := Server{
db: db,
mux: http.NewServeMux(), // 4)
type Server struct {
db DB // 1)
mux *http.ServeMux // 4)

authFn func(http.Handler) http.Handler // 3)
idFn func() string // 11)
}

func NewServer(db DB, opts ...func(*serverOpts)) *Server {
opt := serverOpts{
authFn: func(next http.Handler) http.Handler { // 3)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// defaults to no auth
Expand All @@ -96,9 +105,17 @@ func NewServer(db DB, opts ...func(*Server)) *Server {
// defaults to using a uuid
return uuid.Must(uuid.NewV4()).String()
},
mux: http.NewServeMux(),
}
for _, o := range opts {
o(&s)
o(&opt)
}

s := Server{
db: db,
mux: opt.mux, // 4)
authFn: opt.authFn,
idFn: opt.idFn,
}

s.routes()
Expand All @@ -122,9 +139,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {

type Foo struct {
// 6)
ID string `json:"id" gorm:"id"`
Name string `json:"name" gorm:"name"`
Note string `json:"note" gorm:"note"`
ID string `json:"id" gorm:"id"`
Name string `json:"name" gorm:"name"`
Note string `json:"note" gorm:"note"`
CreatedAt time.Time `json:"-" gorm:"created_at"`
}

func (s *Server) createFoo(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading

0 comments on commit ac1360e

Please sign in to comment.