Skip to content

Commit

Permalink
feature/swagger spec (#302)
Browse files Browse the repository at this point in the history
* Starting point of swagger spec

* Added many models and routes

* Added more routes

* Added potentially all remaining routes

* Fix validation errors

* Added client code generation target

* Ignore generated code

* WIP: Client side codegen

* Missed a few things

* Operation IDs

* Fix notes

* WIP: Server codegen

* Server codegen during build

* Lint requires codegen first

* Codegen needs install

* Install go-swagger

* Fix search

* Fix sorting

* Clean generated files

* Eliminate duplicate types

* Handle null arrays

* Use OAS 3.0 and oapi-codegen

* Special route for current user

* Assume go bin is on the path

* Fixed some types

* Remove golint

* Use RegisteredClaims since StandardClaims is deprecated

* Small tweaks for consistency

* Remove unused deps

* Use put for link

* Separate models into own spec
  • Loading branch information
chadweimer authored Nov 1, 2021
1 parent e00d46b commit 989159c
Show file tree
Hide file tree
Showing 76 changed files with 2,454 additions and 1,306 deletions.
2 changes: 0 additions & 2 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ plugins:
enabled: true
gofmt:
enabled: true
golint:
enabled: true
govet:
enabled: true
ratings:
Expand Down
2 changes: 2 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ RUN apt-get update \
gcc-aarch64-linux-gnu \
libc6-dev-arm64-cross \
gcc-mingw-w64-x86-64 \
# openapi-codegen dependencies
openjdk-11-jdk-headless \
# Puppeteer dependencies
ca-certificates \
fonts-liberation \
Expand Down
1 change: 1 addition & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
}
},
"extensions": [
"42crunch.vscode-openapi",
"christian-kohler.npm-intellisense",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
node-version: "14"
- uses: actions/setup-go@v2
with:
go-version: '^1.16.0'
go-version: "^1.16.0"
- uses: docker/setup-buildx-action@v1
- uses: docker/setup-qemu-action@v1
with:
Expand All @@ -27,6 +27,7 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- run: sudo apt-get update && sudo apt-get install zip gcc-arm-linux-gnueabihf libc6-dev-armhf-cross gcc-aarch64-linux-gnu libc6-dev-arm64-cross gcc-mingw-w64-x86-64
- run: make install
- run: make lint
- run: make build docker BUILD_VERSION="v0.0.0 (master)" DOCKER_TAG=latest
if: startswith(github.ref, 'refs/heads/master')
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ _testmain.go

# Ignore default runtime folder
/data

# Ignore generated files
generated/
1 change: 1 addition & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"42crunch.vscode-openapi",
"bbenoist.vagrant",
"christian-kohler.npm-intellisense",
"davidanson.vscode-markdownlint",
Expand Down
43 changes: 30 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ BUILD_WIN_AMD64_DIR=$(BUILD_DIR)/windows/amd64
CLIENT_INSTALL_DIR=static/node_modules
CLIENT_BUILD_DIR=static/www/static

CODEGEN_DIR=generated
CLIENT_CODEGEN_DIR=static/src/generated

GO_VERSION_FLAGS=-X 'github.com/chadweimer/gomp/metadata.BuildVersion=$(BUILD_VERSION)'
GO_LIN_LD_FLAGS=-ldflags "$(GO_VERSION_FLAGS) -extldflags '-static -static-libgcc'"
GO_WIN_LD_FLAGS=-ldflags "$(GO_VERSION_FLAGS)"
Expand All @@ -19,16 +22,17 @@ GO_ENV_LIN_ARM=GOOS=linux GOARCH=arm CGO_ENABLED=1 CC=arm-linux-gnueabihf-gcc
GO_ENV_LIN_ARM64=GOOS=linux GOARCH=arm64 CGO_ENABLED=1 CC=aarch64-linux-gnu-gcc
GO_ENV_WIN_AMD64=GOOS=windows GOARCH=amd64 CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc

GO_FILES := $(wildcard *.go) $(wildcard **/*.go)
DB_MIGRATION_FILES := $(wildcard db/migrations/*) $(wildcard db/migrations/**/*)
CLIENT_FILES := $(wildcard static/*) $(wildcard static/src/*) $(wildcard static/src/**/*)
GO_FILES := $(filter-out $(shell test -d $(CODEGEN_DIR) && find ./$(CODEGEN_DIR) -name "*"), $(shell find . -type f -name "*.go"))
DB_MIGRATION_FILES := $(shell find db/migrations -type f -name "*.*")
CLIENT_FILES := $(filter-out $(shell test -d $(CLIENT_CODEGEN_DIR) && find $(CLIENT_CODEGEN_DIR) -name "*"), $(shell find static -maxdepth 1 -type f -name "*") $(shell find static/src -type f -name "*"))

.DEFAULT_GOAL := build

# ---- INSTALL ----

.PHONY: install
install: $(CLIENT_INSTALL_DIR)
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest

$(CLIENT_INSTALL_DIR): static/package.json
cd static && npm install --silent
Expand All @@ -38,17 +42,29 @@ uninstall:
cd static && npm run clear


# ---- CODEGEN ----
$(CLIENT_CODEGEN_DIR): $(CLIENT_INSTALL_DIR) openapi.yaml
cd static && npm run codegen

$(CODEGEN_DIR): openapi.yaml
rm -rf $@
mkdir -p $@/models
oapi-codegen -generate types,skip-prune -package models models.yaml > $@/models/models.go
mkdir -p $@/api
oapi-codegen -generate types -package api -import-mapping=./models.yaml:github.com/chadweimer/gomp/generated/models openapi.yaml > $@/api/api.go


# ---- LINT ----

.PHONY: lint
lint: lint-client lint-server

.PHONY: lint-client
lint-client: $(CLIENT_INSTALL_DIR)
lint-client: $(CLIENT_INSTALL_DIR) $(CLIENT_CODEGEN_DIR)
cd static && npm run lint

.PHONY: lint-server
lint-server:
lint-server: $(CODEGEN_DIR)
go vet ./...


Expand All @@ -60,17 +76,18 @@ build: $(BUILD_LIN_AMD64_DIR) $(BUILD_LIN_ARM_DIR) $(BUILD_LIN_ARM64_DIR) $(BUIL
.PHONY: clean
clean: clean-linux-amd64 clean-linux-arm clean-linux-arm64 clean-windows-amd64
rm -rf $(BUILD_DIR)
rm -rf $(CODEGEN_DIR)

# - GENERIC ARCH -

$(CLIENT_BUILD_DIR): $(CLIENT_INSTALL_DIR) $(CLIENT_FILES)
cd static && npm run build
$(CLIENT_BUILD_DIR): $(CLIENT_INSTALL_DIR) $(CLIENT_CODEGEN_DIR) $(CLIENT_FILES)
rm -rf $@ && cd static && npm run build

$(BUILD_DIR)/%/db/migrations: $(DB_MIGRATION_FILES)
mkdir -p $@ && cp -R db/migrations/* $@
rm -rf $@ && mkdir -p $@ && cp -R db/migrations/* $@

$(BUILD_DIR)/%/static: $(CLIENT_BUILD_DIR)
mkdir -p $@ && cp -R $</* $@
rm -rf $@ && mkdir -p $@ && cp -R $</* $@

.PHONY: clean-client
clean-client:
Expand All @@ -80,7 +97,7 @@ clean-client:

$(BUILD_LIN_AMD64_DIR): $(BUILD_LIN_AMD64_DIR)/gomp $(BUILD_LIN_AMD64_DIR)/db/migrations $(BUILD_LIN_AMD64_DIR)/static

$(BUILD_LIN_AMD64_DIR)/gomp: go.mod $(GO_FILES)
$(BUILD_LIN_AMD64_DIR)/gomp: go.mod $(CODEGEN_DIR) $(GO_FILES)
$(GO_ENV_LIN_AMD64) go build -o $@ $(GO_LIN_LD_FLAGS)

.PHONY: clean-linux-amd64
Expand All @@ -93,7 +110,7 @@ clean-linux-amd64: clean-client

$(BUILD_LIN_ARM_DIR): $(BUILD_LIN_ARM_DIR)/gomp $(BUILD_LIN_ARM_DIR)/db/migrations $(BUILD_LIN_ARM_DIR)/static

$(BUILD_LIN_ARM_DIR)/gomp: go.mod $(GO_FILES)
$(BUILD_LIN_ARM_DIR)/gomp: go.mod $(CODEGEN_DIR) $(GO_FILES)
$(GO_ENV_LIN_ARM) go build -o $@ $(GO_LIN_LD_FLAGS)

.PHONY: clean-linux-arm
Expand All @@ -106,7 +123,7 @@ clean-linux-arm: clean-client

$(BUILD_LIN_ARM64_DIR): $(BUILD_LIN_ARM64_DIR)/gomp $(BUILD_LIN_ARM64_DIR)/db/migrations $(BUILD_LIN_ARM64_DIR)/static

$(BUILD_LIN_ARM64_DIR)/gomp: go.mod $(GO_FILES)
$(BUILD_LIN_ARM64_DIR)/gomp: go.mod $(CODEGEN_DIR) $(GO_FILES)
$(GO_ENV_LIN_ARM64) go build -o $@ $(GO_LIN_LD_FLAGS)

.PHONY: clean-linux-arm64
Expand All @@ -119,7 +136,7 @@ clean-linux-arm64: clean-client

$(BUILD_WIN_AMD64_DIR): $(BUILD_WIN_AMD64_DIR)/gomp.exe $(BUILD_WIN_AMD64_DIR)/db/migrations $(BUILD_WIN_AMD64_DIR)/static

$(BUILD_WIN_AMD64_DIR)/gomp.exe: go.mod $(GO_FILES)
$(BUILD_WIN_AMD64_DIR)/gomp.exe: go.mod $(CODEGEN_DIR) $(GO_FILES)
$(GO_ENV_WIN_AMD64) go build -o $@ $(GO_WIN_LD_FLAGS)

.PHONY: clean-windows-amd64
Expand Down
105 changes: 58 additions & 47 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (

// ---- Begin Standard Errors ----

var errMismatchedID = errors.New("id in the path does not match the one specified in the request body")
var errMismatchedId = errors.New("id in the path does not match the one specified in the request body")

// ---- End Standard Errors ----

Expand All @@ -27,12 +27,12 @@ var errMismatchedID = errors.New("id in the path does not match the one specifie
type routeKey string

const (
destRecipeIDKey routeKey = "destRecipeID"
filterIDKey routeKey = "filterID"
imageIDKey routeKey = "imageID"
noteIDKey routeKey = "noteID"
recipeIDKey routeKey = "recipeID"
userIDKey routeKey = "userID"
destRecipeIdKey routeKey = "destRecipeId"
filterIdKey routeKey = "filterId"
imageIdKey routeKey = "imageId"
noteIdKey routeKey = "noteId"
recipeIdKey routeKey = "recipeId"
userIdKey routeKey = "userId"
)

// ---- End Route Keys ----
Expand All @@ -48,7 +48,7 @@ func (k *contextKey) String() string {
}

var (
currentUserIDCtxKey = &contextKey{"CurrentUserID"}
currentUserIdCtxKey = &contextKey{"CurrentUserId"}
currentUserAccessLevelCtxKey = &contextKey{"CurrentUserAccessLevel"}
)

Expand Down Expand Up @@ -82,30 +82,29 @@ func NewHandler(cfg *conf.Config, upl upload.Driver, db db.Driver) http.Handler
r.Use(h.requireAuthentication)

r.Get("/recipes", h.getRecipes)
r.Get(fmt.Sprintf("/recipes/{%s}", recipeIDKey), h.getRecipe)
r.Get(fmt.Sprintf("/recipes/{%s}/image", recipeIDKey), h.getRecipeMainImage)
r.Get(fmt.Sprintf("/recipes/{%s}/images", recipeIDKey), h.getRecipeImages)
r.Get(fmt.Sprintf("/recipes/{%s}/notes", recipeIDKey), h.getRecipeNotes)
r.Get(fmt.Sprintf("/recipes/{%s}/links", recipeIDKey), h.getRecipeLinks)
r.Get("/tags", h.getTags)
r.Get(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.getRecipe)
r.Get(fmt.Sprintf("/recipes/{%s}/image", recipeIdKey), h.getRecipeMainImage)
r.Get(fmt.Sprintf("/recipes/{%s}/images", recipeIdKey), h.getRecipeImages)
r.Get(fmt.Sprintf("/recipes/{%s}/notes", recipeIdKey), h.getRecipeNotes)
r.Get(fmt.Sprintf("/recipes/{%s}/links", recipeIdKey), h.getRecipeLinks)

// Editor
r.Group(func(r chi.Router) {
r.Use(h.requireEditor)

r.Post("/recipes", h.postRecipe)
r.Put(fmt.Sprintf("/recipes/{%s}", recipeIDKey), h.putRecipe)
r.Delete(fmt.Sprintf("/recipes/{%s}", recipeIDKey), h.deleteRecipe)
r.Put(fmt.Sprintf("/recipes/{%s}/state", recipeIDKey), h.putRecipeState)
r.Put(fmt.Sprintf("/recipes/{%s}/rating", recipeIDKey), h.putRecipeRating)
r.Put(fmt.Sprintf("/recipes/{%s}/image", recipeIDKey), h.putRecipeMainImage)
r.Post(fmt.Sprintf("/recipes/{%s}/images", recipeIDKey), h.postRecipeImage)
r.Post(fmt.Sprintf("/recipes/{%s}/links", recipeIDKey), h.postRecipeLink)
r.Delete(fmt.Sprintf("/recipes/{%s}/links/{%s}", recipeIDKey, destRecipeIDKey), h.deleteRecipeLink)
r.Delete(fmt.Sprintf("/recipes/{%s}/images/{%s}", recipeIDKey, imageIDKey), h.deleteImage)
r.Post("/notes", h.postNote)
r.Put(fmt.Sprintf("/notes/{%s}", noteIDKey), h.putNote)
r.Delete(fmt.Sprintf("/notes/{%s}", noteIDKey), h.deleteNote)
r.Put(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.putRecipe)
r.Delete(fmt.Sprintf("/recipes/{%s}", recipeIdKey), h.deleteRecipe)
r.Put(fmt.Sprintf("/recipes/{%s}/state", recipeIdKey), h.putRecipeState)
r.Put(fmt.Sprintf("/recipes/{%s}/rating", recipeIdKey), h.putRecipeRating)
r.Put(fmt.Sprintf("/recipes/{%s}/image", recipeIdKey), h.putRecipeMainImage)
r.Post(fmt.Sprintf("/recipes/{%s}/images", recipeIdKey), h.postRecipeImage)
r.Put(fmt.Sprintf("/recipes/{%s}/links/{%s}", recipeIdKey, destRecipeIdKey), h.putRecipeLink)
r.Delete(fmt.Sprintf("/recipes/{%s}/links/{%s}", recipeIdKey, destRecipeIdKey), h.deleteRecipeLink)
r.Delete(fmt.Sprintf("/recipes/{%s}/images/{%s}", recipeIdKey, imageIdKey), h.deleteImage)
r.Post(fmt.Sprintf("/recipes/{%s}/notes", recipeIdKey), h.postNote)
r.Put(fmt.Sprintf("/recipes/{%s}/notes/{%s}", recipeIdKey, noteIdKey), h.putNote)
r.Delete(fmt.Sprintf("/recipes/{%s}/notes/{%s}", recipeIdKey, noteIdKey), h.deleteNote)
r.Post("/uploads", h.postUpload)
})

Expand All @@ -118,23 +117,23 @@ func NewHandler(cfg *conf.Config, upl upload.Driver, db db.Driver) http.Handler
r.Post("/users", h.postUser)

// Don't allow deleting self
r.With(h.disallowSelf).Delete(fmt.Sprintf("/users/{%s}", userIDKey), h.deleteUser)
r.With(h.disallowSelf).Delete(fmt.Sprintf("/users/{%s}", userIdKey), h.deleteUser)
})

// Admin or Self
r.Group(func(r chi.Router) {
r.Use(h.requireAdminUnlessSelf)

r.Get(fmt.Sprintf("/users/{%s}", userIDKey), h.getUser)
r.Put(fmt.Sprintf("/users/{%s}", userIDKey), h.putUser)
r.Put(fmt.Sprintf("/users/{%s}/password", userIDKey), h.putUserPassword)
r.Get(fmt.Sprintf("/users/{%s}/settings", userIDKey), h.getUserSettings)
r.Put(fmt.Sprintf("/users/{%s}/settings", userIDKey), h.putUserSettings)
r.Get(fmt.Sprintf("/users/{%s}/filters", userIDKey), h.getUserFilters)
r.Post(fmt.Sprintf("/users/{%s}/filters", userIDKey), h.postUserFilter)
r.Get(fmt.Sprintf("/users/{%s}/filters/{%s}", userIDKey, filterIDKey), h.getUserFilter)
r.Put(fmt.Sprintf("/users/{%s}/filters/{%s}", userIDKey, filterIDKey), h.putUserFilter)
r.Delete(fmt.Sprintf("/users/{%s}/filters/{%s}", userIDKey, filterIDKey), h.deleteUserFilter)
r.Get(fmt.Sprintf("/users/{%s}", userIdKey), h.getUser)
r.Put(fmt.Sprintf("/users/{%s}", userIdKey), h.putUser)
r.Put(fmt.Sprintf("/users/{%s}/password", userIdKey), h.putUserPassword)
r.Get(fmt.Sprintf("/users/{%s}/settings", userIdKey), h.getUserSettings)
r.Put(fmt.Sprintf("/users/{%s}/settings", userIdKey), h.putUserSettings)
r.Get(fmt.Sprintf("/users/{%s}/filters", userIdKey), h.getUserFilters)
r.Post(fmt.Sprintf("/users/{%s}/filters", userIdKey), h.postUserFilter)
r.Get(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.getUserFilter)
r.Put(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.putUserFilter)
r.Delete(fmt.Sprintf("/users/{%s}/filters/{%s}", userIdKey, filterIdKey), h.deleteUserFilter)
})
})
})
Expand All @@ -159,7 +158,11 @@ func (h *apiHandler) NoContent(resp http.ResponseWriter) {
resp.WriteHeader(http.StatusNoContent)
}

func (h *apiHandler) Created(resp http.ResponseWriter, location string) {
func (h *apiHandler) Created(resp http.ResponseWriter, v interface{}) {
h.JSON(resp, http.StatusCreated, v)
}

func (h *apiHandler) CreatedWithLocation(resp http.ResponseWriter, location string) {
resp.Header().Set("Location", location)
resp.WriteHeader(http.StatusCreated)
}
Expand Down Expand Up @@ -196,12 +199,12 @@ func readJSONFromRequest(req *http.Request, data interface{}) error {
return json.NewDecoder(req.Body).Decode(data)
}

func getResourceIDFromURL(req *http.Request, idKey routeKey) (int64, error) {
func getResourceIdFromUrl(req *http.Request, idKey routeKey) (int64, error) {
idStr := chi.URLParam(req, string(idKey))

// Special case for userID
if idKey == userIDKey && idStr == "current" {
return getResourceIDFromCtx(req, currentUserIDCtxKey)
// Special case for userId
if idKey == userIdKey && idStr == "current" {
return getResourceIdFromCtx(req, currentUserIdCtxKey)
}

id, err := strconv.ParseInt(idStr, 10, 64)
Expand All @@ -212,10 +215,18 @@ func getResourceIDFromURL(req *http.Request, idKey routeKey) (int64, error) {
return id, nil
}

func getResourceIDFromCtx(req *http.Request, idKey *contextKey) (int64, error) {
id, ok := req.Context().Value(idKey).(int64)
if !ok {
return 0, fmt.Errorf("value of %s is not an integer", idKey)
func getResourceIdFromCtx(req *http.Request, idKey *contextKey) (int64, error) {
idVal := req.Context().Value(idKey)

id, ok := idVal.(int64)
if ok {
return id, nil
}
return id, nil

idPtr, ok := idVal.(*int64)
if ok {
return *idPtr, nil
}

return 0, fmt.Errorf("value of %s is not an integer", idKey)
}
2 changes: 1 addition & 1 deletion api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"net/http"

"github.com/chadweimer/gomp/generated/models"
"github.com/chadweimer/gomp/metadata"
"github.com/chadweimer/gomp/models"
)

func (h *apiHandler) getAppInfo(resp http.ResponseWriter, req *http.Request) {
Expand Down
Loading

0 comments on commit 989159c

Please sign in to comment.