Skip to content

Commit

Permalink
POC for generating API endpoints using code generation
Browse files Browse the repository at this point in the history
Note: This PR is messy as it includes some refactoring of content from
the 'fastly' package into new 'errors' and 'decodebody' packages;
those will be split out into their own PRs before this is ready for
review.

This PR is a proof-of-concept for generating API endpoints using the
Jennifer code-generation module. It generates endpoints for Brotli
compression and NGWAF as examples. For NGWAF, it includes
configuration-during-enablement for the 'workspace_id'.
  • Loading branch information
kpfleming committed Nov 25, 2024
1 parent a84ac0d commit b3991a9
Show file tree
Hide file tree
Showing 19 changed files with 1,194 additions and 641 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ _testmain.go
*.prof

bin/
pkg/
RELEASE_CHANGELOG.md

.envrc
Expand Down
69 changes: 37 additions & 32 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,9 @@ FASTLY_API_KEY ?=
# Enables support for tools such as https://github.com/rakyll/gotest
TEST_COMMAND ?= $(GO) test

all: mod-download dev-dependencies tidy fmt fiximports test vet staticcheck semgrep ## Runs all of the required cleaning and verification targets.
all: mod-download dev-dependencies tidy generate fmt fiximports test vet staticcheck semgrep ## Runs all of the required cleaning and verification targets.
.PHONY: all

tidy: ## Cleans the Go module.
@echo "==> Tidying module"
@$(GO) mod tidy
.PHONY: tidy

mod-download: ## Downloads the Go module.
@echo "==> Downloading Go module"
@$(GO) mod download
Expand All @@ -46,11 +41,47 @@ dev-dependencies: ## Downloads the necessary dev dependencies.
@if [[ "$$(uname)" == 'Darwin' ]]; then brew install semgrep; fi
.PHONY: dev-dependencies

tidy: ## Cleans the Go module.
@echo "==> Tidying module"
@$(GO) mod tidy
.PHONY: tidy

generate: ## Builds and runs generators, to generate code for API endpoints and tests.
@echo "==> Building generators"
@$(GO) build -o generators ./internal/generators/...
@echo "==> Generating code from templates"
@PATH=${PATH}:$(shell pwd)/generators $(GO) generate -v ./internal/templates/...

fmt: ## Properly formats Go files and orders dependencies.
@echo "==> Running gofmt"
@gofmt -s -w ${GOFILES}
.PHONY: fmt

fiximports: ## Properly formats and orders imports.
@echo "==> Fixing imports"
@goimports -w {fastly,tools}
.PHONY: fiximports

test: ## Runs the test suite with VCR mocks enabled.
@echo "==> Testing ${NAME}"
@$(TEST_COMMAND) -timeout=30s -parallel=20 -tags="${GOTAGS}" ${GOPKGS} ${TESTARGS}
.PHONY: test

vet: ## Identifies common errors.
@echo "==> Running go vet"
@$(GO) vet ./...
.PHONY: vet

staticcheck: ## Runs the staticcheck linter.
@echo "==> Running staticcheck"
@staticcheck -version
@staticcheck ./...
.PHONY: staticcheck

semgrep: ## Run semgrep checker.
if command -v semgrep &> /dev/null; then semgrep ci --config auto --exclude-rule generic.secrets.security.detected-private-key.detected-private-key $(SEMGREP_ARGS); fi
.PHONY: semgrep

test-race: ## Runs the test suite with the -race flag to identify race conditions, if they exist.
@echo "==> Testing ${NAME} (race)"
@$(TEST_COMMAND) -timeout=60s -race -tags="${GOTAGS}" ${GOPKGS} ${TESTARGS}
Expand Down Expand Up @@ -85,35 +116,9 @@ check-mod: ## A check which lists extraneous dependencies, if they exist.
@$(shell pwd)/scripts/check-mod.sh
.PHONY: check-mod

fiximports: ## Properly formats and orders imports.
@echo "==> Fixing imports"
@goimports -w {fastly,tools}
.PHONY: fiximports

