Skip to content

Commit

Permalink
Play with grpc/connect and refactor echo server
Browse files Browse the repository at this point in the history
  • Loading branch information
pcriv committed Dec 18, 2024
1 parent 03344a5 commit a282a1e
Show file tree
Hide file tree
Showing 53 changed files with 2,786 additions and 806 deletions.
4 changes: 4 additions & 0 deletions .covignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gen.go
/testdata/
/scripts/
/tests/
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: 1.21
go-version: 1.23
id: go

- name: Check out code into the Go module directory
Expand Down
20 changes: 10 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
.build/
.DS_Store
.idea/
.vscode/
*.dll
*.dylib
*.exe
*.exe~
*.dll
*.out
*.so
*.dylib
*.test
*.out
/vendor/
/Godeps/
.vscode/
/node_modules/
/coverage/
NOTES.md
.build/
.DS_Store
/node_modules/
/vendor/
NOTES.md
6 changes: 0 additions & 6 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
---
run:
modules-download-mode: vendor

output:
format: colored-line-number

linters:
disable-all: true
enable:
Expand Down
3 changes: 0 additions & 3 deletions .yamlfmt.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
---
doublestar: true
exclude:
- "./vendor/**/*"
formatter:
include_document_start: true
retain_line_breaks: true
44 changes: 24 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@ THIS_MAKEFILE := $(abspath $(lastword $(MAKEFILE_LIST)))
export GOOS ?= $(shell go env GOOS)
export GOARCH ?= $(shell go env GOARCH)

# Ensure CI var is empty if false - this makes it easier to do conditionals
ifeq ($(CI),false)
CI :=
endif

##@ General

## Print this help message
Expand Down Expand Up @@ -46,21 +41,21 @@ clean:
rm -rf tools/

## Setup local environment
setup: deps githooks .env .env.test
setup: deps git-hooks .env .env.test

## Install git hooks
githooks:
git-hooks:
lefthook install

## Install dependencies
deps:
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
go install github.com/joho/godotenv/cmd/godotenv@latest
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
go install gotest.tools/gotestsum@latest
go install github.com/evilmartians/lefthook@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
go install github.com/google/yamlfmt/cmd/yamlfmt@latest


##@ Build

## Build for local platform
Expand All @@ -71,12 +66,14 @@ build:

## Run tests
test:
godotenv -f .env.test go test -v -mod=vendor -cover -json ./... 2>&1 | tee /tmp/gotest.log | gotestfmt
godotenv -f .env.test gotestsum --format=testdox -- -cover ./...

## Run tests with coverage
test.coverage:
mkdir -p ./coverage
godotenv -f .env.test go test -v -mod=vendor -json ./... -covermode=count -coverpkg=./... -coverprofile coverage/coverage.out | gotestfmt
godotenv -f .env.test gotestsum --format=testdox -- -covermode=count -coverpkg=./... -coverprofile coverage/coverage.out ./...
grep -v -E -f .covignore ./coverage/coverage.out > ./coverage/coverage.filtered.out
mv ./coverage/coverage.filtered.out ./coverage/coverage.out
go tool cover -func coverage/coverage.out -o coverage/coverage.tool
go tool cover -html coverage/coverage.out -o coverage/coverage.html

Expand All @@ -94,13 +91,21 @@ lint.go:
## Lint yaml files
# Autofix is disabled if CI is set
lint.yml:
yamlfmt -conf .yamlfmt.yml $(if $(CI),-lint -dry) '.github/**/*{.yaml,yml}' '*.{yml,yaml}'
yamlfmt -conf .yamlfmt.yml

##@ Local Development

## Run locally
local.run:
godotenv -f .env go run cmd/server/main.go
## Run grpc-server
grpc-server:
godotenv -f .env go run ./cmd/grpc-server/

## Run connect-server
connect-server:
godotenv -f .env go run ./cmd/connect-server/

## Run rest-server
rest-server:
godotenv -f .env go run ./cmd/rest-server/

# Ensures .env exists
.env:
Expand All @@ -114,10 +119,9 @@ local.run:

##@ Code Generation

## Generate sources from OpenAPI spec
gen.openapi:
oapi-codegen -generate types -package openapi -o internal/web/openapi/types.gen.go openapi/spec.yml
oapi-codegen -generate server -package openapi -o internal/web/openapi/server.gen.go openapi/spec.yml
oapi-codegen -generate spec -package openapi -o internal/web/openapi/spec.gen.go openapi/spec.yml
## Generate code
gen:
go generate ./...
buf generate proto/

.PRECIOUS: .env .env.test
21 changes: 21 additions & 0 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: v2
managed:
enabled: true
disable:
- file_option: go_package
module: buf.build/bufbuild/protovalidate
plugins:
# grpc
- remote: buf.build/grpc/go
out: proto
opt:
- paths=source_relative
- require_unimplemented_servers=false
- remote: buf.build/protocolbuffers/go
out: proto
opt: paths=source_relative
# connect
- remote: buf.build/connectrpc/go:v1.16.2
out: proto
opt:
- paths=source_relative
6 changes: 6 additions & 0 deletions buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: a3320276596649bcad929ac829d451f4
digest: b5:285a6d3a423b195a21f45aacc97ee222ac09cfb01a42f0d546aa51d92177b0b9d00eb9ae93e72dabbbefdc77f35a4c7a11f15d913cc08da764fcb6071f85d148
5 changes: 5 additions & 0 deletions buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
version: v2
modules:
- path: proto
deps:
- buf.build/bufbuild/protovalidate
87 changes: 87 additions & 0 deletions cmd/connect-server/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"context"
"errors"