fmt: ## Properly formats Go files and orders dependencies.
@echo "==> Running gofmt"
@gofmt -s -w ${GOFILES}
.PHONY: fmt

vet: ## Identifies common errors.
@echo "==> Running go vet"
@$(GO) vet ./...
.PHONY: vet

staticcheck: ## Runs the staticcheck linter.
@echo "==> Running staticcheck"
@staticcheck -version
@staticcheck ./...
.PHONY: staticcheck

nilaway: ## Run nilaway
@nilaway ./...

# Run semgrep checker.
.PHONY: semgrep
semgrep:
if command -v semgrep &> /dev/null; then semgrep ci --config auto --exclude-rule generic.secrets.security.detected-private-key.detected-private-key $(SEMGREP_ARGS); fi

.PHONY: help
help: ## Prints this help menu.
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
Expand Down
90 changes: 3 additions & 87 deletions fastly/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ import (
"net/url"
"os"
"path/filepath"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"

"github.com/fastly/go-fastly/v9/pkg/decodebody"
"github.com/google/go-querystring/query"
"github.com/google/jsonapi"
"github.com/hashicorp/go-cleanhttp"
"github.com/mitchellh/mapstructure"
)

// APIKeyEnvVar is the name of the environment variable where the Fastly API
Expand Down Expand Up @@ -653,95 +652,12 @@ func checkResp(resp *http.Response, err error) (*http.Response, error) {

// decodeBodyMap is used to decode an HTTP response body into a mapstructure struct.
func decodeBodyMap(body io.Reader, out any) error {
var parsed any
dec := json.NewDecoder(body)
if err := dec.Decode(&parsed); err != nil {
return err
}

return decodeMap(parsed, out)
return decodebody.DecodeBodyMap(body, out)
}

// decodeMap decodes an `in` struct or map to a mapstructure tagged `out`.
// It applies the decoder defaults used throughout go-fastly.
// Note that this uses opposite argument order from Go's copy().
func decodeMap(in, out any) error {
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapToHTTPHeaderHookFunc(),
stringToTimeHookFunc(),
),
WeaklyTypedInput: true,
Result: out,
})
if err != nil {
return err
}
return decoder.Decode(in)
}

// mapToHTTPHeaderHookFunc returns a function that converts maps into an
// http.Header value.
func mapToHTTPHeaderHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data any,
) (any, error) {
if f.Kind() != reflect.Map {
return data, nil
}
if t != reflect.TypeOf(new(http.Header)) {
return data, nil
}

typed, ok := data.(map[string]any)
if !ok {
return nil, fmt.Errorf("cannot convert %T to http.Header", data)
}

n := map[string][]string{}
for k, v := range typed {
switch tv := v.(type) {
case string:
n[k] = []string{tv}
case []string:
n[k] = tv
case int, int8, int16, int32, int64:
n[k] = []string{fmt.Sprintf("%d", tv)}
case float32, float64:
n[k] = []string{fmt.Sprintf("%f", tv)}
default:
return nil, fmt.Errorf("cannot convert %T to http.Header", v)
}
}

return n, nil
}
}

// stringToTimeHookFunc returns a function that converts strings to a time.Time
// value.
func stringToTimeHookFunc() mapstructure.DecodeHookFunc {
return func(
f reflect.Type,
t reflect.Type,
data any,
) (any, error) {
if f.Kind() != reflect.String {
return data, nil
}
if t != reflect.TypeOf(time.Now()) {
return data, nil
}

// Convert it by parsing
v, err := time.Parse(time.RFC3339, data.(string))
if err != nil {
// DictionaryInfo#get uses it's own special time format for now.
v, _ := data.(string) // type assert to avoid runtime panic (v will have zero value for its type)
return time.Parse("2006-01-02 15:04:05", v)
}
return v, err
}
return decodebody.DecodeMap(in, out)
}
Loading

0 comments on commit b3991a9

Please sign in to comment.