"connectrpc.com/connect"

"github.com/pcriv/mancala/internal/mancala"
"github.com/pcriv/mancala/internal/protomap"
"github.com/pcriv/mancala/proto"
"github.com/pcriv/mancala/proto/protoconnect"
googleproto "google.golang.org/protobuf/proto"
)

var _ protoconnect.ServiceHandler = handler{}

type (
requestValidator interface {
Validate(msg googleproto.Message) error
}

handler struct {
service mancala.Service
validator requestValidator
}
)

func (h handler) CreateGame(ctx context.Context, in *connect.Request[proto.CreateGameRequest]) (*connect.Response[proto.CreateGameResponse], error) {
err := h.validator.Validate(in.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}

g, err := h.service.CreateGame(ctx, in.Msg.Player1, in.Msg.Player2)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&proto.CreateGameResponse{
CreatedGame: protomap.Game(g),
}), nil
}

func (h handler) FindGame(ctx context.Context, in *connect.Request[proto.FindGameRequest]) (*connect.Response[proto.FindGameResponse], error) {
err := h.validator.Validate(in.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}

g, err := h.service.FindGame(ctx, in.Msg.Id)
if err != nil {
switch {
case errors.Is(err, mancala.ErrGameNotFound):
return nil, connect.NewError(connect.CodeNotFound, err)
default:
return nil, connect.NewError(connect.CodeInternal, err)
}
}

return connect.NewResponse(&proto.FindGameResponse{
Game: protomap.Game(g),
}), nil
}

func (h handler) ExecutePlay(ctx context.Context, in *connect.Request[proto.ExecutePlayRequest]) (*connect.Response[proto.ExecutePlayResponse], error) {
err := h.validator.Validate(in.Msg)
if err != nil {
return nil, connect.NewError(connect.CodeInvalidArgument, err)
}

g, err := h.service.ExecutePlay(ctx, in.Msg.GameId, in.Msg.PitIndex)
if err != nil {
switch {
case errors.Is(err, mancala.ErrGameNotFound):
return nil, connect.NewError(connect.CodeNotFound, err)
case errors.Is(err, mancala.ErrInvalidPlay):
return nil, connect.NewError(connect.CodeInvalidArgument, err)
default:
return nil, connect.NewError(connect.CodeInternal, err)
}
}

return connect.NewResponse(&proto.ExecutePlayResponse{
PlayedPitIndex: in.Msg.PitIndex,
Game: protomap.Game(g),
}), nil
}
115 changes: 115 additions & 0 deletions cmd/connect-server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"

"github.com/bufbuild/protovalidate-go"
"github.com/caarlos0/env/v11"
"github.com/pcriv/mancala/internal/mancala"
"github.com/pcriv/mancala/proto/protoconnect"
"github.com/redis/go-redis/v9"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"golang.org/x/sync/errgroup"

redisstore "github.com/pcriv/mancala/internal/store/redis"
)

type envConfig struct {
Env string `env:"ENV" envDefault:"local"`
LogLevel string `env:"LOG_LEVEL" envDefault:"debug"`
RedisURL string `env:"REDIS_URL,required"`
Address string `env:"ADDRESS" envDefault:":50051"`
}

func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()

err := run(ctx)
if err != nil && !errors.Is(err, context.Canceled) {
slog.Error("error running application", slog.String("error", err.Error()))
os.Exit(1)
}

slog.Info("closing server gracefully")
}

func run(ctx context.Context) error {
cfg := envConfig{}
if err := env.Parse(&cfg); err != nil {
return fmt.Errorf("unable to parse config: %w", err)
}

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))

redisClient, err := newRedisClient(ctx, cfg.RedisURL)
if err != nil {
return err
}
gameStore := redisstore.NewGameStore(redisClient)

validator, err := protovalidate.New()
if err != nil {
return fmt.Errorf("failed to create validator: %w", err)
}

path, handler := protoconnect.NewServiceHandler(
handler{
service: mancala.NewService(gameStore),
validator: validator,
},
)

mux := http.NewServeMux()
mux.Handle(path, handler)

// Use h2c so we can serve HTTP/2 without TLS.
srv := http.Server{
Addr: cfg.Address,
Handler: h2c.NewHandler(mux, &http2.Server{}),
}

g, ctx := errgroup.WithContext(ctx)

g.Go(func() error {
slog.Info("starting connect server on address", slog.String("address", cfg.Address))

if err := srv.ListenAndServe(); err != nil {
return fmt.Errorf("failed to listen and serve connect service: %w", err)
}

return nil
})

g.Go(func() error {
<-ctx.Done()

if err := srv.Close(); err != nil {
return fmt.Errorf("failed to close server: %w", err)
}

return nil
})

return g.Wait()
}

func newRedisClient(ctx context.Context, url string) (*redis.Client, error) {
options, err := redis.ParseURL(url)
if err != nil {
return nil, err
}
client := redis.NewClient(options)
_, err = client.Ping(ctx).Result()
if err != nil {
return nil, err
}
return client, nil
}
Loading

0 comments on commit a282a1e

Please sign in to comment.