diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6ed914b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +--- +version: 2 +updates: +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" +- package-ecosystem: "gomod" + directory: "/app" + schedule: + interval: "weekly" +- package-ecosystem: "docker" + directory: "/app" + schedule: + interval: "weekly" diff --git a/.github/workflows/app.yaml b/.github/workflows/app.yaml new file mode 100644 index 0000000..68adb11 --- /dev/null +++ b/.github/workflows/app.yaml @@ -0,0 +1,96 @@ +--- +name: app + +on: [push, pull_request] + +defaults: + run: + working-directory: ./app + +jobs: + prepare: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: true + experimental: true + - uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + cache-dependency-path: 'app/go.sum' + - run: go mod tidy + - run: task gen + - name: Save task cache + uses: actions/cache/save@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - name: Check git status + run: if [[ -n $(git status --porcelain) ]]; then git status --porcelain && exit 1; fi + + lint: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + cache-dependency-path: 'app/go.sum' + - uses: golangci/golangci-lint-action@v6 + with: + version: v1.61.0 + working-directory: ./app + args: --path-prefix=./ + + test: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + cache-dependency-path: 'app/go.sum' + - run: task test + + build: + needs: [prepare] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Restore task cache + uses: actions/cache/restore@v4 + with: + path: ./.task + key: task-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.task/checksum/*') }} + - uses: jdx/mise-action@v2 + with: + version: 2024.10.7 + install: false + - uses: actions/setup-go@v5 + with: + go-version: '1.23.2' + cache-dependency-path: 'app/go.sum' + - run: task build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bc9ab5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Misc +.DS_Store + +# Environment +.env +.env.local +.env.*.local + +# Editor +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# Go +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work + +# Dist +dist/ + +# Task +.task/ \ No newline at end of file diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..1c51d44 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,11 @@ +[tools] +atlas = "0.28.0" +golang = "1.23.2" +golangci-lint = "1.61.0" +protoc = "28.2" +protoc-gen-go = "1.35.1" +protoc-gen-go-grpc = "1.5.1" +task = "3.39.2" + +"go:github.com/sqlc-dev/sqlc/cmd/sqlc" = "1.27.0" +"go:goa.design/goa/v3/cmd/goa" = "3.19.1" diff --git a/README.md b/README.md index 6a75e7d..a85f711 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # Count Up -A little web app game, consolidating the best practices from my years of experience building production-ready software and infrastructure. \ No newline at end of file +A little web app game, consolidating the best practices from my years of experience building production-ready software and infrastructure. + +Probably way overengineered for incrementing a counter, but thought this will be a good exercise to codify my learnings! + +## Features + +- Async, transactional worker system for processing background jobs using [River](https://riverqueue.com/). +- Compiled SQL queries into type-safe application code using [sqlc](https://sqlc.dev/). +- Declarative database schema and migrations using a combination of [Atlas](https://atlasgo.io/) + [Goose](https://pressly.github.io/goose/). +- DDD-lite, interface-driven approach to writing decoupled and testable business logic. \ No newline at end of file diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..6d8d0a8 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,135 @@ +--- +version: '3' + +vars: + VERSION: + sh: git describe --tags --exact-match HEAD 2>/dev/null || echo "0.0.0" + GIT_COMMIT: + sh: git rev-parse --short HEAD + LDFLAGS: + - '-X github.com/jace-ys/countup/internal/versioninfo.Version={{ .VERSION }}' + - '-X github.com/jace-ys/countup/internal/versioninfo.CommitSHA={{ .GIT_COMMIT }}' + +tasks: + run:server: + deps: ['gen'] + dir: app + cmds: + - go run ./cmd/countup/... server {{ .CLI_ARGS }} + env: + DEBUG: true + OTEL_GO_X_EXEMPLAR: true + OTEL_RESOURCE_ATTRIBUTES: tier=app,environment=dev + DATABASE_CONNECTION_URI: postgresql://countup:countup@localhost:5432/countup + + run:client: + deps: ['gen'] + dir: app + cmds: + - go run ./cmd/countup-cli/... {{ .CLI_ARGS }} + + test: + deps: ['gen'] + dir: app + cmds: + - go test -race ./... + + lint: + deps: ['gen'] + dir: app + cmds: + - golangci-lint run ./... + + gen: + dir: app + cmds: + - task: gen:api:v1 + - task: gen:sqlc + + gen:api:*: + internal: true + dir: app + vars: + API_VERSION: '{{ index .MATCH 0 }}' + cmds: + - goa gen github.com/jace-ys/countup/api/{{ .API_VERSION }} -o api/{{ .API_VERSION }} + sources: + - api/{{ .API_VERSION }}/*.go + generates: + - api/{{ .API_VERSION }}/gen/**/*.go + + gen:sqlc: + internal: true + deps: ['migration:plan'] + dir: app + cmds: + - sqlc generate + sources: + - schema/*.sql + + migration:new: + dir: app + cmds: + - atlas migrate new --env dev {{ .NAME }} + requires: + vars: [NAME] + + migration:plan: + dir: app + cmds: + - atlas migrate diff --env dev {{ .NAME }} + sources: + - schema/schema.sql + - schema/migrations/*.sql + generates: + - schema/migrations/*.sql + + migration:hash: + dir: app + cmds: + - atlas migrate hash --env dev + + build: + deps: ['gen'] + dir: app + cmds: + - task: build:server + - task: build:client + - task: build:image + + build:server: + internal: true + dir: app + cmds: + - go build -ldflags='{{ .LDFLAGS | join " " }}' -o ./dist/ ./cmd/countup/... + + build:client: + internal: true + dir: app + cmds: + - go build -ldflags='{{ .LDFLAGS | join " " }}' -o ./dist/ ./cmd/countup-cli/... + + build:image: + internal: true + dir: app + cmds: + - docker build --build-arg LDFLAGS='{{ .LDFLAGS | join " " }}' -t jace-ys/countup:{{ .VERSION }} . + + compose: + ignore_error: true + deps: ['gen'] + cmds: + - docker compose --profile apps up --build {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:infra: + ignore_error: true + deps: ['gen'] + cmds: + - docker compose up {{ .CLI_ARGS }} + - defer: {task: 'compose:down'} + + compose:down: + ignore_error: true + cmds: + - docker compose down -v --remove-orphans diff --git a/app/.golangci.yaml b/app/.golangci.yaml new file mode 100644 index 0000000..4bf917c --- /dev/null +++ b/app/.golangci.yaml @@ -0,0 +1,127 @@ +--- +linters-settings: + cyclop: + package-average: 10.0 + + errcheck: + check-type-assertions: true + check-blank: true + exclude-functions: + - (github.com/jackc/pgx/v5.Tx).Rollback + + exhaustive: + check: + - switch + - map + + funlen: + lines: 100 + statements: 50 + ignore-comments: true + + gci: + sections: + - standard + - default + - localmodule + + gocognit: + min-complexity: 20 + + gocritic: + disabled-checks: + - singleCaseSwitch + + govet: + enable-all: true + disable: + - fieldalignment + - shadow + + nakedret: + max-func-lines: 0 + + perfsprint: + strconcat: false + +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - copyloopvar + - cyclop + - decorder + - dupl + - durationcheck + - errname + - errorlint + - exhaustive + - fatcontext + - funlen + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gochecksumtype + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goimports + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - intrange + - loggercheck + - makezero + - mirror + - musttag + - nakedret + - nestif + - nilerr + - nilnil + - noctx + - nolintlint + - nosprintfhostport + - perfsprint + - prealloc + - predeclared + - promlinter + - protogetter + - reassign + - spancheck + - sqlclosecheck + - tenv + - testableexamples + - testifylint + - testpackage + - tparallel + - unconvert + - usestdlibvars + - wastedassign + - whitespace + - wrapcheck + +issues: + max-same-issues: 50 + + exclude-dirs: + - ^api$ + - ^cmd/countup-cli$ + - ^schema/migrations$ + + exclude-files: + - ^internal/handler/api/echo.go$ diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..44de05a --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.23.2 AS builder + +ARG LDFLAGS +ENV LDFLAGS=${LDFLAGS} + +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . ./ +RUN CGO_ENABLED=0 go build -ldflags="${LDFLAGS}" -o dist/ ./cmd/countup/... + +FROM scratch +WORKDIR /app +COPY --from=builder /src/dist /app/bin +ENTRYPOINT ["/app/bin/countup"] \ No newline at end of file diff --git a/app/api/v1/countup.go b/app/api/v1/countup.go new file mode 100644 index 0000000..19d707f --- /dev/null +++ b/app/api/v1/countup.go @@ -0,0 +1,130 @@ +package apiv1 + +import ( + "embed" + + . "goa.design/goa/v3/dsl" +) + +var ( + //go:embed gen/http/*.json gen/http/*.yaml + OpenAPIFS embed.FS +) + +var _ = API("countup", func() { + Title("Count Up") + Description("A production-ready Go service deployed on Kubernetes") + Version("1.0.0") + Server("countup", func() { + Services("api", "web") + Host("dev-http", func() { + URI("http://localhost:8080") + }) + Host("dev-grpc", func() { + URI("grpc://localhost:8081") + }) + }) +}) + +var CounterInfo = ResultType("application/vnd.countup.counter-info`", "CounterInfo", func() { + Field(1, "count", Int32) + Field(2, "last_increment_by", String) + Field(3, "last_increment_at", String) + Field(4, "next_finalize_at", String) + Required("count", "last_increment_by", "last_increment_at", "next_finalize_at") +}) + +var _ = Service("api", func() { + Error("unauthorized") + Error("existing_increment_request", func() { + Temporary() + }) + + HTTP(func() { + Response("unauthorized", StatusUnauthorized) + Response("existing_increment_request", StatusTooManyRequests) + }) + + GRPC(func() { + Response("unauthorized", CodePermissionDenied) + Response("existing_increment_request", CodeAlreadyExists) + }) + + Method("CounterGet", func() { + Result(CounterInfo) + + HTTP(func() { + GET("/counter") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Method("CounterIncrement", func() { + Payload(func() { + Field(1, "user", String) + Required("user") + }) + + Result(CounterInfo) + + HTTP(func() { + POST("/counter/inc") + Response(StatusAccepted) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Method("Echo", func() { + Payload(func() { + Field(1, "text", String) + Required("text") + }) + + Result(func() { + Field(1, "text", String) + Required("text") + }) + + HTTP(func() { + POST("/echo") + Response(StatusOK) + }) + + GRPC(func() { + Response(CodeOK) + }) + }) + + Files("/openapi.json", "gen/http/openapi3.json") +}) + +var _ = Service("web", func() { + Method("index", func() { + Result(Bytes) + HTTP(func() { + GET("/") + Response(StatusOK, func() { + ContentType("text/html") + }) + }) + }) + + Method("another", func() { + Result(Bytes) + HTTP(func() { + GET("/another") + Response(StatusOK, func() { + ContentType("text/html") + }) + }) + }) + + Files("/static/{*path}", "static/") +}) diff --git a/app/api/v1/gen/api/client.go b/app/api/v1/gen/api/client.go new file mode 100644 index 0000000..3e8a87f --- /dev/null +++ b/app/api/v1/gen/api/client.go @@ -0,0 +1,72 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "api" service client. +type Client struct { + CounterGetEndpoint goa.Endpoint + CounterIncrementEndpoint goa.Endpoint + EchoEndpoint goa.Endpoint +} + +// NewClient initializes a "api" service client given the endpoints. +func NewClient(counterGet, counterIncrement, echo goa.Endpoint) *Client { + return &Client{ + CounterGetEndpoint: counterGet, + CounterIncrementEndpoint: counterIncrement, + EchoEndpoint: echo, + } +} + +// CounterGet calls the "CounterGet" endpoint of the "api" service. +// CounterGet may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (type *goa.ServiceError) +// - error: internal error +func (c *Client) CounterGet(ctx context.Context) (res *CounterInfo, err error) { + var ires any + ires, err = c.CounterGetEndpoint(ctx, nil) + if err != nil { + return + } + return ires.(*CounterInfo), nil +} + +// CounterIncrement calls the "CounterIncrement" endpoint of the "api" service. +// CounterIncrement may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (type *goa.ServiceError) +// - error: internal error +func (c *Client) CounterIncrement(ctx context.Context, p *CounterIncrementPayload) (res *CounterInfo, err error) { + var ires any + ires, err = c.CounterIncrementEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*CounterInfo), nil +} + +// Echo calls the "Echo" endpoint of the "api" service. +// Echo may return the following errors: +// - "unauthorized" (type *goa.ServiceError) +// - "existing_increment_request" (type *goa.ServiceError) +// - error: internal error +func (c *Client) Echo(ctx context.Context, p *EchoPayload) (res *EchoResult, err error) { + var ires any + ires, err = c.EchoEndpoint(ctx, p) + if err != nil { + return + } + return ires.(*EchoResult), nil +} diff --git a/app/api/v1/gen/api/endpoints.go b/app/api/v1/gen/api/endpoints.go new file mode 100644 index 0000000..27587bd --- /dev/null +++ b/app/api/v1/gen/api/endpoints.go @@ -0,0 +1,73 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Endpoints wraps the "api" service endpoints. +type Endpoints struct { + CounterGet goa.Endpoint + CounterIncrement goa.Endpoint + Echo goa.Endpoint +} + +// NewEndpoints wraps the methods of the "api" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + CounterGet: NewCounterGetEndpoint(s), + CounterIncrement: NewCounterIncrementEndpoint(s), + Echo: NewEchoEndpoint(s), + } +} + +// Use applies the given middleware to all the "api" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.CounterGet = m(e.CounterGet) + e.CounterIncrement = m(e.CounterIncrement) + e.Echo = m(e.Echo) +} + +// NewCounterGetEndpoint returns an endpoint function that calls the method +// "CounterGet" of service "api". +func NewCounterGetEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + res, err := s.CounterGet(ctx) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} + +// NewCounterIncrementEndpoint returns an endpoint function that calls the +// method "CounterIncrement" of service "api". +func NewCounterIncrementEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*CounterIncrementPayload) + res, err := s.CounterIncrement(ctx, p) + if err != nil { + return nil, err + } + vres := NewViewedCounterInfo(res, "default") + return vres, nil + } +} + +// NewEchoEndpoint returns an endpoint function that calls the method "Echo" of +// service "api". +func NewEchoEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + p := req.(*EchoPayload) + return s.Echo(ctx, p) + } +} diff --git a/app/api/v1/gen/api/service.go b/app/api/v1/gen/api/service.go new file mode 100644 index 0000000..52c794d --- /dev/null +++ b/app/api/v1/gen/api/service.go @@ -0,0 +1,119 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package api + +import ( + "context" + + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" +) + +// Service is the api service interface. +type Service interface { + // CounterGet implements CounterGet. + CounterGet(context.Context) (res *CounterInfo, err error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementPayload) (res *CounterInfo, err error) + // Echo implements Echo. + Echo(context.Context, *EchoPayload) (res *EchoResult, err error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "countup" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "1.0.0" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "api" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [3]string{"CounterGet", "CounterIncrement", "Echo"} + +// CounterIncrementPayload is the payload type of the api service +// CounterIncrement method. +type CounterIncrementPayload struct { + User string +} + +// CounterInfo is the result type of the api service CounterGet method. +type CounterInfo struct { + Count int32 + LastIncrementBy string + LastIncrementAt string + NextFinalizeAt string +} + +// EchoPayload is the payload type of the api service Echo method. +type EchoPayload struct { + Text string +} + +// EchoResult is the result type of the api service Echo method. +type EchoResult struct { + Text string +} + +// MakeUnauthorized builds a goa.ServiceError from an error. +func MakeUnauthorized(err error) *goa.ServiceError { + return goa.NewServiceError(err, "unauthorized", false, false, false) +} + +// MakeExistingIncrementRequest builds a goa.ServiceError from an error. +func MakeExistingIncrementRequest(err error) *goa.ServiceError { + return goa.NewServiceError(err, "existing_increment_request", false, true, false) +} + +// NewCounterInfo initializes result type CounterInfo from viewed result type +// CounterInfo. +func NewCounterInfo(vres *apiviews.CounterInfo) *CounterInfo { + return newCounterInfo(vres.Projected) +} + +// NewViewedCounterInfo initializes viewed result type CounterInfo from result +// type CounterInfo using the given view. +func NewViewedCounterInfo(res *CounterInfo, view string) *apiviews.CounterInfo { + p := newCounterInfoView(res) + return &apiviews.CounterInfo{Projected: p, View: "default"} +} + +// newCounterInfo converts projected type CounterInfo to service type +// CounterInfo. +func newCounterInfo(vres *apiviews.CounterInfoView) *CounterInfo { + res := &CounterInfo{} + if vres.Count != nil { + res.Count = *vres.Count + } + if vres.LastIncrementBy != nil { + res.LastIncrementBy = *vres.LastIncrementBy + } + if vres.LastIncrementAt != nil { + res.LastIncrementAt = *vres.LastIncrementAt + } + if vres.NextFinalizeAt != nil { + res.NextFinalizeAt = *vres.NextFinalizeAt + } + return res +} + +// newCounterInfoView projects result type CounterInfo to projected type +// CounterInfoView using the "default" view. +func newCounterInfoView(res *CounterInfo) *apiviews.CounterInfoView { + vres := &apiviews.CounterInfoView{ + Count: &res.Count, + LastIncrementBy: &res.LastIncrementBy, + LastIncrementAt: &res.LastIncrementAt, + NextFinalizeAt: &res.NextFinalizeAt, + } + return vres +} diff --git a/app/api/v1/gen/api/views/view.go b/app/api/v1/gen/api/views/view.go new file mode 100644 index 0000000..9453fa3 --- /dev/null +++ b/app/api/v1/gen/api/views/view.go @@ -0,0 +1,71 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api views +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package views + +import ( + goa "goa.design/goa/v3/pkg" +) + +// CounterInfo is the viewed result type that is projected based on a view. +type CounterInfo struct { + // Type to project + Projected *CounterInfoView + // View to render + View string +} + +// CounterInfoView is a type that runs validations on a projected type. +type CounterInfoView struct { + Count *int32 + LastIncrementBy *string + LastIncrementAt *string + NextFinalizeAt *string +} + +var ( + // CounterInfoMap is a map indexing the attribute names of CounterInfo by view + // name. + CounterInfoMap = map[string][]string{ + "default": { + "count", + "last_increment_by", + "last_increment_at", + "next_finalize_at", + }, + } +) + +// ValidateCounterInfo runs the validations defined on the viewed result type +// CounterInfo. +func ValidateCounterInfo(result *CounterInfo) (err error) { + switch result.View { + case "default", "": + err = ValidateCounterInfoView(result.Projected) + default: + err = goa.InvalidEnumValueError("view", result.View, []any{"default"}) + } + return +} + +// ValidateCounterInfoView runs the validations defined on CounterInfoView +// using the "default" view. +func ValidateCounterInfoView(result *CounterInfoView) (err error) { + if result.Count == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("count", "result")) + } + if result.LastIncrementBy == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("last_increment_by", "result")) + } + if result.LastIncrementAt == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("last_increment_at", "result")) + } + if result.NextFinalizeAt == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("next_finalize_at", "result")) + } + return +} diff --git a/app/api/v1/gen/grpc/api/client/cli.go b/app/api/v1/gen/grpc/api/client/cli.go new file mode 100644 index 0000000..0e67ec9 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/cli.go @@ -0,0 +1,55 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" +) + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementMessage string) (*api.CounterIncrementPayload, error) { + var err error + var message apipb.CounterIncrementRequest + { + if apiCounterIncrementMessage != "" { + err = json.Unmarshal([]byte(apiCounterIncrementMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Non accusantium eos culpa autem illum architecto.\"\n }'") + } + } + } + v := &api.CounterIncrementPayload{ + User: message.User, + } + + return v, nil +} + +// BuildEchoPayload builds the payload for the api Echo endpoint from CLI flags. +func BuildEchoPayload(apiEchoMessage string) (*api.EchoPayload, error) { + var err error + var message apipb.EchoRequest + { + if apiEchoMessage != "" { + err = json.Unmarshal([]byte(apiEchoMessage), &message) + if err != nil { + return nil, fmt.Errorf("invalid JSON for message, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Cupiditate tempore harum error iste ipsam natus.\"\n }'") + } + } + } + v := &api.EchoPayload{ + Text: message.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/grpc/api/client/client.go b/app/api/v1/gen/grpc/api/client/client.go new file mode 100644 index 0000000..38b0cf8 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/client.go @@ -0,0 +1,88 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + goapb "goa.design/goa/v3/grpc/pb" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" +) + +// Client lists the service endpoint gRPC clients. +type Client struct { + grpccli apipb.APIClient + opts []grpc.CallOption +} // NewClient instantiates gRPC client for all the api service servers. +func NewClient(cc *grpc.ClientConn, opts ...grpc.CallOption) *Client { + return &Client{ + grpccli: apipb.NewAPIClient(cc), + opts: opts, + } +} // CounterGet calls the "CounterGet" function in apipb.APIClient interface. +func (c *Client) CounterGet() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildCounterGetFunc(c.grpccli, c.opts...), + nil, + DecodeCounterGetResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} // CounterIncrement calls the "CounterIncrement" function in apipb.APIClient +// interface. +func (c *Client) CounterIncrement() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildCounterIncrementFunc(c.grpccli, c.opts...), + EncodeCounterIncrementRequest, + DecodeCounterIncrementResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} // Echo calls the "Echo" function in apipb.APIClient interface. +func (c *Client) Echo() goa.Endpoint { + return func(ctx context.Context, v any) (any, error) { + inv := goagrpc.NewInvoker( + BuildEchoFunc(c.grpccli, c.opts...), + EncodeEchoRequest, + DecodeEchoResponse) + res, err := inv.Invoke(ctx, v) + if err != nil { + resp := goagrpc.DecodeError(err) + switch message := resp.(type) { + case *goapb.ErrorResponse: + return nil, goagrpc.NewServiceError(message) + default: + return nil, goa.Fault(err.Error()) + } + } + return res, nil + } +} diff --git a/app/api/v1/gen/grpc/api/client/encode_decode.go b/app/api/v1/gen/grpc/api/client/encode_decode.go new file mode 100644 index 0000000..358b5b5 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/encode_decode.go @@ -0,0 +1,127 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +// BuildCounterGetFunc builds the remote method to invoke for "api" service +// "CounterGet" endpoint. +func BuildCounterGetFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.CounterGet(ctx, reqpb.(*apipb.CounterGetRequest), opts...) + } + return grpccli.CounterGet(ctx, &apipb.CounterGetRequest{}, opts...) + } +} + +// DecodeCounterGetResponse decodes responses from the api CounterGet endpoint. +func DecodeCounterGetResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + var view string + { + if vals := hdr.Get("goa-view"); len(vals) > 0 { + view = vals[0] + } + } + message, ok := v.(*apipb.CounterGetResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterGet", "*apipb.CounterGetResponse", v) + } + res := NewCounterGetResult(message) + vres := &apiviews.CounterInfo{Projected: res, View: view} + if err := apiviews.ValidateCounterInfo(vres); err != nil { + return nil, err + } + return api.NewCounterInfo(vres), nil +} // BuildCounterIncrementFunc builds the remote method to invoke for "api" +// service "CounterIncrement" endpoint. +func BuildCounterIncrementFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.CounterIncrement(ctx, reqpb.(*apipb.CounterIncrementRequest), opts...) + } + return grpccli.CounterIncrement(ctx, &apipb.CounterIncrementRequest{}, opts...) + } +} + +// EncodeCounterIncrementRequest encodes requests sent to api CounterIncrement +// endpoint. +func EncodeCounterIncrementRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.CounterIncrementPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) + } + return NewProtoCounterIncrementRequest(payload), nil +} + +// DecodeCounterIncrementResponse decodes responses from the api +// CounterIncrement endpoint. +func DecodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + var view string + { + if vals := hdr.Get("goa-view"); len(vals) > 0 { + view = vals[0] + } + } + message, ok := v.(*apipb.CounterIncrementResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apipb.CounterIncrementResponse", v) + } + res := NewCounterIncrementResult(message) + vres := &apiviews.CounterInfo{Projected: res, View: view} + if err := apiviews.ValidateCounterInfo(vres); err != nil { + return nil, err + } + return api.NewCounterInfo(vres), nil +} // BuildEchoFunc builds the remote method to invoke for "api" service "Echo" +// endpoint. +func BuildEchoFunc(grpccli apipb.APIClient, cliopts ...grpc.CallOption) goagrpc.RemoteFunc { + return func(ctx context.Context, reqpb any, opts ...grpc.CallOption) (any, error) { + for _, opt := range cliopts { + opts = append(opts, opt) + } + if reqpb != nil { + return grpccli.Echo(ctx, reqpb.(*apipb.EchoRequest), opts...) + } + return grpccli.Echo(ctx, &apipb.EchoRequest{}, opts...) + } +} + +// EncodeEchoRequest encodes requests sent to api Echo endpoint. +func EncodeEchoRequest(ctx context.Context, v any, md *metadata.MD) (any, error) { + payload, ok := v.(*api.EchoPayload) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*api.EchoPayload", v) + } + return NewProtoEchoRequest(payload), nil +} + +// DecodeEchoResponse decodes responses from the api Echo endpoint. +func DecodeEchoResponse(ctx context.Context, v any, hdr, trlr metadata.MD) (any, error) { + message, ok := v.(*apipb.EchoResponse) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*apipb.EchoResponse", v) + } + res := NewEchoResult(message) + return res, nil +} diff --git a/app/api/v1/gen/grpc/api/client/types.go b/app/api/v1/gen/grpc/api/client/types.go new file mode 100644 index 0000000..596e175 --- /dev/null +++ b/app/api/v1/gen/grpc/api/client/types.go @@ -0,0 +1,72 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" +) + +// NewProtoCounterGetRequest builds the gRPC request type from the payload of +// the "CounterGet" endpoint of the "api" service. +func NewProtoCounterGetRequest() *apipb.CounterGetRequest { + message := &apipb.CounterGetRequest{} + return message +} + +// NewCounterGetResult builds the result type of the "CounterGet" endpoint of +// the "api" service from the gRPC response type. +func NewCounterGetResult(message *apipb.CounterGetResponse) *apiviews.CounterInfoView { + result := &apiviews.CounterInfoView{ + Count: &message.Count, + LastIncrementBy: &message.LastIncrementBy, + LastIncrementAt: &message.LastIncrementAt, + NextFinalizeAt: &message.NextFinalizeAt, + } + return result +} + +// NewProtoCounterIncrementRequest builds the gRPC request type from the +// payload of the "CounterIncrement" endpoint of the "api" service. +func NewProtoCounterIncrementRequest(payload *api.CounterIncrementPayload) *apipb.CounterIncrementRequest { + message := &apipb.CounterIncrementRequest{ + User: payload.User, + } + return message +} + +// NewCounterIncrementResult builds the result type of the "CounterIncrement" +// endpoint of the "api" service from the gRPC response type. +func NewCounterIncrementResult(message *apipb.CounterIncrementResponse) *apiviews.CounterInfoView { + result := &apiviews.CounterInfoView{ + Count: &message.Count, + LastIncrementBy: &message.LastIncrementBy, + LastIncrementAt: &message.LastIncrementAt, + NextFinalizeAt: &message.NextFinalizeAt, + } + return result +} + +// NewProtoEchoRequest builds the gRPC request type from the payload of the +// "Echo" endpoint of the "api" service. +func NewProtoEchoRequest(payload *api.EchoPayload) *apipb.EchoRequest { + message := &apipb.EchoRequest{ + Text: payload.Text, + } + return message +} + +// NewEchoResult builds the result type of the "Echo" endpoint of the "api" +// service from the gRPC response type. +func NewEchoResult(message *apipb.EchoResponse) *api.EchoResult { + result := &api.EchoResult{ + Text: message.Text, + } + return result +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go new file mode 100644 index 0000000..ecc0f0a --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.pb.go @@ -0,0 +1,448 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.35.1 +// protoc v5.28.2 +// source: goagen_v1_api.proto + +package apipb + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type CounterGetRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *CounterGetRequest) Reset() { + *x = CounterGetRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterGetRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterGetRequest) ProtoMessage() {} + +func (x *CounterGetRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CounterGetRequest.ProtoReflect.Descriptor instead. +func (*CounterGetRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{0} +} + +type CounterGetResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int32 `protobuf:"zigzag32,1,opt,name=count,proto3" json:"count,omitempty"` + LastIncrementBy string `protobuf:"bytes,2,opt,name=last_increment_by,json=lastIncrementBy,proto3" json:"last_increment_by,omitempty"` + LastIncrementAt string `protobuf:"bytes,3,opt,name=last_increment_at,json=lastIncrementAt,proto3" json:"last_increment_at,omitempty"` + NextFinalizeAt string `protobuf:"bytes,4,opt,name=next_finalize_at,json=nextFinalizeAt,proto3" json:"next_finalize_at,omitempty"` +} + +func (x *CounterGetResponse) Reset() { + *x = CounterGetResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterGetResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterGetResponse) ProtoMessage() {} + +func (x *CounterGetResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CounterGetResponse.ProtoReflect.Descriptor instead. +func (*CounterGetResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{1} +} + +func (x *CounterGetResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *CounterGetResponse) GetLastIncrementBy() string { + if x != nil { + return x.LastIncrementBy + } + return "" +} + +func (x *CounterGetResponse) GetLastIncrementAt() string { + if x != nil { + return x.LastIncrementAt + } + return "" +} + +func (x *CounterGetResponse) GetNextFinalizeAt() string { + if x != nil { + return x.NextFinalizeAt + } + return "" +} + +type CounterIncrementRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` +} + +func (x *CounterIncrementRequest) Reset() { + *x = CounterIncrementRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterIncrementRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterIncrementRequest) ProtoMessage() {} + +func (x *CounterIncrementRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CounterIncrementRequest.ProtoReflect.Descriptor instead. +func (*CounterIncrementRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{2} +} + +func (x *CounterIncrementRequest) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +type CounterIncrementResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Count int32 `protobuf:"zigzag32,1,opt,name=count,proto3" json:"count,omitempty"` + LastIncrementBy string `protobuf:"bytes,2,opt,name=last_increment_by,json=lastIncrementBy,proto3" json:"last_increment_by,omitempty"` + LastIncrementAt string `protobuf:"bytes,3,opt,name=last_increment_at,json=lastIncrementAt,proto3" json:"last_increment_at,omitempty"` + NextFinalizeAt string `protobuf:"bytes,4,opt,name=next_finalize_at,json=nextFinalizeAt,proto3" json:"next_finalize_at,omitempty"` +} + +func (x *CounterIncrementResponse) Reset() { + *x = CounterIncrementResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CounterIncrementResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CounterIncrementResponse) ProtoMessage() {} + +func (x *CounterIncrementResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CounterIncrementResponse.ProtoReflect.Descriptor instead. +func (*CounterIncrementResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{3} +} + +func (x *CounterIncrementResponse) GetCount() int32 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *CounterIncrementResponse) GetLastIncrementBy() string { + if x != nil { + return x.LastIncrementBy + } + return "" +} + +func (x *CounterIncrementResponse) GetLastIncrementAt() string { + if x != nil { + return x.LastIncrementAt + } + return "" +} + +func (x *CounterIncrementResponse) GetNextFinalizeAt() string { + if x != nil { + return x.NextFinalizeAt + } + return "" +} + +type EchoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` +} + +func (x *EchoRequest) Reset() { + *x = EchoRequest{} + mi := &file_goagen_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EchoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest) ProtoMessage() {} + +func (x *EchoRequest) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{4} +} + +func (x *EchoRequest) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type EchoResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Text string `protobuf:"bytes,1,opt,name=text,proto3" json:"text,omitempty"` +} + +func (x *EchoResponse) Reset() { + *x = EchoResponse{} + mi := &file_goagen_v1_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EchoResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoResponse) ProtoMessage() {} + +func (x *EchoResponse) ProtoReflect() protoreflect.Message { + mi := &file_goagen_v1_api_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoResponse.ProtoReflect.Descriptor instead. +func (*EchoResponse) Descriptor() ([]byte, []int) { + return file_goagen_v1_api_proto_rawDescGZIP(), []int{5} +} + +func (x *EchoResponse) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +var File_goagen_v1_api_proto protoreflect.FileDescriptor + +var file_goagen_v1_api_proto_rawDesc = []byte{ + 0x0a, 0x13, 0x67, 0x6f, 0x61, 0x67, 0x65, 0x6e, 0x5f, 0x76, 0x31, 0x5f, 0x61, 0x70, 0x69, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x03, 0x61, 0x70, 0x69, 0x22, 0x13, 0x0a, 0x11, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, + 0xac, 0x01, 0x0a, 0x12, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x62, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, + 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, + 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x74, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x66, 0x69, 0x6e, + 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, + 0x6e, 0x65, 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x41, 0x74, 0x22, 0x2d, + 0x0a, 0x17, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, + 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0xb2, 0x01, + 0x0a, 0x18, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x2a, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, + 0x6e, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, + 0x74, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x79, 0x12, 0x2a, 0x0a, 0x11, + 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x69, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x49, 0x6e, 0x63, + 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6e, 0x65, 0x78, 0x74, + 0x5f, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x5f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x0e, 0x6e, 0x65, 0x78, 0x74, 0x46, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x7a, 0x65, + 0x41, 0x74, 0x22, 0x21, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x65, 0x78, 0x74, 0x22, 0x22, 0x0a, 0x0c, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x32, 0xc2, 0x01, 0x0a, 0x03, 0x41, 0x50, + 0x49, 0x12, 0x3d, 0x0a, 0x0a, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x12, + 0x16, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, + 0x75, 0x6e, 0x74, 0x65, 0x72, 0x47, 0x65, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x4f, 0x0a, 0x10, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, + 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, + 0x49, 0x6e, 0x63, 0x72, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x45, 0x63, 0x68, 0x6f, 0x12, 0x10, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x08, + 0x5a, 0x06, 0x2f, 0x61, 0x70, 0x69, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_goagen_v1_api_proto_rawDescOnce sync.Once + file_goagen_v1_api_proto_rawDescData = file_goagen_v1_api_proto_rawDesc +) + +func file_goagen_v1_api_proto_rawDescGZIP() []byte { + file_goagen_v1_api_proto_rawDescOnce.Do(func() { + file_goagen_v1_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_goagen_v1_api_proto_rawDescData) + }) + return file_goagen_v1_api_proto_rawDescData +} + +var file_goagen_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_goagen_v1_api_proto_goTypes = []any{ + (*CounterGetRequest)(nil), // 0: api.CounterGetRequest + (*CounterGetResponse)(nil), // 1: api.CounterGetResponse + (*CounterIncrementRequest)(nil), // 2: api.CounterIncrementRequest + (*CounterIncrementResponse)(nil), // 3: api.CounterIncrementResponse + (*EchoRequest)(nil), // 4: api.EchoRequest + (*EchoResponse)(nil), // 5: api.EchoResponse +} +var file_goagen_v1_api_proto_depIdxs = []int32{ + 0, // 0: api.API.CounterGet:input_type -> api.CounterGetRequest + 2, // 1: api.API.CounterIncrement:input_type -> api.CounterIncrementRequest + 4, // 2: api.API.Echo:input_type -> api.EchoRequest + 1, // 3: api.API.CounterGet:output_type -> api.CounterGetResponse + 3, // 4: api.API.CounterIncrement:output_type -> api.CounterIncrementResponse + 5, // 5: api.API.Echo:output_type -> api.EchoResponse + 3, // [3:6] is the sub-list for method output_type + 0, // [0:3] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_goagen_v1_api_proto_init() } +func file_goagen_v1_api_proto_init() { + if File_goagen_v1_api_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_goagen_v1_api_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_goagen_v1_api_proto_goTypes, + DependencyIndexes: file_goagen_v1_api_proto_depIdxs, + MessageInfos: file_goagen_v1_api_proto_msgTypes, + }.Build() + File_goagen_v1_api_proto = out.File + file_goagen_v1_api_proto_rawDesc = nil + file_goagen_v1_api_proto_goTypes = nil + file_goagen_v1_api_proto_depIdxs = nil +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto new file mode 100644 index 0000000..ec2860b --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api.proto @@ -0,0 +1,51 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +syntax = "proto3"; + +package api; + +option go_package = "/apipb"; + +// Service is the api service interface. +service API { + // CounterGet implements CounterGet. + rpc CounterGet (CounterGetRequest) returns (CounterGetResponse); + // CounterIncrement implements CounterIncrement. + rpc CounterIncrement (CounterIncrementRequest) returns (CounterIncrementResponse); + // Echo implements Echo. + rpc Echo (EchoRequest) returns (EchoResponse); +} + +message CounterGetRequest { +} + +message CounterGetResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message CounterIncrementRequest { + string user = 1; +} + +message CounterIncrementResponse { + sint32 count = 1; + string last_increment_by = 2; + string last_increment_at = 3; + string next_finalize_at = 4; +} + +message EchoRequest { + string text = 1; +} + +message EchoResponse { + string text = 1; +} diff --git a/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go new file mode 100644 index 0000000..eed11d7 --- /dev/null +++ b/app/api/v1/gen/grpc/api/pb/goagen_v1_api_grpc.pb.go @@ -0,0 +1,214 @@ +// Code generated with goa v3.19.1, DO NOT EDIT. +// +// api protocol buffer definition +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc v5.28.2 +// source: goagen_v1_api.proto + +package apipb + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + API_CounterGet_FullMethodName = "/api.API/CounterGet" + API_CounterIncrement_FullMethodName = "/api.API/CounterIncrement" + API_Echo_FullMethodName = "/api.API/Echo" +) + +// APIClient is the client API for API service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// Service is the api service interface. +type APIClient interface { + // CounterGet implements CounterGet. + CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(ctx context.Context, in *CounterIncrementRequest, opts ...grpc.CallOption) (*CounterIncrementResponse, error) + // Echo implements Echo. + Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) +} + +type aPIClient struct { + cc grpc.ClientConnInterface +} + +func NewAPIClient(cc grpc.ClientConnInterface) APIClient { + return &aPIClient{cc} +} + +func (c *aPIClient) CounterGet(ctx context.Context, in *CounterGetRequest, opts ...grpc.CallOption) (*CounterGetResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CounterGetResponse) + err := c.cc.Invoke(ctx, API_CounterGet_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) CounterIncrement(ctx context.Context, in *CounterIncrementRequest, opts ...grpc.CallOption) (*CounterIncrementResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CounterIncrementResponse) + err := c.cc.Invoke(ctx, API_CounterIncrement_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *aPIClient) Echo(ctx context.Context, in *EchoRequest, opts ...grpc.CallOption) (*EchoResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EchoResponse) + err := c.cc.Invoke(ctx, API_Echo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// APIServer is the server API for API service. +// All implementations must embed UnimplementedAPIServer +// for forward compatibility. +// +// Service is the api service interface. +type APIServer interface { + // CounterGet implements CounterGet. + CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) + // CounterIncrement implements CounterIncrement. + CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, error) + // Echo implements Echo. + Echo(context.Context, *EchoRequest) (*EchoResponse, error) + mustEmbedUnimplementedAPIServer() +} + +// UnimplementedAPIServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAPIServer struct{} + +func (UnimplementedAPIServer) CounterGet(context.Context, *CounterGetRequest) (*CounterGetResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CounterGet not implemented") +} +func (UnimplementedAPIServer) CounterIncrement(context.Context, *CounterIncrementRequest) (*CounterIncrementResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CounterIncrement not implemented") +} +func (UnimplementedAPIServer) Echo(context.Context, *EchoRequest) (*EchoResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method Echo not implemented") +} +func (UnimplementedAPIServer) mustEmbedUnimplementedAPIServer() {} +func (UnimplementedAPIServer) testEmbeddedByValue() {} + +// UnsafeAPIServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to APIServer will +// result in compilation errors. +type UnsafeAPIServer interface { + mustEmbedUnimplementedAPIServer() +} + +func RegisterAPIServer(s grpc.ServiceRegistrar, srv APIServer) { + // If the following call pancis, it indicates UnimplementedAPIServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&API_ServiceDesc, srv) +} + +func _API_CounterGet_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CounterGetRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CounterGet(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_CounterGet_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CounterGet(ctx, req.(*CounterGetRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_CounterIncrement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CounterIncrementRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).CounterIncrement(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_CounterIncrement_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).CounterIncrement(ctx, req.(*CounterIncrementRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _API_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EchoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(APIServer).Echo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: API_Echo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(APIServer).Echo(ctx, req.(*EchoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// API_ServiceDesc is the grpc.ServiceDesc for API service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var API_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "api.API", + HandlerType: (*APIServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CounterGet", + Handler: _API_CounterGet_Handler, + }, + { + MethodName: "CounterIncrement", + Handler: _API_CounterIncrement_Handler, + }, + { + MethodName: "Echo", + Handler: _API_Echo_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "goagen_v1_api.proto", +} diff --git a/app/api/v1/gen/grpc/api/server/encode_decode.go b/app/api/v1/gen/grpc/api/server/encode_decode.go new file mode 100644 index 0000000..2c40680 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/encode_decode.go @@ -0,0 +1,91 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + "google.golang.org/grpc/metadata" +) + +// EncodeCounterGetResponse encodes responses from the "api" service +// "CounterGet" endpoint. +func EncodeCounterGetResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + vres, ok := v.(*apiviews.CounterInfo) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterGet", "*apiviews.CounterInfo", v) + } + result := vres.Projected + (*hdr).Append("goa-view", vres.View) + resp := NewProtoCounterGetResponse(result) + return resp, nil +} + +// EncodeCounterIncrementResponse encodes responses from the "api" service +// "CounterIncrement" endpoint. +func EncodeCounterIncrementResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + vres, ok := v.(*apiviews.CounterInfo) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apiviews.CounterInfo", v) + } + result := vres.Projected + (*hdr).Append("goa-view", vres.View) + resp := NewProtoCounterIncrementResponse(result) + return resp, nil +} + +// DecodeCounterIncrementRequest decodes requests sent to "api" service +// "CounterIncrement" endpoint. +func DecodeCounterIncrementRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.CounterIncrementRequest + ok bool + ) + { + if message, ok = v.(*apipb.CounterIncrementRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "CounterIncrement", "*apipb.CounterIncrementRequest", v) + } + } + var payload *api.CounterIncrementPayload + { + payload = NewCounterIncrementPayload(message) + } + return payload, nil +} + +// EncodeEchoResponse encodes responses from the "api" service "Echo" endpoint. +func EncodeEchoResponse(ctx context.Context, v any, hdr, trlr *metadata.MD) (any, error) { + result, ok := v.(*api.EchoResult) + if !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*api.EchoResult", v) + } + resp := NewProtoEchoResponse(result) + return resp, nil +} + +// DecodeEchoRequest decodes requests sent to "api" service "Echo" endpoint. +func DecodeEchoRequest(ctx context.Context, v any, md metadata.MD) (any, error) { + var ( + message *apipb.EchoRequest + ok bool + ) + { + if message, ok = v.(*apipb.EchoRequest); !ok { + return nil, goagrpc.ErrInvalidType("api", "Echo", "*apipb.EchoRequest", v) + } + } + var payload *api.EchoPayload + { + payload = NewEchoPayload(message) + } + return payload, nil +} diff --git a/app/api/v1/gen/grpc/api/server/server.go b/app/api/v1/gen/grpc/api/server/server.go new file mode 100644 index 0000000..dcbceb8 --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/server.go @@ -0,0 +1,124 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + goagrpc "goa.design/goa/v3/grpc" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc/codes" +) + +// Server implements the apipb.APIServer interface. +type Server struct { + CounterGetH goagrpc.UnaryHandler + CounterIncrementH goagrpc.UnaryHandler + EchoH goagrpc.UnaryHandler + apipb.UnimplementedAPIServer +} + +// New instantiates the server struct with the api service endpoints. +func New(e *api.Endpoints, uh goagrpc.UnaryHandler) *Server { + return &Server{ + CounterGetH: NewCounterGetHandler(e.CounterGet, uh), + CounterIncrementH: NewCounterIncrementHandler(e.CounterIncrement, uh), + EchoH: NewEchoHandler(e.Echo, uh), + } +} + +// NewCounterGetHandler creates a gRPC handler which serves the "api" service +// "CounterGet" endpoint. +func NewCounterGetHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, nil, EncodeCounterGetResponse) + } + return h +} + +// CounterGet implements the "CounterGet" method in apipb.APIServer interface. +func (s *Server) CounterGet(ctx context.Context, message *apipb.CounterGetRequest) (*apipb.CounterGetResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "CounterGet") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.CounterGetH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterGetResponse), nil +} + +// NewCounterIncrementHandler creates a gRPC handler which serves the "api" +// service "CounterIncrement" endpoint. +func NewCounterIncrementHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeCounterIncrementRequest, EncodeCounterIncrementResponse) + } + return h +} + +// CounterIncrement implements the "CounterIncrement" method in apipb.APIServer +// interface. +func (s *Server) CounterIncrement(ctx context.Context, message *apipb.CounterIncrementRequest) (*apipb.CounterIncrementResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "CounterIncrement") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.CounterIncrementH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.CounterIncrementResponse), nil +} + +// NewEchoHandler creates a gRPC handler which serves the "api" service "Echo" +// endpoint. +func NewEchoHandler(endpoint goa.Endpoint, h goagrpc.UnaryHandler) goagrpc.UnaryHandler { + if h == nil { + h = goagrpc.NewUnaryHandler(endpoint, DecodeEchoRequest, EncodeEchoResponse) + } + return h +} + +// Echo implements the "Echo" method in apipb.APIServer interface. +func (s *Server) Echo(ctx context.Context, message *apipb.EchoRequest) (*apipb.EchoResponse, error) { + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + resp, err := s.EchoH.Handle(ctx, message) + if err != nil { + var en goa.GoaErrorNamer + if errors.As(err, &en) { + switch en.GoaErrorName() { + case "unauthorized": + return nil, goagrpc.NewStatusError(codes.PermissionDenied, err, goagrpc.NewErrorResponse(err)) + case "existing_increment_request": + return nil, goagrpc.NewStatusError(codes.AlreadyExists, err, goagrpc.NewErrorResponse(err)) + } + } + return nil, goagrpc.EncodeError(err) + } + return resp.(*apipb.EchoResponse), nil +} diff --git a/app/api/v1/gen/grpc/api/server/types.go b/app/api/v1/gen/grpc/api/server/types.go new file mode 100644 index 0000000..9b23b3a --- /dev/null +++ b/app/api/v1/gen/grpc/api/server/types.go @@ -0,0 +1,65 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api gRPC server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" +) + +// NewProtoCounterGetResponse builds the gRPC response type from the result of +// the "CounterGet" endpoint of the "api" service. +func NewProtoCounterGetResponse(result *apiviews.CounterInfoView) *apipb.CounterGetResponse { + message := &apipb.CounterGetResponse{ + Count: *result.Count, + LastIncrementBy: *result.LastIncrementBy, + LastIncrementAt: *result.LastIncrementAt, + NextFinalizeAt: *result.NextFinalizeAt, + } + return message +} + +// NewCounterIncrementPayload builds the payload of the "CounterIncrement" +// endpoint of the "api" service from the gRPC request type. +func NewCounterIncrementPayload(message *apipb.CounterIncrementRequest) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{ + User: message.User, + } + return v +} + +// NewProtoCounterIncrementResponse builds the gRPC response type from the +// result of the "CounterIncrement" endpoint of the "api" service. +func NewProtoCounterIncrementResponse(result *apiviews.CounterInfoView) *apipb.CounterIncrementResponse { + message := &apipb.CounterIncrementResponse{ + Count: *result.Count, + LastIncrementBy: *result.LastIncrementBy, + LastIncrementAt: *result.LastIncrementAt, + NextFinalizeAt: *result.NextFinalizeAt, + } + return message +} + +// NewEchoPayload builds the payload of the "Echo" endpoint of the "api" +// service from the gRPC request type. +func NewEchoPayload(message *apipb.EchoRequest) *api.EchoPayload { + v := &api.EchoPayload{ + Text: message.Text, + } + return v +} + +// NewProtoEchoResponse builds the gRPC response type from the result of the +// "Echo" endpoint of the "api" service. +func NewProtoEchoResponse(result *api.EchoResult) *apipb.EchoResponse { + message := &apipb.EchoResponse{ + Text: result.Text, + } + return message +} diff --git a/app/api/v1/gen/grpc/cli/countup/cli.go b/app/api/v1/gen/grpc/cli/countup/cli.go new file mode 100644 index 0000000..232bfc0 --- /dev/null +++ b/app/api/v1/gen/grpc/cli/countup/cli.go @@ -0,0 +1,186 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// countup gRPC client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package cli + +import ( + "flag" + "fmt" + "os" + + apic "github.com/jace-ys/countup/api/v1/gen/grpc/api/client" + goa "goa.design/goa/v3/pkg" + grpc "google.golang.org/grpc" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `api (counter-get|counter-increment|echo) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api counter-get` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint(cc *grpc.ClientConn, opts ...grpc.CallOption) (goa.Endpoint, any, error) { + var ( + apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementMessageFlag = apiCounterIncrementFlags.String("message", "", "") + + apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + apiEchoMessageFlag = apiEchoFlags.String("message", "", "") + ) + apiFlags.Usage = apiUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + apiEchoFlags.Usage = apiEchoUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "api": + svcf = apiFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "api": + switch epn { + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + case "echo": + epf = apiEchoFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "api": + c := apic.NewClient(cc, opts...) + switch epn { + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementMessageFlag) + case "echo": + endpoint = c.Echo() + data, err = apic.BuildEchoPayload(*apiEchoMessageFlag) + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} // apiUsage displays the usage of the api command and its subcommands. +func apiUsage() { + fmt.Fprintf(os.Stderr, `Service is the api service interface. +Usage: + %[1]s [globalflags] api COMMAND [flags] + +COMMAND: + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + echo: Echo implements Echo. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiCounterGetUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + +CounterGet implements CounterGet. + +Example: + %[1]s api counter-get +`, os.Args[0]) +} + +func apiCounterIncrementUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -message JSON + +CounterIncrement implements CounterIncrement. + -message JSON: + +Example: + %[1]s api counter-increment --message '{ + "user": "Non accusantium eos culpa autem illum architecto." + }' +`, os.Args[0]) +} + +func apiEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api echo -message JSON + +Echo implements Echo. + -message JSON: + +Example: + %[1]s api echo --message '{ + "text": "Cupiditate tempore harum error iste ipsam natus." + }' +`, os.Args[0]) +} diff --git a/app/api/v1/gen/http/api/client/cli.go b/app/api/v1/gen/http/api/client/cli.go new file mode 100644 index 0000000..58489ba --- /dev/null +++ b/app/api/v1/gen/http/api/client/cli.go @@ -0,0 +1,50 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "encoding/json" + "fmt" + + api "github.com/jace-ys/countup/api/v1/gen/api" +) + +// BuildCounterIncrementPayload builds the payload for the api CounterIncrement +// endpoint from CLI flags. +func BuildCounterIncrementPayload(apiCounterIncrementBody string) (*api.CounterIncrementPayload, error) { + var err error + var body CounterIncrementRequestBody + { + err = json.Unmarshal([]byte(apiCounterIncrementBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"user\": \"Nihil doloribus et sed sequi consequatur.\"\n }'") + } + } + v := &api.CounterIncrementPayload{ + User: body.User, + } + + return v, nil +} + +// BuildEchoPayload builds the payload for the api Echo endpoint from CLI flags. +func BuildEchoPayload(apiEchoBody string) (*api.EchoPayload, error) { + var err error + var body EchoRequestBody + { + err = json.Unmarshal([]byte(apiEchoBody), &body) + if err != nil { + return nil, fmt.Errorf("invalid JSON for body, \nerror: %s, \nexample of valid JSON:\n%s", err, "'{\n \"text\": \"Vel omnis quo sit.\"\n }'") + } + } + v := &api.EchoPayload{ + Text: body.Text, + } + + return v, nil +} diff --git a/app/api/v1/gen/http/api/client/client.go b/app/api/v1/gen/http/api/client/client.go new file mode 100644 index 0000000..6de9935 --- /dev/null +++ b/app/api/v1/gen/http/api/client/client.go @@ -0,0 +1,127 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api client HTTP transport +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the api service endpoint HTTP clients. +type Client struct { + // CounterGet Doer is the HTTP client used to make requests to the CounterGet + // endpoint. + CounterGetDoer goahttp.Doer + + // CounterIncrement Doer is the HTTP client used to make requests to the + // CounterIncrement endpoint. + CounterIncrementDoer goahttp.Doer + + // Echo Doer is the HTTP client used to make requests to the Echo endpoint. + EchoDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the api service servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + CounterGetDoer: doer, + CounterIncrementDoer: doer, + EchoDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// CounterGet returns an endpoint that makes HTTP requests to the api service +// CounterGet server. +func (c *Client) CounterGet() goa.Endpoint { + var ( + decodeResponse = DecodeCounterGetResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildCounterGetRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.CounterGetDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "CounterGet", err) + } + return decodeResponse(resp) + } +} + +// CounterIncrement returns an endpoint that makes HTTP requests to the api +// service CounterIncrement server. +func (c *Client) CounterIncrement() goa.Endpoint { + var ( + encodeRequest = EncodeCounterIncrementRequest(c.encoder) + decodeResponse = DecodeCounterIncrementResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildCounterIncrementRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.CounterIncrementDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "CounterIncrement", err) + } + return decodeResponse(resp) + } +} + +// Echo returns an endpoint that makes HTTP requests to the api service Echo +// server. +func (c *Client) Echo() goa.Endpoint { + var ( + encodeRequest = EncodeEchoRequest(c.encoder) + decodeResponse = DecodeEchoResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildEchoRequest(ctx, v) + if err != nil { + return nil, err + } + err = encodeRequest(req, v) + if err != nil { + return nil, err + } + resp, err := c.EchoDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("api", "Echo", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/api/client/encode_decode.go b/app/api/v1/gen/http/api/client/encode_decode.go new file mode 100644 index 0000000..dd511c9 --- /dev/null +++ b/app/api/v1/gen/http/api/client/encode_decode.go @@ -0,0 +1,317 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goahttp "goa.design/goa/v3/http" +) + +// BuildCounterGetRequest instantiates a HTTP request object with method and +// path set to call the "api" service "CounterGet" endpoint +func (c *Client) BuildCounterGetRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: CounterGetAPIPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "CounterGet", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeCounterGetResponse returns a decoder for responses returned by the api +// CounterGet endpoint. restoreBody controls whether the response body should +// be restored after having been read. +// DecodeCounterGetResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - error: internal error +func DecodeCounterGetResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body CounterGetResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + p := NewCounterGetCounterInfoOK(&body) + view := "default" + vres := &apiviews.CounterInfo{Projected: p, View: view} + if err = apiviews.ValidateCounterInfo(vres); err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + res := api.NewCounterInfo(vres) + return res, nil + case http.StatusUnauthorized: + var ( + body CounterGetUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetUnauthorized(&body) + case http.StatusTooManyRequests: + var ( + body CounterGetExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterGet", err) + } + err = ValidateCounterGetExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterGet", err) + } + return nil, NewCounterGetExistingIncrementRequest(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterGet", resp.StatusCode, string(body)) + } + } +} + +// BuildCounterIncrementRequest instantiates a HTTP request object with method +// and path set to call the "api" service "CounterIncrement" endpoint +func (c *Client) BuildCounterIncrementRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: CounterIncrementAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "CounterIncrement", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeCounterIncrementRequest returns an encoder for requests sent to the +// api CounterIncrement server. +func EncodeCounterIncrementRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.CounterIncrementPayload) + if !ok { + return goahttp.ErrInvalidType("api", "CounterIncrement", "*api.CounterIncrementPayload", v) + } + body := NewCounterIncrementRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "CounterIncrement", err) + } + return nil + } +} + +// DecodeCounterIncrementResponse returns a decoder for responses returned by +// the api CounterIncrement endpoint. restoreBody controls whether the response +// body should be restored after having been read. +// DecodeCounterIncrementResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - error: internal error +func DecodeCounterIncrementResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusAccepted: + var ( + body CounterIncrementResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + p := NewCounterIncrementCounterInfoAccepted(&body) + view := "default" + vres := &apiviews.CounterInfo{Projected: p, View: view} + if err = apiviews.ValidateCounterInfo(vres); err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + res := api.NewCounterInfo(vres) + return res, nil + case http.StatusUnauthorized: + var ( + body CounterIncrementUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementUnauthorized(&body) + case http.StatusTooManyRequests: + var ( + body CounterIncrementExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "CounterIncrement", err) + } + err = ValidateCounterIncrementExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "CounterIncrement", err) + } + return nil, NewCounterIncrementExistingIncrementRequest(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "CounterIncrement", resp.StatusCode, string(body)) + } + } +} + +// BuildEchoRequest instantiates a HTTP request object with method and path set +// to call the "api" service "Echo" endpoint +func (c *Client) BuildEchoRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: EchoAPIPath()} + req, err := http.NewRequest("POST", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("api", "Echo", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// EncodeEchoRequest returns an encoder for requests sent to the api Echo +// server. +func EncodeEchoRequest(encoder func(*http.Request) goahttp.Encoder) func(*http.Request, any) error { + return func(req *http.Request, v any) error { + p, ok := v.(*api.EchoPayload) + if !ok { + return goahttp.ErrInvalidType("api", "Echo", "*api.EchoPayload", v) + } + body := NewEchoRequestBody(p) + if err := encoder(req).Encode(&body); err != nil { + return goahttp.ErrEncodingError("api", "Echo", err) + } + return nil + } +} + +// DecodeEchoResponse returns a decoder for responses returned by the api Echo +// endpoint. restoreBody controls whether the response body should be restored +// after having been read. +// DecodeEchoResponse may return the following errors: +// - "unauthorized" (type *goa.ServiceError): http.StatusUnauthorized +// - "existing_increment_request" (type *goa.ServiceError): http.StatusTooManyRequests +// - error: internal error +func DecodeEchoResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body EchoResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "Echo", err) + } + err = ValidateEchoResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + res := NewEchoResultOK(&body) + return res, nil + case http.StatusUnauthorized: + var ( + body EchoUnauthorizedResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "Echo", err) + } + err = ValidateEchoUnauthorizedResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + return nil, NewEchoUnauthorized(&body) + case http.StatusTooManyRequests: + var ( + body EchoExistingIncrementRequestResponseBody + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("api", "Echo", err) + } + err = ValidateEchoExistingIncrementRequestResponseBody(&body) + if err != nil { + return nil, goahttp.ErrValidationError("api", "Echo", err) + } + return nil, NewEchoExistingIncrementRequest(&body) + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("api", "Echo", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/api/client/paths.go b/app/api/v1/gen/http/api/client/paths.go new file mode 100644 index 0000000..0f1045e --- /dev/null +++ b/app/api/v1/gen/http/api/client/paths.go @@ -0,0 +1,23 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the api service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/counter/inc" +} + +// EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. +func EchoAPIPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/api/client/types.go b/app/api/v1/gen/http/api/client/types.go new file mode 100644 index 0000000..a92300a --- /dev/null +++ b/app/api/v1/gen/http/api/client/types.go @@ -0,0 +1,457 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" +) + +// CounterIncrementRequestBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP request body. +type CounterIncrementRequestBody struct { + User string `form:"user" json:"user" xml:"user"` +} + +// EchoRequestBody is the type of the "api" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// CounterGetResponseBody is the type of the "api" service "CounterGet" +// endpoint HTTP response body. +type CounterGetResponseBody struct { + Count *int32 `form:"count,omitempty" json:"count,omitempty" xml:"count,omitempty"` + LastIncrementBy *string `form:"last_increment_by,omitempty" json:"last_increment_by,omitempty" xml:"last_increment_by,omitempty"` + LastIncrementAt *string `form:"last_increment_at,omitempty" json:"last_increment_at,omitempty" xml:"last_increment_at,omitempty"` + NextFinalizeAt *string `form:"next_finalize_at,omitempty" json:"next_finalize_at,omitempty" xml:"next_finalize_at,omitempty"` +} + +// CounterIncrementResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body. +type CounterIncrementResponseBody struct { + Count *int32 `form:"count,omitempty" json:"count,omitempty" xml:"count,omitempty"` + LastIncrementBy *string `form:"last_increment_by,omitempty" json:"last_increment_by,omitempty" xml:"last_increment_by,omitempty"` + LastIncrementAt *string `form:"last_increment_at,omitempty" json:"last_increment_at,omitempty" xml:"last_increment_at,omitempty"` + NextFinalizeAt *string `form:"next_finalize_at,omitempty" json:"next_finalize_at,omitempty" xml:"next_finalize_at,omitempty"` +} + +// EchoResponseBody is the type of the "api" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// CounterGetUnauthorizedResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "unauthorized" error. +type CounterGetUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterGetExistingIncrementRequestResponseBody is the type of the "api" +// service "CounterGet" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterGetExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterIncrementUnauthorizedResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "unauthorized" error. +type CounterIncrementUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// CounterIncrementExistingIncrementRequestResponseBody is the type of the +// "api" service "CounterIncrement" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterIncrementExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// EchoUnauthorizedResponseBody is the type of the "api" service "Echo" +// endpoint HTTP response body for the "unauthorized" error. +type EchoUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// EchoExistingIncrementRequestResponseBody is the type of the "api" service +// "Echo" endpoint HTTP response body for the "existing_increment_request" +// error. +type EchoExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name *string `form:"name,omitempty" json:"name,omitempty" xml:"name,omitempty"` + // ID is a unique identifier for this particular occurrence of the problem. + ID *string `form:"id,omitempty" json:"id,omitempty" xml:"id,omitempty"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message *string `form:"message,omitempty" json:"message,omitempty" xml:"message,omitempty"` + // Is the error temporary? + Temporary *bool `form:"temporary,omitempty" json:"temporary,omitempty" xml:"temporary,omitempty"` + // Is the error a timeout? + Timeout *bool `form:"timeout,omitempty" json:"timeout,omitempty" xml:"timeout,omitempty"` + // Is the error a server-side fault? + Fault *bool `form:"fault,omitempty" json:"fault,omitempty" xml:"fault,omitempty"` +} + +// NewCounterIncrementRequestBody builds the HTTP request body from the payload +// of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementRequestBody(p *api.CounterIncrementPayload) *CounterIncrementRequestBody { + body := &CounterIncrementRequestBody{ + User: p.User, + } + return body +} + +// NewEchoRequestBody builds the HTTP request body from the payload of the +// "Echo" endpoint of the "api" service. +func NewEchoRequestBody(p *api.EchoPayload) *EchoRequestBody { + body := &EchoRequestBody{ + Text: p.Text, + } + return body +} + +// NewCounterGetCounterInfoOK builds a "api" service "CounterGet" endpoint +// result from a HTTP "OK" response. +func NewCounterGetCounterInfoOK(body *CounterGetResponseBody) *apiviews.CounterInfoView { + v := &apiviews.CounterInfoView{ + Count: body.Count, + LastIncrementBy: body.LastIncrementBy, + LastIncrementAt: body.LastIncrementAt, + NextFinalizeAt: body.NextFinalizeAt, + } + + return v +} + +// NewCounterGetUnauthorized builds a api service CounterGet endpoint +// unauthorized error. +func NewCounterGetUnauthorized(body *CounterGetUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterGetExistingIncrementRequest builds a api service CounterGet +// endpoint existing_increment_request error. +func NewCounterGetExistingIncrementRequest(body *CounterGetExistingIncrementRequestResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterIncrementCounterInfoAccepted builds a "api" service +// "CounterIncrement" endpoint result from a HTTP "Accepted" response. +func NewCounterIncrementCounterInfoAccepted(body *CounterIncrementResponseBody) *apiviews.CounterInfoView { + v := &apiviews.CounterInfoView{ + Count: body.Count, + LastIncrementBy: body.LastIncrementBy, + LastIncrementAt: body.LastIncrementAt, + NextFinalizeAt: body.NextFinalizeAt, + } + + return v +} + +// NewCounterIncrementUnauthorized builds a api service CounterIncrement +// endpoint unauthorized error. +func NewCounterIncrementUnauthorized(body *CounterIncrementUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewCounterIncrementExistingIncrementRequest builds a api service +// CounterIncrement endpoint existing_increment_request error. +func NewCounterIncrementExistingIncrementRequest(body *CounterIncrementExistingIncrementRequestResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewEchoResultOK builds a "api" service "Echo" endpoint result from a HTTP +// "OK" response. +func NewEchoResultOK(body *EchoResponseBody) *api.EchoResult { + v := &api.EchoResult{ + Text: *body.Text, + } + + return v +} + +// NewEchoUnauthorized builds a api service Echo endpoint unauthorized error. +func NewEchoUnauthorized(body *EchoUnauthorizedResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// NewEchoExistingIncrementRequest builds a api service Echo endpoint +// existing_increment_request error. +func NewEchoExistingIncrementRequest(body *EchoExistingIncrementRequestResponseBody) *goa.ServiceError { + v := &goa.ServiceError{ + Name: *body.Name, + ID: *body.ID, + Message: *body.Message, + Temporary: *body.Temporary, + Timeout: *body.Timeout, + Fault: *body.Fault, + } + + return v +} + +// ValidateEchoResponseBody runs the validations defined on EchoResponseBody +func ValidateEchoResponseBody(body *EchoResponseBody) (err error) { + if body.Text == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("text", "body")) + } + return +} + +// ValidateCounterGetUnauthorizedResponseBody runs the validations defined on +// CounterGet_unauthorized_Response_Body +func ValidateCounterGetUnauthorizedResponseBody(body *CounterGetUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterGetExistingIncrementRequestResponseBody runs the validations +// defined on CounterGet_existing_increment_request_Response_Body +func ValidateCounterGetExistingIncrementRequestResponseBody(body *CounterGetExistingIncrementRequestResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterIncrementUnauthorizedResponseBody runs the validations +// defined on CounterIncrement_unauthorized_Response_Body +func ValidateCounterIncrementUnauthorizedResponseBody(body *CounterIncrementUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateCounterIncrementExistingIncrementRequestResponseBody runs the +// validations defined on +// CounterIncrement_existing_increment_request_Response_Body +func ValidateCounterIncrementExistingIncrementRequestResponseBody(body *CounterIncrementExistingIncrementRequestResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateEchoUnauthorizedResponseBody runs the validations defined on +// Echo_unauthorized_Response_Body +func ValidateEchoUnauthorizedResponseBody(body *EchoUnauthorizedResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} + +// ValidateEchoExistingIncrementRequestResponseBody runs the validations +// defined on Echo_existing_increment_request_Response_Body +func ValidateEchoExistingIncrementRequestResponseBody(body *EchoExistingIncrementRequestResponseBody) (err error) { + if body.Name == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("name", "body")) + } + if body.ID == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("id", "body")) + } + if body.Message == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("message", "body")) + } + if body.Temporary == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("temporary", "body")) + } + if body.Timeout == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("timeout", "body")) + } + if body.Fault == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("fault", "body")) + } + return +} diff --git a/app/api/v1/gen/http/api/server/encode_decode.go b/app/api/v1/gen/http/api/server/encode_decode.go new file mode 100644 index 0000000..6e7545a --- /dev/null +++ b/app/api/v1/gen/http/api/server/encode_decode.go @@ -0,0 +1,240 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "errors" + "io" + "net/http" + + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// EncodeCounterGetResponse returns an encoder for responses returned by the +// api CounterGet endpoint. +func EncodeCounterGetResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res := v.(*apiviews.CounterInfo) + enc := encoder(ctx, w) + body := NewCounterGetResponseBody(res.Projected) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeCounterGetError returns an encoder for errors returned by the +// CounterGet api endpoint. +func EncodeCounterGetError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterGetUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "existing_increment_request": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterGetExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeCounterIncrementResponse returns an encoder for responses returned by +// the api CounterIncrement endpoint. +func EncodeCounterIncrementResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res := v.(*apiviews.CounterInfo) + enc := encoder(ctx, w) + body := NewCounterIncrementResponseBody(res.Projected) + w.WriteHeader(http.StatusAccepted) + return enc.Encode(body) + } +} + +// DecodeCounterIncrementRequest returns a decoder for requests sent to the api +// CounterIncrement endpoint. +func DecodeCounterIncrementRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body CounterIncrementRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateCounterIncrementRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewCounterIncrementPayload(&body) + + return payload, nil + } +} + +// EncodeCounterIncrementError returns an encoder for errors returned by the +// CounterIncrement api endpoint. +func EncodeCounterIncrementError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "existing_increment_request": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewCounterIncrementExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} + +// EncodeEchoResponse returns an encoder for responses returned by the api Echo +// endpoint. +func EncodeEchoResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.(*api.EchoResult) + enc := encoder(ctx, w) + body := NewEchoResponseBody(res) + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// DecodeEchoRequest returns a decoder for requests sent to the api Echo +// endpoint. +func DecodeEchoRequest(mux goahttp.Muxer, decoder func(*http.Request) goahttp.Decoder) func(*http.Request) (any, error) { + return func(r *http.Request) (any, error) { + var ( + body EchoRequestBody + err error + ) + err = decoder(r).Decode(&body) + if err != nil { + if err == io.EOF { + return nil, goa.MissingPayloadError() + } + var gerr *goa.ServiceError + if errors.As(err, &gerr) { + return nil, gerr + } + return nil, goa.DecodePayloadError(err.Error()) + } + err = ValidateEchoRequestBody(&body) + if err != nil { + return nil, err + } + payload := NewEchoPayload(&body) + + return payload, nil + } +} + +// EncodeEchoError returns an encoder for errors returned by the Echo api +// endpoint. +func EncodeEchoError(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, formatter func(ctx context.Context, err error) goahttp.Statuser) func(context.Context, http.ResponseWriter, error) error { + encodeError := goahttp.ErrorEncoder(encoder, formatter) + return func(ctx context.Context, w http.ResponseWriter, v error) error { + var en goa.GoaErrorNamer + if !errors.As(v, &en) { + return encodeError(ctx, w, v) + } + switch en.GoaErrorName() { + case "unauthorized": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewEchoUnauthorizedResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusUnauthorized) + return enc.Encode(body) + case "existing_increment_request": + var res *goa.ServiceError + errors.As(v, &res) + enc := encoder(ctx, w) + var body any + if formatter != nil { + body = formatter(ctx, res) + } else { + body = NewEchoExistingIncrementRequestResponseBody(res) + } + w.Header().Set("goa-error", res.GoaErrorName()) + w.WriteHeader(http.StatusTooManyRequests) + return enc.Encode(body) + default: + return encodeError(ctx, w, v) + } + } +} diff --git a/app/api/v1/gen/http/api/server/paths.go b/app/api/v1/gen/http/api/server/paths.go new file mode 100644 index 0000000..e1aae0e --- /dev/null +++ b/app/api/v1/gen/http/api/server/paths.go @@ -0,0 +1,23 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the api service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// CounterGetAPIPath returns the URL path to the api service CounterGet HTTP endpoint. +func CounterGetAPIPath() string { + return "/counter" +} + +// CounterIncrementAPIPath returns the URL path to the api service CounterIncrement HTTP endpoint. +func CounterIncrementAPIPath() string { + return "/counter/inc" +} + +// EchoAPIPath returns the URL path to the api service Echo HTTP endpoint. +func EchoAPIPath() string { + return "/echo" +} diff --git a/app/api/v1/gen/http/api/server/server.go b/app/api/v1/gen/http/api/server/server.go new file mode 100644 index 0000000..bb690b0 --- /dev/null +++ b/app/api/v1/gen/http/api/server/server.go @@ -0,0 +1,272 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + api "github.com/jace-ys/countup/api/v1/gen/api" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the api service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + CounterGet http.Handler + CounterIncrement http.Handler + Echo http.Handler + GenHTTPOpenapi3JSON http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the api service endpoints using the +// provided encoder and decoder. The handlers are mounted on the given mux +// using the HTTP verb and path defined in the design. errhandler is called +// whenever a response fails to be encoded. formatter is used to format errors +// returned by the service methods prior to encoding. Both errhandler and +// formatter are optional and can be nil. +func New( + e *api.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemGenHTTPOpenapi3JSON http.FileSystem, +) *Server { + if fileSystemGenHTTPOpenapi3JSON == nil { + fileSystemGenHTTPOpenapi3JSON = http.Dir(".") + } + fileSystemGenHTTPOpenapi3JSON = appendPrefix(fileSystemGenHTTPOpenapi3JSON, "/gen/http") + return &Server{ + Mounts: []*MountPoint{ + {"CounterGet", "GET", "/counter"}, + {"CounterIncrement", "POST", "/counter/inc"}, + {"Echo", "POST", "/echo"}, + {"Serve gen/http/openapi3.json", "GET", "/openapi.json"}, + }, + CounterGet: NewCounterGetHandler(e.CounterGet, mux, decoder, encoder, errhandler, formatter), + CounterIncrement: NewCounterIncrementHandler(e.CounterIncrement, mux, decoder, encoder, errhandler, formatter), + Echo: NewEchoHandler(e.Echo, mux, decoder, encoder, errhandler, formatter), + GenHTTPOpenapi3JSON: http.FileServer(fileSystemGenHTTPOpenapi3JSON), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "api" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.CounterGet = m(s.CounterGet) + s.CounterIncrement = m(s.CounterIncrement) + s.Echo = m(s.Echo) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return api.MethodNames[:] } + +// Mount configures the mux to serve the api endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountCounterGetHandler(mux, h.CounterGet) + MountCounterIncrementHandler(mux, h.CounterIncrement) + MountEchoHandler(mux, h.Echo) + MountGenHTTPOpenapi3JSON(mux, h.GenHTTPOpenapi3JSON) +} + +// Mount configures the mux to serve the api endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountCounterGetHandler configures the mux to serve the "api" service +// "CounterGet" endpoint. +func MountCounterGetHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/counter", f) +} + +// NewCounterGetHandler creates a HTTP handler which loads the HTTP request and +// calls the "api" service "CounterGet" endpoint. +func NewCounterGetHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeCounterGetResponse(encoder) + encodeError = EncodeCounterGetError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "CounterGet") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountCounterIncrementHandler configures the mux to serve the "api" service +// "CounterIncrement" endpoint. +func MountCounterIncrementHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/counter/inc", f) +} + +// NewCounterIncrementHandler creates a HTTP handler which loads the HTTP +// request and calls the "api" service "CounterIncrement" endpoint. +func NewCounterIncrementHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeCounterIncrementRequest(mux, decoder) + encodeResponse = EncodeCounterIncrementResponse(encoder) + encodeError = EncodeCounterIncrementError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "CounterIncrement") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountEchoHandler configures the mux to serve the "api" service "Echo" +// endpoint. +func MountEchoHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("POST", "/echo", f) +} + +// NewEchoHandler creates a HTTP handler which loads the HTTP request and calls +// the "api" service "Echo" endpoint. +func NewEchoHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + decodeRequest = DecodeEchoRequest(mux, decoder) + encodeResponse = EncodeEchoResponse(encoder) + encodeError = EncodeEchoError(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "Echo") + ctx = context.WithValue(ctx, goa.ServiceKey, "api") + payload, err := decodeRequest(r) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + res, err := endpoint(ctx, payload) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + case "/openapi.json": + name = "/openapi3.json" + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountGenHTTPOpenapi3JSON configures the mux to serve GET request made to +// "/openapi.json". +func MountGenHTTPOpenapi3JSON(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/openapi.json", h.ServeHTTP) +} diff --git a/app/api/v1/gen/http/api/server/types.go b/app/api/v1/gen/http/api/server/types.go new file mode 100644 index 0000000..c517257 --- /dev/null +++ b/app/api/v1/gen/http/api/server/types.go @@ -0,0 +1,315 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// api HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + api "github.com/jace-ys/countup/api/v1/gen/api" + apiviews "github.com/jace-ys/countup/api/v1/gen/api/views" + goa "goa.design/goa/v3/pkg" +) + +// CounterIncrementRequestBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP request body. +type CounterIncrementRequestBody struct { + User *string `form:"user,omitempty" json:"user,omitempty" xml:"user,omitempty"` +} + +// EchoRequestBody is the type of the "api" service "Echo" endpoint HTTP +// request body. +type EchoRequestBody struct { + Text *string `form:"text,omitempty" json:"text,omitempty" xml:"text,omitempty"` +} + +// CounterGetResponseBody is the type of the "api" service "CounterGet" +// endpoint HTTP response body. +type CounterGetResponseBody struct { + Count int32 `form:"count" json:"count" xml:"count"` + LastIncrementBy string `form:"last_increment_by" json:"last_increment_by" xml:"last_increment_by"` + LastIncrementAt string `form:"last_increment_at" json:"last_increment_at" xml:"last_increment_at"` + NextFinalizeAt string `form:"next_finalize_at" json:"next_finalize_at" xml:"next_finalize_at"` +} + +// CounterIncrementResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body. +type CounterIncrementResponseBody struct { + Count int32 `form:"count" json:"count" xml:"count"` + LastIncrementBy string `form:"last_increment_by" json:"last_increment_by" xml:"last_increment_by"` + LastIncrementAt string `form:"last_increment_at" json:"last_increment_at" xml:"last_increment_at"` + NextFinalizeAt string `form:"next_finalize_at" json:"next_finalize_at" xml:"next_finalize_at"` +} + +// EchoResponseBody is the type of the "api" service "Echo" endpoint HTTP +// response body. +type EchoResponseBody struct { + Text string `form:"text" json:"text" xml:"text"` +} + +// CounterGetUnauthorizedResponseBody is the type of the "api" service +// "CounterGet" endpoint HTTP response body for the "unauthorized" error. +type CounterGetUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterGetExistingIncrementRequestResponseBody is the type of the "api" +// service "CounterGet" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterGetExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterIncrementUnauthorizedResponseBody is the type of the "api" service +// "CounterIncrement" endpoint HTTP response body for the "unauthorized" error. +type CounterIncrementUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// CounterIncrementExistingIncrementRequestResponseBody is the type of the +// "api" service "CounterIncrement" endpoint HTTP response body for the +// "existing_increment_request" error. +type CounterIncrementExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// EchoUnauthorizedResponseBody is the type of the "api" service "Echo" +// endpoint HTTP response body for the "unauthorized" error. +type EchoUnauthorizedResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// EchoExistingIncrementRequestResponseBody is the type of the "api" service +// "Echo" endpoint HTTP response body for the "existing_increment_request" +// error. +type EchoExistingIncrementRequestResponseBody struct { + // Name is the name of this class of errors. + Name string `form:"name" json:"name" xml:"name"` + // ID is a unique identifier for this particular occurrence of the problem. + ID string `form:"id" json:"id" xml:"id"` + // Message is a human-readable explanation specific to this occurrence of the + // problem. + Message string `form:"message" json:"message" xml:"message"` + // Is the error temporary? + Temporary bool `form:"temporary" json:"temporary" xml:"temporary"` + // Is the error a timeout? + Timeout bool `form:"timeout" json:"timeout" xml:"timeout"` + // Is the error a server-side fault? + Fault bool `form:"fault" json:"fault" xml:"fault"` +} + +// NewCounterGetResponseBody builds the HTTP response body from the result of +// the "CounterGet" endpoint of the "api" service. +func NewCounterGetResponseBody(res *apiviews.CounterInfoView) *CounterGetResponseBody { + body := &CounterGetResponseBody{ + Count: *res.Count, + LastIncrementBy: *res.LastIncrementBy, + LastIncrementAt: *res.LastIncrementAt, + NextFinalizeAt: *res.NextFinalizeAt, + } + return body +} + +// NewCounterIncrementResponseBody builds the HTTP response body from the +// result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementResponseBody(res *apiviews.CounterInfoView) *CounterIncrementResponseBody { + body := &CounterIncrementResponseBody{ + Count: *res.Count, + LastIncrementBy: *res.LastIncrementBy, + LastIncrementAt: *res.LastIncrementAt, + NextFinalizeAt: *res.NextFinalizeAt, + } + return body +} + +// NewEchoResponseBody builds the HTTP response body from the result of the +// "Echo" endpoint of the "api" service. +func NewEchoResponseBody(res *api.EchoResult) *EchoResponseBody { + body := &EchoResponseBody{ + Text: res.Text, + } + return body +} + +// NewCounterGetUnauthorizedResponseBody builds the HTTP response body from the +// result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetUnauthorizedResponseBody(res *goa.ServiceError) *CounterGetUnauthorizedResponseBody { + body := &CounterGetUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterGetExistingIncrementRequestResponseBody builds the HTTP response +// body from the result of the "CounterGet" endpoint of the "api" service. +func NewCounterGetExistingIncrementRequestResponseBody(res *goa.ServiceError) *CounterGetExistingIncrementRequestResponseBody { + body := &CounterGetExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementUnauthorizedResponseBody builds the HTTP response body +// from the result of the "CounterIncrement" endpoint of the "api" service. +func NewCounterIncrementUnauthorizedResponseBody(res *goa.ServiceError) *CounterIncrementUnauthorizedResponseBody { + body := &CounterIncrementUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementExistingIncrementRequestResponseBody builds the HTTP +// response body from the result of the "CounterIncrement" endpoint of the +// "api" service. +func NewCounterIncrementExistingIncrementRequestResponseBody(res *goa.ServiceError) *CounterIncrementExistingIncrementRequestResponseBody { + body := &CounterIncrementExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewEchoUnauthorizedResponseBody builds the HTTP response body from the +// result of the "Echo" endpoint of the "api" service. +func NewEchoUnauthorizedResponseBody(res *goa.ServiceError) *EchoUnauthorizedResponseBody { + body := &EchoUnauthorizedResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewEchoExistingIncrementRequestResponseBody builds the HTTP response body +// from the result of the "Echo" endpoint of the "api" service. +func NewEchoExistingIncrementRequestResponseBody(res *goa.ServiceError) *EchoExistingIncrementRequestResponseBody { + body := &EchoExistingIncrementRequestResponseBody{ + Name: res.Name, + ID: res.ID, + Message: res.Message, + Temporary: res.Temporary, + Timeout: res.Timeout, + Fault: res.Fault, + } + return body +} + +// NewCounterIncrementPayload builds a api service CounterIncrement endpoint +// payload. +func NewCounterIncrementPayload(body *CounterIncrementRequestBody) *api.CounterIncrementPayload { + v := &api.CounterIncrementPayload{ + User: *body.User, + } + + return v +} + +// NewEchoPayload builds a api service Echo endpoint payload. +func NewEchoPayload(body *EchoRequestBody) *api.EchoPayload { + v := &api.EchoPayload{ + Text: *body.Text, + } + + return v +} + +// ValidateCounterIncrementRequestBody runs the validations defined on +// CounterIncrementRequestBody +func ValidateCounterIncrementRequestBody(body *CounterIncrementRequestBody) (err error) { + if body.User == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("user", "body")) + } + return +} + +// ValidateEchoRequestBody runs the validations defined on EchoRequestBody +func ValidateEchoRequestBody(body *EchoRequestBody) (err error) { + if body.Text == nil { + err = goa.MergeErrors(err, goa.MissingFieldError("text", "body")) + } + return +} diff --git a/app/api/v1/gen/http/cli/countup/cli.go b/app/api/v1/gen/http/cli/countup/cli.go new file mode 100644 index 0000000..fb50aaa --- /dev/null +++ b/app/api/v1/gen/http/cli/countup/cli.go @@ -0,0 +1,262 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// countup HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package cli + +import ( + "flag" + "fmt" + "net/http" + "os" + + apic "github.com/jace-ys/countup/api/v1/gen/http/api/client" + webc "github.com/jace-ys/countup/api/v1/gen/http/web/client" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// UsageCommands returns the set of commands and sub-commands using the format +// +// command (subcommand1|subcommand2|...) +func UsageCommands() string { + return `api (counter-get|counter-increment|echo) +web (index|another) +` +} + +// UsageExamples produces an example of a valid invocation of the CLI tool. +func UsageExamples() string { + return os.Args[0] + ` api counter-get` + "\n" + + os.Args[0] + ` web index` + "\n" + + "" +} + +// ParseEndpoint returns the endpoint and payload as specified on the command +// line. +func ParseEndpoint( + scheme, host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restore bool, +) (goa.Endpoint, any, error) { + var ( + apiFlags = flag.NewFlagSet("api", flag.ContinueOnError) + + apiCounterGetFlags = flag.NewFlagSet("counter-get", flag.ExitOnError) + + apiCounterIncrementFlags = flag.NewFlagSet("counter-increment", flag.ExitOnError) + apiCounterIncrementBodyFlag = apiCounterIncrementFlags.String("body", "REQUIRED", "") + + apiEchoFlags = flag.NewFlagSet("echo", flag.ExitOnError) + apiEchoBodyFlag = apiEchoFlags.String("body", "REQUIRED", "") + + webFlags = flag.NewFlagSet("web", flag.ContinueOnError) + + webIndexFlags = flag.NewFlagSet("index", flag.ExitOnError) + + webAnotherFlags = flag.NewFlagSet("another", flag.ExitOnError) + ) + apiFlags.Usage = apiUsage + apiCounterGetFlags.Usage = apiCounterGetUsage + apiCounterIncrementFlags.Usage = apiCounterIncrementUsage + apiEchoFlags.Usage = apiEchoUsage + + webFlags.Usage = webUsage + webIndexFlags.Usage = webIndexUsage + webAnotherFlags.Usage = webAnotherUsage + + if err := flag.CommandLine.Parse(os.Args[1:]); err != nil { + return nil, nil, err + } + + if flag.NArg() < 2 { // two non flag args are required: SERVICE and ENDPOINT (aka COMMAND) + return nil, nil, fmt.Errorf("not enough arguments") + } + + var ( + svcn string + svcf *flag.FlagSet + ) + { + svcn = flag.Arg(0) + switch svcn { + case "api": + svcf = apiFlags + case "web": + svcf = webFlags + default: + return nil, nil, fmt.Errorf("unknown service %q", svcn) + } + } + if err := svcf.Parse(flag.Args()[1:]); err != nil { + return nil, nil, err + } + + var ( + epn string + epf *flag.FlagSet + ) + { + epn = svcf.Arg(0) + switch svcn { + case "api": + switch epn { + case "counter-get": + epf = apiCounterGetFlags + + case "counter-increment": + epf = apiCounterIncrementFlags + + case "echo": + epf = apiEchoFlags + + } + + case "web": + switch epn { + case "index": + epf = webIndexFlags + + case "another": + epf = webAnotherFlags + + } + + } + } + if epf == nil { + return nil, nil, fmt.Errorf("unknown %q endpoint %q", svcn, epn) + } + + // Parse endpoint flags if any + if svcf.NArg() > 1 { + if err := epf.Parse(svcf.Args()[1:]); err != nil { + return nil, nil, err + } + } + + var ( + data any + endpoint goa.Endpoint + err error + ) + { + switch svcn { + case "api": + c := apic.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "counter-get": + endpoint = c.CounterGet() + case "counter-increment": + endpoint = c.CounterIncrement() + data, err = apic.BuildCounterIncrementPayload(*apiCounterIncrementBodyFlag) + case "echo": + endpoint = c.Echo() + data, err = apic.BuildEchoPayload(*apiEchoBodyFlag) + } + case "web": + c := webc.NewClient(scheme, host, doer, enc, dec, restore) + switch epn { + case "index": + endpoint = c.Index() + case "another": + endpoint = c.Another() + } + } + } + if err != nil { + return nil, nil, err + } + + return endpoint, data, nil +} + +// apiUsage displays the usage of the api command and its subcommands. +func apiUsage() { + fmt.Fprintf(os.Stderr, `Service is the api service interface. +Usage: + %[1]s [globalflags] api COMMAND [flags] + +COMMAND: + counter-get: CounterGet implements CounterGet. + counter-increment: CounterIncrement implements CounterIncrement. + echo: Echo implements Echo. + +Additional help: + %[1]s api COMMAND --help +`, os.Args[0]) +} +func apiCounterGetUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-get + +CounterGet implements CounterGet. + +Example: + %[1]s api counter-get +`, os.Args[0]) +} + +func apiCounterIncrementUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api counter-increment -body JSON + +CounterIncrement implements CounterIncrement. + -body JSON: + +Example: + %[1]s api counter-increment --body '{ + "user": "Nihil doloribus et sed sequi consequatur." + }' +`, os.Args[0]) +} + +func apiEchoUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] api echo -body JSON + +Echo implements Echo. + -body JSON: + +Example: + %[1]s api echo --body '{ + "text": "Vel omnis quo sit." + }' +`, os.Args[0]) +} + +// webUsage displays the usage of the web command and its subcommands. +func webUsage() { + fmt.Fprintf(os.Stderr, `Service is the web service interface. +Usage: + %[1]s [globalflags] web COMMAND [flags] + +COMMAND: + index: Index implements index. + another: Another implements another. + +Additional help: + %[1]s web COMMAND --help +`, os.Args[0]) +} +func webIndexUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web index + +Index implements index. + +Example: + %[1]s web index +`, os.Args[0]) +} + +func webAnotherUsage() { + fmt.Fprintf(os.Stderr, `%[1]s [flags] web another + +Another implements another. + +Example: + %[1]s web another +`, os.Args[0]) +} diff --git a/app/api/v1/gen/http/openapi.json b/app/api/v1/gen/http/openapi.json new file mode 100644 index 0000000..3cfe53a --- /dev/null +++ b/app/api/v1/gen/http/openapi.json @@ -0,0 +1 @@ +{"swagger":"2.0","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"host":"localhost:8080","consumes":["application/json","application/xml","application/gob"],"produces":["application/json","application/xml","application/gob"],"paths":{"/":{"get":{"tags":["web"],"summary":"index web","operationId":"web#index","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}}},"schemes":["http"]}},"/another":{"get":{"tags":["web"],"summary":"another web","operationId":"web#another","produces":["text/html"],"responses":{"200":{"description":"OK response.","schema":{"type":"string","format":"byte"}}},"schemes":["http"]}},"/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterGetUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterGetExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","parameters":[{"name":"CounterIncrementRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APICounterIncrementRequestBody","required":["user"]}}],"responses":{"202":{"description":"Accepted response.","schema":{"$ref":"#/definitions/CounterInfo"}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APICounterIncrementUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APICounterIncrementExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","parameters":[{"name":"EchoRequestBody","in":"body","required":true,"schema":{"$ref":"#/definitions/APIEchoRequestBody","required":["text"]}}],"responses":{"200":{"description":"OK response.","schema":{"$ref":"#/definitions/APIEchoResponseBody","required":["text"]}},"401":{"description":"Unauthorized response.","schema":{"$ref":"#/definitions/APIEchoUnauthorizedResponseBody"}},"429":{"description":"Too Many Requests response.","schema":{"$ref":"#/definitions/APIEchoExistingIncrementRequestResponseBody"}}},"schemes":["http"]}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded","schema":{"type":"file"}}},"schemes":["http"]}},"/static/{path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true,"type":"string"}],"responses":{"200":{"description":"File downloaded","schema":{"type":"file"}},"404":{"description":"File not found","schema":{"$ref":"#/definitions/Error"}}},"schemes":["http"]}}},"definitions":{"APICounterGetExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterGetUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterGet_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_existing_increment_request_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"APICounterIncrementRequestBody":{"title":"APICounterIncrementRequestBody","type":"object","properties":{"user":{"type":"string","example":"Non perspiciatis eum dicta sit."}},"example":{"user":"Odio tenetur temporibus."},"required":["user"]},"APICounterIncrementUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":true}},"description":"CounterIncrement_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoExistingIncrementRequestResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Echo_existing_increment_request_Response_Body result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]},"APIEchoRequestBody":{"title":"APIEchoRequestBody","type":"object","properties":{"text":{"type":"string","example":"Deleniti necessitatibus numquam perspiciatis quia ipsa quam."}},"example":{"text":"Nemo dolorem ullam magnam."},"required":["text"]},"APIEchoResponseBody":{"title":"APIEchoResponseBody","type":"object","properties":{"text":{"type":"string","example":"Praesentium qui debitis."}},"example":{"text":"Odit cum blanditiis ut."},"required":["text"]},"APIEchoUnauthorizedResponseBody":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":false},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Echo_unauthorized_Response_Body result type (default view)","example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":false,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]},"CounterInfo":{"title":"Mediatype identifier: application/vnd.countup.counter-info`; view=default","type":"object","properties":{"count":{"type":"integer","example":1315490442,"format":"int32"},"last_increment_at":{"type":"string","example":"Eligendi quisquam."},"last_increment_by":{"type":"string","example":"Vero molestiae."},"next_finalize_at":{"type":"string","example":"Aut in dolor eum consequatur."}},"description":"CounterGetResponseBody result type (default view)","example":{"count":1162746042,"last_increment_at":"Tempore asperiores.","last_increment_by":"Tempora repellendus.","next_finalize_at":"Quae voluptatibus dolor fugit quia."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"Error":{"title":"Mediatype identifier: application/vnd.goa.error; view=default","type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":false},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"description":"Error response result type (default view)","example":{"fault":true,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":true},"required":["name","id","message","temporary","timeout","fault"]}}} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi.yaml b/app/api/v1/gen/http/openapi.yaml new file mode 100644 index 0000000..9ccd634 --- /dev/null +++ b/app/api/v1/gen/http/openapi.yaml @@ -0,0 +1,527 @@ +swagger: "2.0" +info: + title: Count Up + description: A production-ready Go service deployed on Kubernetes + version: 1.0.0 +host: localhost:8080 +consumes: + - application/json + - application/xml + - application/gob +produces: + - application/json + - application/xml + - application/gob +paths: + /: + get: + tags: + - web + summary: index web + operationId: web#index + produces: + - text/html + responses: + "200": + description: OK response. + schema: + type: string + format: byte + schemes: + - http + /another: + get: + tags: + - web + summary: another web + operationId: web#another + produces: + - text/html + responses: + "200": + description: OK response. + schema: + type: string + format: byte + schemes: + - http + /counter: + get: + tags: + - api + summary: CounterGet api + operationId: api#CounterGet + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterGetUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterGetExistingIncrementRequestResponseBody' + schemes: + - http + /counter/inc: + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + parameters: + - name: CounterIncrementRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APICounterIncrementRequestBody' + required: + - user + responses: + "202": + description: Accepted response. + schema: + $ref: '#/definitions/CounterInfo' + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APICounterIncrementUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APICounterIncrementExistingIncrementRequestResponseBody' + schemes: + - http + /echo: + post: + tags: + - api + summary: Echo api + operationId: api#Echo + parameters: + - name: EchoRequestBody + in: body + required: true + schema: + $ref: '#/definitions/APIEchoRequestBody' + required: + - text + responses: + "200": + description: OK response. + schema: + $ref: '#/definitions/APIEchoResponseBody' + required: + - text + "401": + description: Unauthorized response. + schema: + $ref: '#/definitions/APIEchoUnauthorizedResponseBody' + "429": + description: Too Many Requests response. + schema: + $ref: '#/definitions/APIEchoExistingIncrementRequestResponseBody' + schemes: + - http + /openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/openapi.json + responses: + "200": + description: File downloaded + schema: + type: file + schemes: + - http + /static/{path}: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/{*path} + parameters: + - name: path + in: path + description: Relative file path + required: true + type: string + responses: + "200": + description: File downloaded + schema: + type: file + "404": + description: File not found + schema: + $ref: '#/definitions/Error' + schemes: + - http +definitions: + APICounterGetExistingIncrementRequestResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_existing_increment_request_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterGetUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterGet_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementExistingIncrementRequestResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterIncrement_existing_increment_request_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + APICounterIncrementRequestBody: + title: APICounterIncrementRequestBody + type: object + properties: + user: + type: string + example: Non perspiciatis eum dicta sit. + example: + user: Odio tenetur temporibus. + required: + - user + APICounterIncrementUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: true + description: CounterIncrement_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APIEchoExistingIncrementRequestResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: Echo_existing_increment_request_Response_Body result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault + APIEchoRequestBody: + title: APIEchoRequestBody + type: object + properties: + text: + type: string + example: Deleniti necessitatibus numquam perspiciatis quia ipsa quam. + example: + text: Nemo dolorem ullam magnam. + required: + - text + APIEchoResponseBody: + title: APIEchoResponseBody + type: object + properties: + text: + type: string + example: Praesentium qui debitis. + example: + text: Odit cum blanditiis ut. + required: + - text + APIEchoUnauthorizedResponseBody: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: false + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: Echo_unauthorized_Response_Body result type (default view) + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: false + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault + CounterInfo: + title: 'Mediatype identifier: application/vnd.countup.counter-info`; view=default' + type: object + properties: + count: + type: integer + example: 1315490442 + format: int32 + last_increment_at: + type: string + example: Eligendi quisquam. + last_increment_by: + type: string + example: Vero molestiae. + next_finalize_at: + type: string + example: Aut in dolor eum consequatur. + description: CounterGetResponseBody result type (default view) + example: + count: 1162746042 + last_increment_at: Tempore asperiores. + last_increment_by: Tempora repellendus. + next_finalize_at: Quae voluptatibus dolor fugit quia. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + Error: + title: 'Mediatype identifier: application/vnd.goa.error; view=default' + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: false + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + description: Error response result type (default view) + example: + fault: true + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: true + required: + - name + - id + - message + - temporary + - timeout + - fault diff --git a/app/api/v1/gen/http/openapi3.json b/app/api/v1/gen/http/openapi3.json new file mode 100644 index 0000000..0787a4c --- /dev/null +++ b/app/api/v1/gen/http/openapi3.json @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"Count Up","description":"A production-ready Go service deployed on Kubernetes","version":"1.0.0"},"servers":[{"url":"http://localhost:8080"},{"url":"http://localhost:80"}],"paths":{"/":{"get":{"tags":["web"],"summary":"index web","operationId":"web#index","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"TmVtbyBtYWduaSBkdWNpbXVzIGltcGVkaXQu","format":"binary"},"example":"TW9sbGl0aWEgZGVsZW5pdGkgZXhwZWRpdGEgYWNjdXNhbnRpdW0u"}}}}}},"/another":{"get":{"tags":["web"],"summary":"another web","operationId":"web#another","responses":{"200":{"description":"OK response.","content":{"text/html":{"schema":{"type":"string","example":"TnVtcXVhbSBkb2xvcmlidXMgc3VzY2lwaXQu","format":"binary"},"example":"SW52ZW50b3JlIGxhYm9yZSBlcnJvciBhbmltaS4="}}}}}},"/counter":{"get":{"tags":["api"],"summary":"CounterGet api","operationId":"api#CounterGet","responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":1425094436,"last_increment_at":"Iste et distinctio accusantium.","last_increment_by":"Sequi ipsa aliquam esse.","next_finalize_at":"A non."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/counter/inc":{"post":{"tags":["api"],"summary":"CounterIncrement api","operationId":"api#CounterIncrement","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterIncrementRequestBody"},"example":{"user":"Nihil doloribus et sed sequi consequatur."}}}},"responses":{"202":{"description":"Accepted response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CounterInfo"},"example":{"count":278214526,"last_increment_at":"Inventore accusantium.","last_increment_by":"Laborum vel mollitia aut.","next_finalize_at":"Voluptas ut eius."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/echo":{"post":{"tags":["api"],"summary":"Echo api","operationId":"api#Echo","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Vel omnis quo sit."}}}},"responses":{"200":{"description":"OK response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EchoRequestBody"},"example":{"text":"Distinctio illo."}}}},"401":{"description":"unauthorized: Unauthorized response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"existing_increment_request: Too Many Requests response.","content":{"application/vnd.goa.error":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/openapi.json":{"get":{"tags":["api"],"summary":"Download gen/http/openapi3.json","operationId":"api#/openapi.json","responses":{"200":{"description":"File downloaded"}}}},"/static/{*path}":{"get":{"tags":["web"],"summary":"Download static/","operationId":"web#/static/{*path}","parameters":[{"name":"path","in":"path","description":"Relative file path","required":true}],"responses":{"200":{"description":"File not found"},"404":{"description":"File not found"}}}}},"components":{"schemas":{"CounterIncrementRequestBody":{"type":"object","properties":{"user":{"type":"string","example":"Omnis debitis."}},"example":{"user":"Eos minima dolorem id sunt voluptates voluptas."},"required":["user"]},"CounterInfo":{"type":"object","properties":{"count":{"type":"integer","example":495281550,"format":"int32"},"last_increment_at":{"type":"string","example":"Aut tenetur eos."},"last_increment_by":{"type":"string","example":"Consectetur odio."},"next_finalize_at":{"type":"string","example":"Laborum et veniam et illum quaerat et."}},"example":{"count":1773148578,"last_increment_at":"Voluptatum omnis possimus saepe deleniti.","last_increment_by":"Corporis est sunt voluptatem reprehenderit neque modi.","next_finalize_at":"Rerum facere veritatis."},"required":["count","last_increment_by","last_increment_at","next_finalize_at"]},"EchoRequestBody":{"type":"object","properties":{"text":{"type":"string","example":"Cumque maxime dolore hic laboriosam."}},"example":{"text":"Aut recusandae cum."},"required":["text"]},"Error":{"type":"object","properties":{"fault":{"type":"boolean","description":"Is the error a server-side fault?","example":true},"id":{"type":"string","description":"ID is a unique identifier for this particular occurrence of the problem.","example":"123abc"},"message":{"type":"string","description":"Message is a human-readable explanation specific to this occurrence of the problem.","example":"parameter 'p' must be an integer"},"name":{"type":"string","description":"Name is the name of this class of errors.","example":"bad_request"},"temporary":{"type":"boolean","description":"Is the error temporary?","example":true},"timeout":{"type":"boolean","description":"Is the error a timeout?","example":false}},"example":{"fault":false,"id":"123abc","message":"parameter 'p' must be an integer","name":"bad_request","temporary":true,"timeout":false},"required":["name","id","message","temporary","timeout","fault"]}}},"tags":[{"name":"api"},{"name":"web"}]} \ No newline at end of file diff --git a/app/api/v1/gen/http/openapi3.yaml b/app/api/v1/gen/http/openapi3.yaml new file mode 100644 index 0000000..659f61e --- /dev/null +++ b/app/api/v1/gen/http/openapi3.yaml @@ -0,0 +1,383 @@ +openapi: 3.0.3 +info: + title: Count Up + description: A production-ready Go service deployed on Kubernetes + version: 1.0.0 +servers: + - url: http://localhost:8080 + - url: http://localhost:80 +paths: + /: + get: + tags: + - web + summary: index web + operationId: web#index + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 78 + - 101 + - 109 + - 111 + - 32 + - 109 + - 97 + - 103 + - 110 + - 105 + - 32 + - 100 + - 117 + - 99 + - 105 + - 109 + - 117 + - 115 + - 32 + - 105 + - 109 + - 112 + - 101 + - 100 + - 105 + - 116 + - 46 + format: binary + example: + - 77 + - 111 + - 108 + - 108 + - 105 + - 116 + - 105 + - 97 + - 32 + - 100 + - 101 + - 108 + - 101 + - 110 + - 105 + - 116 + - 105 + - 32 + - 101 + - 120 + - 112 + - 101 + - 100 + - 105 + - 116 + - 97 + - 32 + - 97 + - 99 + - 99 + - 117 + - 115 + - 97 + - 110 + - 116 + - 105 + - 117 + - 109 + - 46 + /another: + get: + tags: + - web + summary: another web + operationId: web#another + responses: + "200": + description: OK response. + content: + text/html: + schema: + type: string + example: + - 78 + - 117 + - 109 + - 113 + - 117 + - 97 + - 109 + - 32 + - 100 + - 111 + - 108 + - 111 + - 114 + - 105 + - 98 + - 117 + - 115 + - 32 + - 115 + - 117 + - 115 + - 99 + - 105 + - 112 + - 105 + - 116 + - 46 + format: binary + example: + - 73 + - 110 + - 118 + - 101 + - 110 + - 116 + - 111 + - 114 + - 101 + - 32 + - 108 + - 97 + - 98 + - 111 + - 114 + - 101 + - 32 + - 101 + - 114 + - 114 + - 111 + - 114 + - 32 + - 97 + - 110 + - 105 + - 109 + - 105 + - 46 + /counter: + get: + tags: + - api + summary: CounterGet api + operationId: api#CounterGet + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 1425094436 + last_increment_at: Iste et distinctio accusantium. + last_increment_by: Sequi ipsa aliquam esse. + next_finalize_at: A non. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "429": + description: 'existing_increment_request: Too Many Requests response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /counter/inc: + post: + tags: + - api + summary: CounterIncrement api + operationId: api#CounterIncrement + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CounterIncrementRequestBody' + example: + user: Nihil doloribus et sed sequi consequatur. + responses: + "202": + description: Accepted response. + content: + application/json: + schema: + $ref: '#/components/schemas/CounterInfo' + example: + count: 278214526 + last_increment_at: Inventore accusantium. + last_increment_by: Laborum vel mollitia aut. + next_finalize_at: Voluptas ut eius. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "429": + description: 'existing_increment_request: Too Many Requests response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /echo: + post: + tags: + - api + summary: Echo api + operationId: api#Echo + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Vel omnis quo sit. + responses: + "200": + description: OK response. + content: + application/json: + schema: + $ref: '#/components/schemas/EchoRequestBody' + example: + text: Distinctio illo. + "401": + description: 'unauthorized: Unauthorized response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + "429": + description: 'existing_increment_request: Too Many Requests response.' + content: + application/vnd.goa.error: + schema: + $ref: '#/components/schemas/Error' + /openapi.json: + get: + tags: + - api + summary: Download gen/http/openapi3.json + operationId: api#/openapi.json + responses: + "200": + description: File downloaded + /static/{*path}: + get: + tags: + - web + summary: Download static/ + operationId: web#/static/{*path} + parameters: + - name: path + in: path + description: Relative file path + required: true + responses: + "200": + description: File not found + "404": + description: File not found +components: + schemas: + CounterIncrementRequestBody: + type: object + properties: + user: + type: string + example: Omnis debitis. + example: + user: Eos minima dolorem id sunt voluptates voluptas. + required: + - user + CounterInfo: + type: object + properties: + count: + type: integer + example: 495281550 + format: int32 + last_increment_at: + type: string + example: Aut tenetur eos. + last_increment_by: + type: string + example: Consectetur odio. + next_finalize_at: + type: string + example: Laborum et veniam et illum quaerat et. + example: + count: 1773148578 + last_increment_at: Voluptatum omnis possimus saepe deleniti. + last_increment_by: Corporis est sunt voluptatem reprehenderit neque modi. + next_finalize_at: Rerum facere veritatis. + required: + - count + - last_increment_by + - last_increment_at + - next_finalize_at + EchoRequestBody: + type: object + properties: + text: + type: string + example: Cumque maxime dolore hic laboriosam. + example: + text: Aut recusandae cum. + required: + - text + Error: + type: object + properties: + fault: + type: boolean + description: Is the error a server-side fault? + example: true + id: + type: string + description: ID is a unique identifier for this particular occurrence of the problem. + example: 123abc + message: + type: string + description: Message is a human-readable explanation specific to this occurrence of the problem. + example: parameter 'p' must be an integer + name: + type: string + description: Name is the name of this class of errors. + example: bad_request + temporary: + type: boolean + description: Is the error temporary? + example: true + timeout: + type: boolean + description: Is the error a timeout? + example: false + example: + fault: false + id: 123abc + message: parameter 'p' must be an integer + name: bad_request + temporary: true + timeout: false + required: + - name + - id + - message + - temporary + - timeout + - fault +tags: + - name: api + - name: web diff --git a/app/api/v1/gen/http/web/client/cli.go b/app/api/v1/gen/http/web/client/cli.go new file mode 100644 index 0000000..fafe96a --- /dev/null +++ b/app/api/v1/gen/http/web/client/cli.go @@ -0,0 +1,8 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client CLI support package +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client diff --git a/app/api/v1/gen/http/web/client/client.go b/app/api/v1/gen/http/web/client/client.go new file mode 100644 index 0000000..df4b614 --- /dev/null +++ b/app/api/v1/gen/http/web/client/client.go @@ -0,0 +1,93 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web client HTTP transport +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Client lists the web service endpoint HTTP clients. +type Client struct { + // Index Doer is the HTTP client used to make requests to the index endpoint. + IndexDoer goahttp.Doer + + // Another Doer is the HTTP client used to make requests to the another + // endpoint. + AnotherDoer goahttp.Doer + + // RestoreResponseBody controls whether the response bodies are reset after + // decoding so they can be read again. + RestoreResponseBody bool + + scheme string + host string + encoder func(*http.Request) goahttp.Encoder + decoder func(*http.Response) goahttp.Decoder +} + +// NewClient instantiates HTTP clients for all the web service servers. +func NewClient( + scheme string, + host string, + doer goahttp.Doer, + enc func(*http.Request) goahttp.Encoder, + dec func(*http.Response) goahttp.Decoder, + restoreBody bool, +) *Client { + return &Client{ + IndexDoer: doer, + AnotherDoer: doer, + RestoreResponseBody: restoreBody, + scheme: scheme, + host: host, + decoder: dec, + encoder: enc, + } +} + +// Index returns an endpoint that makes HTTP requests to the web service index +// server. +func (c *Client) Index() goa.Endpoint { + var ( + decodeResponse = DecodeIndexResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildIndexRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.IndexDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "index", err) + } + return decodeResponse(resp) + } +} + +// Another returns an endpoint that makes HTTP requests to the web service +// another server. +func (c *Client) Another() goa.Endpoint { + var ( + decodeResponse = DecodeAnotherResponse(c.decoder, c.RestoreResponseBody) + ) + return func(ctx context.Context, v any) (any, error) { + req, err := c.BuildAnotherRequest(ctx, v) + if err != nil { + return nil, err + } + resp, err := c.AnotherDoer.Do(req) + if err != nil { + return nil, goahttp.ErrRequestError("web", "another", err) + } + return decodeResponse(resp) + } +} diff --git a/app/api/v1/gen/http/web/client/encode_decode.go b/app/api/v1/gen/http/web/client/encode_decode.go new file mode 100644 index 0000000..ebee749 --- /dev/null +++ b/app/api/v1/gen/http/web/client/encode_decode.go @@ -0,0 +1,118 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + + goahttp "goa.design/goa/v3/http" +) + +// BuildIndexRequest instantiates a HTTP request object with method and path +// set to call the "web" service "index" endpoint +func (c *Client) BuildIndexRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: IndexWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "index", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeIndexResponse returns a decoder for responses returned by the web +// index endpoint. restoreBody controls whether the response body should be +// restored after having been read. +func DecodeIndexResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "index", err) + } + return body, nil + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "index", resp.StatusCode, string(body)) + } + } +} + +// BuildAnotherRequest instantiates a HTTP request object with method and path +// set to call the "web" service "another" endpoint +func (c *Client) BuildAnotherRequest(ctx context.Context, v any) (*http.Request, error) { + u := &url.URL{Scheme: c.scheme, Host: c.host, Path: AnotherWebPath()} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, goahttp.ErrInvalidURL("web", "another", u.String(), err) + } + if ctx != nil { + req = req.WithContext(ctx) + } + + return req, nil +} + +// DecodeAnotherResponse returns a decoder for responses returned by the web +// another endpoint. restoreBody controls whether the response body should be +// restored after having been read. +func DecodeAnotherResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) { + return func(resp *http.Response) (any, error) { + if restoreBody { + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + defer func() { + resp.Body = io.NopCloser(bytes.NewBuffer(b)) + }() + } else { + defer resp.Body.Close() + } + switch resp.StatusCode { + case http.StatusOK: + var ( + body []byte + err error + ) + err = decoder(resp).Decode(&body) + if err != nil { + return nil, goahttp.ErrDecodingError("web", "another", err) + } + return body, nil + default: + body, _ := io.ReadAll(resp.Body) + return nil, goahttp.ErrInvalidResponse("web", "another", resp.StatusCode, string(body)) + } + } +} diff --git a/app/api/v1/gen/http/web/client/paths.go b/app/api/v1/gen/http/web/client/paths.go new file mode 100644 index 0000000..6ea65ce --- /dev/null +++ b/app/api/v1/gen/http/web/client/paths.go @@ -0,0 +1,18 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the web service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client + +// IndexWebPath returns the URL path to the web service index HTTP endpoint. +func IndexWebPath() string { + return "/" +} + +// AnotherWebPath returns the URL path to the web service another HTTP endpoint. +func AnotherWebPath() string { + return "/another" +} diff --git a/app/api/v1/gen/http/web/client/types.go b/app/api/v1/gen/http/web/client/types.go new file mode 100644 index 0000000..0f1ccd1 --- /dev/null +++ b/app/api/v1/gen/http/web/client/types.go @@ -0,0 +1,8 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP client types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package client diff --git a/app/api/v1/gen/http/web/server/encode_decode.go b/app/api/v1/gen/http/web/server/encode_decode.go new file mode 100644 index 0000000..57b11ff --- /dev/null +++ b/app/api/v1/gen/http/web/server/encode_decode.go @@ -0,0 +1,41 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server encoders and decoders +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + + goahttp "goa.design/goa/v3/http" +) + +// EncodeIndexResponse returns an encoder for responses returned by the web +// index endpoint. +func EncodeIndexResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/html") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} + +// EncodeAnotherResponse returns an encoder for responses returned by the web +// another endpoint. +func EncodeAnotherResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error { + return func(ctx context.Context, w http.ResponseWriter, v any) error { + res, _ := v.([]byte) + ctx = context.WithValue(ctx, goahttp.ContentTypeKey, "text/html") + enc := encoder(ctx, w) + body := res + w.WriteHeader(http.StatusOK) + return enc.Encode(body) + } +} diff --git a/app/api/v1/gen/http/web/server/paths.go b/app/api/v1/gen/http/web/server/paths.go new file mode 100644 index 0000000..a72edb5 --- /dev/null +++ b/app/api/v1/gen/http/web/server/paths.go @@ -0,0 +1,18 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// HTTP request path constructors for the web service. +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +// IndexWebPath returns the URL path to the web service index HTTP endpoint. +func IndexWebPath() string { + return "/" +} + +// AnotherWebPath returns the URL path to the web service another HTTP endpoint. +func AnotherWebPath() string { + return "/another" +} diff --git a/app/api/v1/gen/http/web/server/server.go b/app/api/v1/gen/http/web/server/server.go new file mode 100644 index 0000000..b1225c6 --- /dev/null +++ b/app/api/v1/gen/http/web/server/server.go @@ -0,0 +1,207 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server + +import ( + "context" + "net/http" + "path" + + web "github.com/jace-ys/countup/api/v1/gen/web" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +// Server lists the web service endpoint HTTP handlers. +type Server struct { + Mounts []*MountPoint + Index http.Handler + Another http.Handler + Static http.Handler +} + +// MountPoint holds information about the mounted endpoints. +type MountPoint struct { + // Method is the name of the service method served by the mounted HTTP handler. + Method string + // Verb is the HTTP method used to match requests to the mounted handler. + Verb string + // Pattern is the HTTP request path pattern used to match requests to the + // mounted handler. + Pattern string +} + +// New instantiates HTTP handlers for all the web service endpoints using the +// provided encoder and decoder. The handlers are mounted on the given mux +// using the HTTP verb and path defined in the design. errhandler is called +// whenever a response fails to be encoded. formatter is used to format errors +// returned by the service methods prior to encoding. Both errhandler and +// formatter are optional and can be nil. +func New( + e *web.Endpoints, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, + fileSystemStatic http.FileSystem, +) *Server { + if fileSystemStatic == nil { + fileSystemStatic = http.Dir(".") + } + fileSystemStatic = appendPrefix(fileSystemStatic, "/static/") + return &Server{ + Mounts: []*MountPoint{ + {"Index", "GET", "/"}, + {"Another", "GET", "/another"}, + {"Serve static/", "GET", "/static"}, + }, + Index: NewIndexHandler(e.Index, mux, decoder, encoder, errhandler, formatter), + Another: NewAnotherHandler(e.Another, mux, decoder, encoder, errhandler, formatter), + Static: http.FileServer(fileSystemStatic), + } +} + +// Service returns the name of the service served. +func (s *Server) Service() string { return "web" } + +// Use wraps the server handlers with the given middleware. +func (s *Server) Use(m func(http.Handler) http.Handler) { + s.Index = m(s.Index) + s.Another = m(s.Another) +} + +// MethodNames returns the methods served. +func (s *Server) MethodNames() []string { return web.MethodNames[:] } + +// Mount configures the mux to serve the web endpoints. +func Mount(mux goahttp.Muxer, h *Server) { + MountIndexHandler(mux, h.Index) + MountAnotherHandler(mux, h.Another) + MountStatic(mux, http.StripPrefix("/static", h.Static)) +} + +// Mount configures the mux to serve the web endpoints. +func (s *Server) Mount(mux goahttp.Muxer) { + Mount(mux, s) +} + +// MountIndexHandler configures the mux to serve the "web" service "index" +// endpoint. +func MountIndexHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/", f) +} + +// NewIndexHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "index" endpoint. +func NewIndexHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeIndexResponse(encoder) + encodeError = goahttp.ErrorEncoder(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "index") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// MountAnotherHandler configures the mux to serve the "web" service "another" +// endpoint. +func MountAnotherHandler(mux goahttp.Muxer, h http.Handler) { + f, ok := h.(http.HandlerFunc) + if !ok { + f = func(w http.ResponseWriter, r *http.Request) { + h.ServeHTTP(w, r) + } + } + mux.Handle("GET", "/another", f) +} + +// NewAnotherHandler creates a HTTP handler which loads the HTTP request and +// calls the "web" service "another" endpoint. +func NewAnotherHandler( + endpoint goa.Endpoint, + mux goahttp.Muxer, + decoder func(*http.Request) goahttp.Decoder, + encoder func(context.Context, http.ResponseWriter) goahttp.Encoder, + errhandler func(context.Context, http.ResponseWriter, error), + formatter func(ctx context.Context, err error) goahttp.Statuser, +) http.Handler { + var ( + encodeResponse = EncodeAnotherResponse(encoder) + encodeError = goahttp.ErrorEncoder(encoder, formatter) + ) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept")) + ctx = context.WithValue(ctx, goa.MethodKey, "another") + ctx = context.WithValue(ctx, goa.ServiceKey, "web") + var err error + res, err := endpoint(ctx, nil) + if err != nil { + if err := encodeError(ctx, w, err); err != nil { + errhandler(ctx, w, err) + } + return + } + if err := encodeResponse(ctx, w, res); err != nil { + errhandler(ctx, w, err) + } + }) +} + +// appendFS is a custom implementation of fs.FS that appends a specified prefix +// to the file paths before delegating the Open call to the underlying fs.FS. +type appendFS struct { + prefix string + fs http.FileSystem +} + +// Open opens the named file, appending the prefix to the file path before +// passing it to the underlying fs.FS. +func (s appendFS) Open(name string) (http.File, error) { + switch name { + } + return s.fs.Open(path.Join(s.prefix, name)) +} + +// appendPrefix returns a new fs.FS that appends the specified prefix to file paths +// before delegating to the provided embed.FS. +func appendPrefix(fsys http.FileSystem, prefix string) http.FileSystem { + return appendFS{prefix: prefix, fs: fsys} +} + +// MountStatic configures the mux to serve GET request made to "/static". +func MountStatic(mux goahttp.Muxer, h http.Handler) { + mux.Handle("GET", "/static/", h.ServeHTTP) + mux.Handle("GET", "/static/{*path}", h.ServeHTTP) +} diff --git a/app/api/v1/gen/http/web/server/types.go b/app/api/v1/gen/http/web/server/types.go new file mode 100644 index 0000000..573bc71 --- /dev/null +++ b/app/api/v1/gen/http/web/server/types.go @@ -0,0 +1,8 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web HTTP server types +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package server diff --git a/app/api/v1/gen/web/client.go b/app/api/v1/gen/web/client.go new file mode 100644 index 0000000..2a2f38a --- /dev/null +++ b/app/api/v1/gen/web/client.go @@ -0,0 +1,48 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web client +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Client is the "web" service client. +type Client struct { + IndexEndpoint goa.Endpoint + AnotherEndpoint goa.Endpoint +} + +// NewClient initializes a "web" service client given the endpoints. +func NewClient(index, another goa.Endpoint) *Client { + return &Client{ + IndexEndpoint: index, + AnotherEndpoint: another, + } +} + +// Index calls the "index" endpoint of the "web" service. +func (c *Client) Index(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.IndexEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} + +// Another calls the "another" endpoint of the "web" service. +func (c *Client) Another(ctx context.Context) (res []byte, err error) { + var ires any + ires, err = c.AnotherEndpoint(ctx, nil) + if err != nil { + return + } + return ires.([]byte), nil +} diff --git a/app/api/v1/gen/web/endpoints.go b/app/api/v1/gen/web/endpoints.go new file mode 100644 index 0000000..280b9d1 --- /dev/null +++ b/app/api/v1/gen/web/endpoints.go @@ -0,0 +1,50 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web endpoints +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" + + goa "goa.design/goa/v3/pkg" +) + +// Endpoints wraps the "web" service endpoints. +type Endpoints struct { + Index goa.Endpoint + Another goa.Endpoint +} + +// NewEndpoints wraps the methods of the "web" service with endpoints. +func NewEndpoints(s Service) *Endpoints { + return &Endpoints{ + Index: NewIndexEndpoint(s), + Another: NewAnotherEndpoint(s), + } +} + +// Use applies the given middleware to all the "web" service endpoints. +func (e *Endpoints) Use(m func(goa.Endpoint) goa.Endpoint) { + e.Index = m(e.Index) + e.Another = m(e.Another) +} + +// NewIndexEndpoint returns an endpoint function that calls the method "index" +// of service "web". +func NewIndexEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Index(ctx) + } +} + +// NewAnotherEndpoint returns an endpoint function that calls the method +// "another" of service "web". +func NewAnotherEndpoint(s Service) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + return s.Another(ctx) + } +} diff --git a/app/api/v1/gen/web/service.go b/app/api/v1/gen/web/service.go new file mode 100644 index 0000000..6aaa0e1 --- /dev/null +++ b/app/api/v1/gen/web/service.go @@ -0,0 +1,36 @@ +// Code generated by goa v3.19.1, DO NOT EDIT. +// +// web service +// +// Command: +// $ goa gen github.com/jace-ys/countup/api/v1 -o api/v1 + +package web + +import ( + "context" +) + +// Service is the web service interface. +type Service interface { + // Index implements index. + Index(context.Context) (res []byte, err error) + // Another implements another. + Another(context.Context) (res []byte, err error) +} + +// APIName is the name of the API as defined in the design. +const APIName = "countup" + +// APIVersion is the version of the API as defined in the design. +const APIVersion = "1.0.0" + +// ServiceName is the name of the service as defined in the design. This is the +// same value that is set in the endpoint request contexts under the ServiceKey +// key. +const ServiceName = "web" + +// MethodNames lists the service method names as defined in the design. These +// are the same values that are set in the endpoint request contexts under the +// MethodKey key. +var MethodNames = [2]string{"index", "another"} diff --git a/app/atlas.hcl b/app/atlas.hcl new file mode 100644 index 0000000..75f4a1b --- /dev/null +++ b/app/atlas.hcl @@ -0,0 +1,9 @@ +env "dev" { + src = "file://schema/schema.sql" + dev = "docker://postgres/15/dev" + + migration { + dir = "file://schema/migrations" + format = "goose" + } +} \ No newline at end of file diff --git a/app/cmd/countup-cli/grpc.go b/app/cmd/countup-cli/grpc.go new file mode 100644 index 0000000..753d3c1 --- /dev/null +++ b/app/cmd/countup-cli/grpc.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + + cli "github.com/jace-ys/countup/api/v1/gen/grpc/cli/countup" + goa "goa.design/goa/v3/pkg" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +func doGRPC(_, host string, _ int, _ bool) (goa.Endpoint, any, error) { + conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + fmt.Fprintf(os.Stderr, "could not connect to gRPC server at %s: %v\n", host, err) + } + return cli.ParseEndpoint(conn) +} diff --git a/app/cmd/countup-cli/http.go b/app/cmd/countup-cli/http.go new file mode 100644 index 0000000..dd87b0f --- /dev/null +++ b/app/cmd/countup-cli/http.go @@ -0,0 +1,39 @@ +package main + +import ( + "net/http" + "time" + + cli "github.com/jace-ys/countup/api/v1/gen/http/cli/countup" + goahttp "goa.design/goa/v3/http" + goa "goa.design/goa/v3/pkg" +) + +func doHTTP(scheme, host string, timeout int, debug bool) (goa.Endpoint, any, error) { + var ( + doer goahttp.Doer + ) + { + doer = &http.Client{Timeout: time.Duration(timeout) * time.Second} + if debug { + doer = goahttp.NewDebugDoer(doer) + } + } + + return cli.ParseEndpoint( + scheme, + host, + doer, + goahttp.RequestEncoder, + goahttp.ResponseDecoder, + debug, + ) +} + +func httpUsageCommands() string { + return cli.UsageCommands() +} + +func httpUsageExamples() string { + return cli.UsageExamples() +} diff --git a/app/cmd/countup-cli/main.go b/app/cmd/countup-cli/main.go new file mode 100644 index 0000000..c16bf71 --- /dev/null +++ b/app/cmd/countup-cli/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net/url" + "os" + "strings" + + goa "goa.design/goa/v3/pkg" +) + +func main() { + var ( + hostF = flag.String("host", "dev-http", "Server host (valid values: dev-http, dev-grpc)") + addrF = flag.String("url", "", "URL to service host") + + verboseF = flag.Bool("verbose", false, "Print request and response details") + vF = flag.Bool("v", false, "Print request and response details") + timeoutF = flag.Int("timeout", 30, "Maximum number of seconds to wait for response") + ) + flag.Usage = usage + flag.Parse() + var ( + addr string + timeout int + debug bool + ) + { + addr = *addrF + if addr == "" { + switch *hostF { + case "dev-http": + addr = "http://localhost:8080" + case "dev-grpc": + addr = "grpc://localhost:8081" + default: + fmt.Fprintf(os.Stderr, "invalid host argument: %q (valid hosts: dev-http|dev-grpc)\n", *hostF) + os.Exit(1) + } + } + timeout = *timeoutF + debug = *verboseF || *vF + } + + var ( + scheme string + host string + ) + { + u, err := url.Parse(addr) + if err != nil { + fmt.Fprintf(os.Stderr, "invalid URL %#v: %s\n", addr, err) + os.Exit(1) + } + scheme = u.Scheme + host = u.Host + } + var ( + endpoint goa.Endpoint + payload any + err error + ) + { + switch scheme { + case "http", "https": + endpoint, payload, err = doHTTP(scheme, host, timeout, debug) + case "grpc", "grpcs": + endpoint, payload, err = doGRPC(scheme, host, timeout, debug) + default: + fmt.Fprintf(os.Stderr, "invalid scheme: %q (valid schemes: grpc|http)\n", scheme) + os.Exit(1) + } + } + if err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + fmt.Fprintln(os.Stderr, err.Error()) + fmt.Fprintln(os.Stderr, "run '"+os.Args[0]+" --help' for detailed usage.") + os.Exit(1) + } + + data, err := endpoint(context.Background(), payload) + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } + + if data != nil { + m, _ := json.MarshalIndent(data, "", " ") + fmt.Println(string(m)) + } +} + +func usage() { + fmt.Fprintf(os.Stderr, `%s is a command line client for the countup API. + +Usage: + %s [-host HOST][-url URL][-timeout SECONDS][-verbose|-v] SERVICE ENDPOINT [flags] + + -host HOST: server host (dev-http). valid values: dev-http, dev-grpc + -url URL: specify service URL overriding host URL (http://localhost:8080) + -timeout: maximum number of seconds to wait for response (30) + -verbose|-v: print request and response details (false) + +Commands: +%s +Additional help: + %s SERVICE [ENDPOINT] --help + +Example: +%s +`, os.Args[0], os.Args[0], indent(httpUsageCommands()), os.Args[0], indent(httpUsageExamples())) +} + +func indent(s string) string { + if s == "" { + return "" + } + return " " + strings.Replace(s, "\n", "\n ", -1) +} diff --git a/app/cmd/countup/globals.go b/app/cmd/countup/globals.go new file mode 100644 index 0000000..8cb1178 --- /dev/null +++ b/app/cmd/countup/globals.go @@ -0,0 +1,10 @@ +package main + +import ( + "io" +) + +type Globals struct { + Debug bool `env:"DEBUG" help:"Enable debug logging."` + Writer io.Writer `kong:"-"` +} diff --git a/app/cmd/countup/main.go b/app/cmd/countup/main.go new file mode 100644 index 0000000..db56b13 --- /dev/null +++ b/app/cmd/countup/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/alecthomas/kong" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/slog" +) + +type RootCmd struct { + Globals + + Migrate MigrateCmd `cmd:"" help:"Run database migrations."` + Server ServerCmd `cmd:"" help:"Run the countup server."` + Version VersionCmd `cmd:"" help:"Show version information."` +} + +func main() { + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + go func() { + <-ctx.Done() + stop() + }() + + var root RootCmd + cli := kong.Parse(&root, + kong.Name(api.APIName), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + FlagsLast: true, + NoExpandSubcommands: true, + }), + ) + + root.Writer = os.Stdout + ctx = slog.NewContext(ctx, root.Writer, root.Debug) + + cli.BindTo(ctx, (*context.Context)(nil)) + cli.FatalIfErrorf(cli.Run(&root.Globals)) +} diff --git a/app/cmd/countup/migrate.go b/app/cmd/countup/migrate.go new file mode 100644 index 0000000..a66a46b --- /dev/null +++ b/app/cmd/countup/migrate.go @@ -0,0 +1,63 @@ +package main + +import ( + "context" + "fmt" + + "github.com/jackc/pgx/v5/stdlib" + "github.com/pressly/goose/v3" + + "github.com/jace-ys/countup/internal/postgres" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/schema/migrations" +) + +type MigrateCmd struct { + Command string `arg:"" help:"The command to pass to goose migrate."` + Args []string `arg:"" optional:"" passthrough:"" help:"Additional args to pass to goose migrate."` + + Database struct { + ConnectionURI string `env:"CONNECTION_URI" required:"" help:"Connection URI for connecting to the database."` + } `embed:"" envprefix:"DATABASE_" prefix:"database."` + + Migrations struct { + Dir string `env:"DIR" default:"." help:"Path to the directory containing migration files."` + LocalFS bool `env:"LOCALFS" help:"Allows discovering of migration files from OS filesystem."` + } `embed:"" envprefix:"MIGRATIONS_" prefix:"migrations."` +} + +func (c *MigrateCmd) Run(ctx context.Context, g *Globals) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + if err := migrations.WithRiverMigrate(db); err != nil { + return fmt.Errorf("init river migrate: %w", err) + } + + if err := goose.SetDialect(string(goose.DialectPostgres)); err != nil { + return fmt.Errorf("set goose dailect: %w", err) + } + + conn := stdlib.OpenDBFromPool(db) + defer func() { + if err := conn.Close(); err != nil { + slog.Error(ctx, "error closing db connection", err) + } + }() + + if !c.Migrations.LocalFS { + goose.SetBaseFS(migrations.FSDir) + } + + if err := goose.RunContext(ctx, c.Command, conn, c.Migrations.Dir, c.Args...); err != nil { + return fmt.Errorf("run goose command: %w", err) + } + + return nil +} diff --git a/app/cmd/countup/server.go b/app/cmd/countup/server.go new file mode 100644 index 0000000..cb3a252 --- /dev/null +++ b/app/cmd/countup/server.go @@ -0,0 +1,120 @@ +package main + +import ( + "context" + "fmt" + "time" + + apiv1 "github.com/jace-ys/countup/api/v1" + genapi "github.com/jace-ys/countup/api/v1/gen/api" + apipb "github.com/jace-ys/countup/api/v1/gen/grpc/api/pb" + grpcapi "github.com/jace-ys/countup/api/v1/gen/grpc/api/server" + httpapi "github.com/jace-ys/countup/api/v1/gen/http/api/server" + httpweb "github.com/jace-ys/countup/api/v1/gen/http/web/server" + genweb "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/app" + "github.com/jace-ys/countup/internal/endpoints" + "github.com/jace-ys/countup/internal/handler/api" + "github.com/jace-ys/countup/internal/handler/web" + "github.com/jace-ys/countup/internal/instrument" + "github.com/jace-ys/countup/internal/postgres" + "github.com/jace-ys/countup/internal/service/counter" + counterstore "github.com/jace-ys/countup/internal/service/counter/store" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport" + "github.com/jace-ys/countup/internal/worker" +) + +type ServerCmd struct { + Port int `env:"PORT" default:"8080" help:"Port for application server to listen on."` + AdminPort int `env:"ADMIN_PORT" default:"9090" help:"Port for admin server to listen on."` + + OTLP struct { + MetricsEndpoint string `env:"METRICS_ENDPOINT" default:"127.0.0.1:4317" help:"OTLP gRPC endpoint to send OpenTelemetry metrics to."` + TracesEndpoint string `env:"TRACES_ENDPOINT" default:"127.0.0.1:4317" help:"OTLP gRPC endpoint to send OpenTelemetry traces to."` + } `embed:"" envprefix:"OTLP_" prefix:"otlp."` + + Database struct { + ConnectionURI string `env:"CONNECTION_URI" required:"" help:"Connection URI for connecting to the database."` + } `embed:"" envprefix:"DATABASE_" prefix:"database."` + + Worker struct { + Concurrency int `env:"CONCURRENCY" default:"50" help:"Number of workers to run in the worker pool."` + } `embed:"" envprefix:"WORKER_" prefix:"worker."` + + Counter struct { + FinalizeWindow time.Duration `env:"FINALIZE_WINDOW" default:"1m" help:"Time period to wait before finalizing counter increments."` + } `embed:"" envprefix:"COUNTER_" prefix:"counter."` +} + +func (c *ServerCmd) Run(ctx context.Context, g *Globals) error { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + instrument.MustInitOTelProvider(ctx, genapi.APIName, genapi.APIVersion, c.OTLP.MetricsEndpoint, c.OTLP.TracesEndpoint) + defer func() { + if err := instrument.OTel.Shutdown(context.Background()); err != nil { + slog.Error(ctx, "error shutting down otel provider", err) + } + }() + + db, err := postgres.NewPool(ctx, c.Database.ConnectionURI) + if err != nil { + return fmt.Errorf("init db pool: %w", err) + } + defer db.Close() + + worker, err := worker.NewPool(ctx, "app.worker", db, g.Writer, c.Worker.Concurrency) + if err != nil { + return fmt.Errorf("init worker pool: %w", err) + } + + httpsrv := app.NewHTTPServer(ctx, "app.http", c.Port) + grpcsrv := app.NewGRPCServer[apipb.APIServer](ctx, "app.grpc", c.Port+1) + + admin := app.NewAdminServer(ctx, c.AdminPort, g.Debug) + admin.Administer(httpsrv, grpcsrv, worker) + + { + countersvc := counter.New(db, worker, counterstore.New(), c.Counter.FinalizeWindow) + // usersvc := counter.New(db, counterstore.New()) + + handler, err := api.NewHandler(worker, countersvc) + if err != nil { + return fmt.Errorf("init handler: %w", err) + } + admin.Administer(handler) + + ep := endpoints.Goa(genapi.NewEndpoints).Adapt(handler) + + { + transport := transport.GoaHTTP(httpapi.New, httpapi.Mount) + httpsrv.RegisterHandler("/api/v1", transport.Adapt(ctx, ep, apiv1.OpenAPIFS)) + } + { + transport := transport.GoaGRPC(grpcapi.New) + grpcsrv.RegisterHandler(&apipb.API_ServiceDesc, transport.Adapt(ctx, ep)) + } + } + + { + handler, err := web.NewHandler() + if err != nil { + return fmt.Errorf("init web: %w", err) + } + admin.Administer(handler) + + ep := endpoints.Goa(genweb.NewEndpoints).Adapt(handler) + + transport := transport.GoaHTTP(httpweb.New, httpweb.Mount) + httpsrv.RegisterHandler("/", transport.Adapt(ctx, ep, web.StaticFS)) + } + + err = app.New(httpsrv, grpcsrv, admin, worker).Run(ctx) + if err != nil { + slog.Error(ctx, "encountered error while running app", err) + return fmt.Errorf("app run: %w", err) + } + + return nil +} diff --git a/app/cmd/countup/version.go b/app/cmd/countup/version.go new file mode 100644 index 0000000..b6c510f --- /dev/null +++ b/app/cmd/countup/version.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + "runtime" + + "github.com/jace-ys/countup/internal/versioninfo" +) + +type VersionCmd struct { +} + +func (c *VersionCmd) Run(ctx context.Context, g *Globals) error { + fmt.Printf( + "Version: %v\nGit SHA: %v\nGo Version: %v\nGo OS/Arch: %v/%v\n", + versioninfo.Version, versioninfo.CommitSHA, runtime.Version(), runtime.GOOS, runtime.GOARCH, + ) + return nil +} diff --git a/app/go.mod b/app/go.mod new file mode 100644 index 0000000..560f040 --- /dev/null +++ b/app/go.mod @@ -0,0 +1,75 @@ +module github.com/jace-ys/countup + +go 1.23.2 + +require ( + github.com/alecthomas/kong v1.2.1 + github.com/alexliesenfeld/health v0.8.0 + github.com/exaring/otelpgx v0.6.2 + github.com/go-chi/chi/v5 v5.1.0 + github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/jackc/pgx/v5 v5.7.1 + github.com/pressly/goose/v3 v3.22.1 + github.com/riverqueue/river v0.13.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0 + github.com/riverqueue/river/rivertype v0.13.0 + github.com/stretchr/testify v1.9.0 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 + go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 + go.opentelemetry.io/otel v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 + go.opentelemetry.io/otel/metric v1.31.0 + go.opentelemetry.io/otel/sdk v1.31.0 + go.opentelemetry.io/otel/trace v1.31.0 + goa.design/clue v1.0.7 + goa.design/goa/v3 v3.19.1 + golang.org/x/sync v0.8.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.35.1 +) + +require ( + github.com/aws/smithy-go v1.22.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect + github.com/mfridman/interpolate v0.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/riverqueue/river/riverdriver v0.13.0 // indirect + github.com/riverqueue/river/rivershared v0.13.0 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.31.0 // indirect + go.opentelemetry.io/proto/otlp v1.3.1 // indirect + go.uber.org/goleak v1.3.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect + google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/app/go.sum b/app/go.sum new file mode 100644 index 0000000..694f93a --- /dev/null +++ b/app/go.sum @@ -0,0 +1,193 @@ +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alexliesenfeld/health v0.8.0 h1:lCV0i+ZJPTbqP7LfKG7p3qZBl5VhelwUFCIVWl77fgk= +github.com/alexliesenfeld/health v0.8.0/go.mod h1:TfNP0f+9WQVWMQRzvMUjlws4ceXKEL3WR+6Hp95HUFc= +github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= +github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI= +github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/exaring/otelpgx v0.6.2 h1:z1ayuDusPITNOhzvmx3nLpFax+tv7Hu7mdrjtgW3ZeA= +github.com/exaring/otelpgx v0.6.2/go.mod h1:DuRveXIeRNz6VJrMTj2uCBFqiocMx4msCN1mIMmbZUI= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= +github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ= +github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4= +github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b/go.mod h1:Bj8LjjP0ReT1eKt5QlKjwgi5AFm5mI6O1A2G4ChI0Ag= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= +github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= +github.com/pressly/goose/v3 v3.22.1/go.mod h1:xtMpbstWyCpyH+0cxLTMCENWBG+0CSxvTsXhW95d5eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/riverqueue/river v0.13.0 h1:BvEJfXAnHJ7HwraoPZWiD271t2jDVvX1SPCtvLzojiA= +github.com/riverqueue/river v0.13.0/go.mod h1:SOG+j28RQpKDsTA8AlfxjFdYpoPm+MSOio+Ev4ljN2U= +github.com/riverqueue/river/riverdriver v0.13.0 h1:UVzMtNfp3R+Ehr/yaRqgF58YOFEWGVqIAamCeK7RMkA= +github.com/riverqueue/river/riverdriver v0.13.0/go.mod h1:pxmx6qmGl+dNCrfa+xuktg8zrrZO3AEqlUFlFWOy8U4= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.13.0 h1:xiiwQVFUoPv/7PQIsEIerpw2ux1lZ14oZScgiB4JHdE= +github.com/riverqueue/river/riverdriver/riverdatabasesql v0.13.0/go.mod h1:f7TWWD965tE6v96qi1Y40IP2shsAai0qJBHbqT7yFLM= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0 h1:wjLgea/eI5rIMh0+TCjS+/+dsULIst3Wu8bZQo2DHno= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.13.0/go.mod h1:Vzt3E33kNks2vN9lTgLJL8VFrbcAWDbwzyZLo02FlBk= +github.com/riverqueue/river/rivershared v0.13.0 h1:AqRP54GgtwoLIvV5eoZmOGOCZXL8Ce5Zm8s60R8NKOA= +github.com/riverqueue/river/rivershared v0.13.0/go.mod h1:vzvawQpDy2Z1U5chkvh1NykzWNkRhc9RLcURsJRhlbE= +github.com/riverqueue/river/rivertype v0.13.0 h1:PkT3h9tP0ZV3h0EGy2MiwEhgZqpRMN4fXfj27UKc9Q0= +github.com/riverqueue/river/rivertype v0.13.0/go.mod h1:wVOhGBeay6+JcIi0pTFlF4KtUgHYFkhMYv8dpxU46W0= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0 h1:s7wHG+t8bEoH7ibWk1nk682h7EoWLJ5/8j+TSO3bX/o= +go.opentelemetry.io/contrib/instrumentation/runtime v0.56.0/go.mod h1:Q8Hsv3d9DwryfIl+ebj4mY81IYVRSPy4QfxroVZwqLo= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0 h1:FZ6ei8GFW7kyPYdxJaV2rgI6M+4tvZzhYsQ2wgyVC08= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.31.0/go.mod h1:MdEu/mC6j3D+tTEfvI15b5Ci2Fn7NneJ71YMoiS3tpI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0 h1:ZsXq73BERAiNuuFXYqP4MR5hBrjXfMGSO+Cx7qoOZiM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.31.0/go.mod h1:hg1zaDMpyZJuUzjFxFsRYBoccE86tM9Uf4IqNMUxvrY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy03iVTXP6ffeN2iXrxfGsZGCjVx0/4KlizjyBwU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0 h1:HZgBIps9wH0RDrwjrmNa3DVbNRW60HEhdzqZFyAp3fI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.31.0/go.mod h1:RDRhvt6TDG0eIXmonAx5bd9IcwpqCkziwkOClzWKwAQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= +go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +goa.design/clue v1.0.7 h1:Z0qhUTvMMo2C7bxn9X7Wt4DXahGMdYuIg7pr3F+iLOs= +goa.design/clue v1.0.7/go.mod h1:z9vhVyNCV02Aggr20KilzR/QQigD/wuz+0uGvWr4MYk= +goa.design/goa/v3 v3.19.1 h1:jpV3LEy7YANzPMwm++Lu17RoThRJgXrPxdEM0A1nlOE= +goa.design/goa/v3 v3.19.1/go.mod h1:astNE9ube0YCxqq7DQkt1MtLxB/b3kRPEFkEZovcO2I= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 h1:Df6WuGvthPzc+JiQ/G+m+sNX24kc0aTBqoDN/0yyykE= +google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53/go.mod h1:fheguH3Am2dGp1LfXkrvwqC/KlFq8F0nLq3LryOMrrE= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= +google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.33.0 h1:WWkA/T2G17okiLGgKAj4/RMIvgyMT19yQ038160IeYk= +modernc.org/sqlite v1.33.0/go.mod h1:9uQ9hF/pCZoYZK73D/ud5Z7cIRIILSZI8NdIemVMTX8= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/app/internal/app/admin.go b/app/internal/app/admin.go new file mode 100644 index 0000000..85427df --- /dev/null +++ b/app/internal/app/admin.go @@ -0,0 +1,83 @@ +package app + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/alexliesenfeld/health" + "github.com/go-chi/chi/v5" + "goa.design/clue/debug" + "goa.design/clue/log" + goahttp "goa.design/goa/v3/http" + + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" +) + +type AdminServer struct { + debug bool + srv *HTTPServer + mux *chi.Mux + checks []health.Check +} + +func NewAdminServer(ctx context.Context, port int, debug bool) *AdminServer { + return &AdminServer{ + debug: debug, + srv: NewHTTPServer(ctx, "admin", port), + mux: chi.NewRouter(), + } +} + +var _ Server = (*AdminServer)(nil) + +func (s *AdminServer) Name() string { + return s.srv.Name() +} + +func (s *AdminServer) Kind() string { + return s.srv.Kind() +} + +func (s *AdminServer) Addr() string { + return s.srv.Addr() +} + +func (s *AdminServer) Serve(ctx context.Context) error { + s.srv.srv.Handler = s.router(ctx) + if err := s.srv.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("serving admin server: %w", err) + } + return nil +} + +func (s *AdminServer) router(ctx context.Context) http.Handler { + s.mux.Get("/healthz", health.NewHandler(healthz.NewChecker(s.checks...))) + + goamux := goahttp.NewMuxer() + debug.MountPprofHandlers(debug.Adapt(goamux), debug.WithPrefix("/pprof")) + if s.debug { + debug.MountDebugLogEnabler(debug.Adapt(goamux), debug.WithPath("/settings")) + } + s.mux.Mount("/debug", goamux) + + logCtx := log.With(ctx, slog.KV("server", s.Name())) + return chainMiddleware(s.mux, + recovery.HTTP(logCtx), + slog.HTTP(logCtx), + debug.HTTP(), + ) +} + +func (s *AdminServer) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) +} + +func (s *AdminServer) Administer(targets ...healthz.Target) { + for _, target := range targets { + s.checks = append(s.checks, target.HealthChecks()...) + } +} diff --git a/app/internal/app/application.go b/app/internal/app/application.go new file mode 100644 index 0000000..d175bf3 --- /dev/null +++ b/app/internal/app/application.go @@ -0,0 +1,69 @@ +package app + +import ( + "context" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/jace-ys/countup/internal/slog" +) + +type Application struct { + servers []Server +} + +func New(servers ...Server) *Application { + return &Application{ + servers: servers, + } +} + +type Server interface { + Name() string + Kind() string + Addr() string + Serve(ctx context.Context) error + Shutdown(ctx context.Context) error +} + +func (a *Application) Run(ctx context.Context) error { + g, ctx := errgroup.WithContext(ctx) + + for _, srv := range a.servers { + g.Go(func() error { + slog.Print(ctx, "server listening", + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + slog.KV("addr", srv.Addr()), + ) + return srv.Serve(ctx) + }) + } + + slog.Print(ctx, "application started") + <-ctx.Done() + slog.Print(ctx, "application shutting down gracefully") + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + for _, srv := range a.servers { + g.Go(func() error { + if err := srv.Shutdown(shutdownCtx); err != nil { + slog.Error(ctx, "server shutdown error", err, + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + ) + } + slog.Print(ctx, "server shutdown complete", + slog.KV("server", srv.Name()), + slog.KV("kind", srv.Kind()), + ) + return nil + }) + } + + defer slog.Print(ctx, "application stopped") + return g.Wait() //nolint:wrapcheck +} diff --git a/app/internal/app/grpc.go b/app/internal/app/grpc.go new file mode 100644 index 0000000..b45b434 --- /dev/null +++ b/app/internal/app/grpc.go @@ -0,0 +1,131 @@ +package app + +import ( + "context" + "fmt" + "net" + + "github.com/alexliesenfeld/health" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/otel/attribute" + "goa.design/clue/debug" + "goa.design/clue/log" + "google.golang.org/grpc" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/reflection/grpc_reflection_v1" + "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" + "google.golang.org/grpc/stats" + + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" +) + +type GRPCServer[SS any] struct { + name string + addr string + srv *grpc.Server +} + +func NewGRPCServer[SS any](ctx context.Context, name string, port int) *GRPCServer[SS] { + addr := fmt.Sprintf(":%d", port) + + excludedMethods := map[string]bool{ + grpc_reflection_v1.ServerReflection_ServerReflectionInfo_FullMethodName: true, + grpc_reflection_v1alpha.ServerReflection_ServerReflectionInfo_FullMethodName: true, + healthpb.Health_Check_FullMethodName: true, + healthpb.Health_Watch_FullMethodName: true, + } + + logCtx := log.With(ctx, slog.KV("server", name)) + srv := grpc.NewServer( + grpc.ChainUnaryInterceptor( + recovery.UnaryServerInterceptor(logCtx), + withMethodFilter(idgen.UnaryServerInterceptor(), excludedMethods), + withMethodFilter(slog.UnaryServerInterceptor(logCtx), excludedMethods), + withMethodFilter(debug.UnaryServerInterceptor(), excludedMethods), + ), + grpc.StatsHandler(otelgrpc.NewServerHandler( + otelgrpc.WithSpanAttributes(attribute.String("rpc.server.name", name)), + otelgrpc.WithFilter(func(info *stats.RPCTagInfo) bool { + return !excludedMethods[info.FullMethodName] + }), + )), + ) + + reflection.Register(srv) + healthpb.RegisterHealthServer(srv, healthz.NewGRPCHandler()) + + return &GRPCServer[SS]{ + name: name, + addr: addr, + srv: srv, + } +} + +func withMethodFilter(interceptor grpc.UnaryServerInterceptor, excluded map[string]bool) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if excluded := excluded[info.FullMethod]; excluded { + return handler(ctx, req) + } + return interceptor(ctx, req, info, handler) + } +} + +func (s *GRPCServer[SS]) RegisterHandler(sd *grpc.ServiceDesc, ss SS) { + s.srv.RegisterService(sd, ss) +} + +var _ Server = (*GRPCServer[any])(nil) + +func (s *GRPCServer[SS]) Name() string { + return s.name +} + +func (s *GRPCServer[SS]) Kind() string { + return "grpc" +} + +func (s *GRPCServer[SS]) Addr() string { + return s.addr +} + +func (s *GRPCServer[SS]) Serve(ctx context.Context) error { + lis, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("tcp listener: %w", err) + } + + if err := s.srv.Serve(lis); err != nil { + return fmt.Errorf("serving grpc server: %w", err) + } + + return nil +} + +func (s *GRPCServer[SS]) Shutdown(ctx context.Context) error { + ok := make(chan struct{}) + + go func() { + s.srv.GracefulStop() + close(ok) + }() + + select { + case <-ok: + return nil + case <-ctx.Done(): + s.srv.Stop() + return ctx.Err() //nolint:wrapcheck + } +} + +var _ healthz.Target = (*GRPCServer[any])(nil) + +func (s *GRPCServer[SS]) HealthChecks() []health.Check { + return []health.Check{ + healthz.GRPCCheck(s.Name(), s.Addr()), + } +} diff --git a/app/internal/app/http.go b/app/internal/app/http.go new file mode 100644 index 0000000..bea276f --- /dev/null +++ b/app/internal/app/http.go @@ -0,0 +1,120 @@ +package app + +import ( + "context" + "errors" + "fmt" + "net/http" + "time" + + "github.com/alexliesenfeld/health" + "github.com/go-chi/chi/v5" + "go.opentelemetry.io/otel/attribute" + "goa.design/clue/debug" + "goa.design/clue/log" + + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" + "github.com/jace-ys/countup/internal/transport/middleware/recovery" + "github.com/jace-ys/countup/internal/transport/middleware/telemetry" +) + +type HTTPServer struct { + name string + addr string + srv *http.Server + mux *chi.Mux +} + +func NewHTTPServer(ctx context.Context, name string, port int) *HTTPServer { + addr := fmt.Sprintf(":%d", port) + + return &HTTPServer{ + name: name, + addr: addr, + srv: &http.Server{ + Addr: addr, + ReadHeaderTimeout: time.Second, + }, + mux: chi.NewRouter(), + } +} + +func (s *HTTPServer) RegisterHandler(path string, h http.Handler) { + if path == "/" { + s.mux.Mount(path, h) + } else { + s.mux.Mount(path, http.StripPrefix(path, h)) + } +} + +var _ Server = (*HTTPServer)(nil) + +func (s *HTTPServer) Name() string { + return s.name +} + +func (s *HTTPServer) Kind() string { + return "http" +} + +func (s *HTTPServer) Addr() string { + return s.addr +} + +func (s *HTTPServer) Serve(ctx context.Context) error { + s.srv.Handler = s.router(ctx) + if err := s.srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("serving http server: %w", err) + } + return nil +} + +func (s *HTTPServer) router(ctx context.Context) http.Handler { + s.mux.Get("/healthz", healthz.NewHTTPHandler()) + + excludedPaths := map[string]bool{ + "/healthz": true, + } + + logCtx := log.With(ctx, slog.KV("server", s.Name())) + return chainMiddleware(s.mux, + withPathFilter(telemetry.HTTP(attribute.String("http.server.name", s.Name())), excludedPaths), + recovery.HTTP(logCtx), + withPathFilter(idgen.HTTP(), excludedPaths), + withPathFilter(slog.HTTP(logCtx), excludedPaths), + withPathFilter(debug.HTTP(), excludedPaths), + ) +} + +func chainMiddleware(h http.Handler, m ...func(http.Handler) http.Handler) http.Handler { + for i := len(m) - 1; i >= 0; i-- { + h = m[i](h) + } + return h +} + +func withPathFilter(m func(http.Handler) http.Handler, excluded map[string]bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if excluded := excluded[r.URL.Path]; excluded { + next.ServeHTTP(w, r) + return + } + m(next).ServeHTTP(w, r) + }) + } +} + +func (s *HTTPServer) Shutdown(ctx context.Context) error { + return s.srv.Shutdown(ctx) //nolint:wrapcheck +} + +var _ healthz.Target = (*HTTPServer)(nil) + +func (s *HTTPServer) HealthChecks() []health.Check { + return []health.Check{ + healthz.HTTPCheck(s.Name(), fmt.Sprintf("http://%s/healthz", s.Addr())), + } +} diff --git a/app/internal/endpoints/goa.go b/app/internal/endpoints/goa.go new file mode 100644 index 0000000..4d3616d --- /dev/null +++ b/app/internal/endpoints/goa.go @@ -0,0 +1,45 @@ +package endpoints + +import ( + "goa.design/clue/debug" + "goa.design/clue/log" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/endpoints/middleware/goaerror" + "github.com/jace-ys/countup/internal/endpoints/middleware/tracer" +) + +type GoaAdapter[S any, E GoaEndpoints] struct { + newFunc GoaNewFunc[S, E] +} + +type GoaEndpoints interface { + Use(func(goa.Endpoint) goa.Endpoint) +} + +type GoaNewFunc[S any, E GoaEndpoints] func(svc S) E + +func Goa[S any, E GoaEndpoints](newFunc GoaNewFunc[S, E]) *GoaAdapter[S, E] { + return &GoaAdapter[S, E]{ + newFunc: newFunc, + } +} + +func (a *GoaAdapter[S, E]) Adapt(svc S) E { + ep := a.newFunc(svc) + + chainMiddleware(ep, + tracer.Endpoint, + log.Endpoint, + debug.LogPayloads(), + goaerror.Reporter, + ) + + return ep +} + +func chainMiddleware[E GoaEndpoints](ep E, m ...func(goa.Endpoint) goa.Endpoint) { + for i := len(m) - 1; i >= 0; i-- { + ep.Use(m[i]) + } +} diff --git a/app/internal/endpoints/middleware/goaerror/reporter.go b/app/internal/endpoints/middleware/goaerror/reporter.go new file mode 100644 index 0000000..393340b --- /dev/null +++ b/app/internal/endpoints/middleware/goaerror/reporter.go @@ -0,0 +1,36 @@ +package goaerror + +import ( + "context" + "errors" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/slog" + "github.com/jace-ys/countup/internal/transport/middleware/idgen" +) + +func Reporter(e goa.Endpoint) goa.Endpoint { + return func(ctx context.Context, req any) (any, error) { + res, err := e(ctx, req) + if err == nil { + return res, nil + } + + var gerr *goa.ServiceError + if !errors.As(err, &gerr) { + gerr = goa.Fault("an unexpected error occurred") + } + gerr.ID = idgen.RequestIDFromContext(ctx) + + span := trace.SpanFromContext(ctx) + span.SetStatus(codes.Error, gerr.Name) + span.SetAttributes(attribute.String("error", err.Error())) + slog.Error(ctx, "endpoint error", err, slog.KV("err.name", gerr.Name)) + + return res, gerr + } +} diff --git a/app/internal/endpoints/middleware/tracer/endpoint.go b/app/internal/endpoints/middleware/tracer/endpoint.go new file mode 100644 index 0000000..df5fa2b --- /dev/null +++ b/app/internal/endpoints/middleware/tracer/endpoint.go @@ -0,0 +1,31 @@ +package tracer + +import ( + "context" + "fmt" + + "go.opentelemetry.io/otel/attribute" + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/internal/instrument" +) + +func Endpoint(e goa.Endpoint) goa.Endpoint { + return func(ctx context.Context, req interface{}) (interface{}, error) { + service := "unknown" + if s, ok := ctx.Value(goa.ServiceKey).(string); ok { + service = s + } + + method := "unknown" + if m, ok := ctx.Value(goa.MethodKey).(string); ok { + method = m + } + + ctx, span := instrument.OTel.Tracer().Start(ctx, fmt.Sprintf("goa.endpoint/%s.%s", service, method)) + span.SetAttributes(attribute.String("endpoint.service", service), attribute.String("endpoint.method", method)) + defer span.End() + + return e(ctx, req) + } +} diff --git a/app/internal/handler/api/echo.go b/app/internal/handler/api/echo.go new file mode 100644 index 0000000..01b708a --- /dev/null +++ b/app/internal/handler/api/echo.go @@ -0,0 +1,72 @@ +package api + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/riverqueue/river" + + "github.com/jace-ys/countup/api/v1/gen/api" +) + +func (h *Handler) Echo(ctx context.Context, req *api.EchoPayload) (*api.EchoResult, error) { + switch { + case strings.HasPrefix(req.Text, "error: "): + msg := strings.TrimPrefix(req.Text, "error: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Error: msg}) + return nil, api.MakeUnauthorized(errors.New(msg)) + + case strings.HasPrefix(req.Text, "panic: "): + msg := strings.TrimPrefix(req.Text, "panic: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Panic: msg}) + panic(msg) + + case strings.HasPrefix(req.Text, "cancel: "): + msg := strings.TrimPrefix(req.Text, "cancel: ") + h.workers.Enqueue(ctx, &EchoJobArgs{Cancel: msg}) + return nil, api.MakeUnauthorized(errors.New(msg)) + + case strings.HasPrefix(req.Text, "sleep: "): + msg := strings.TrimPrefix(req.Text, "sleep: ") + duration, err := time.ParseDuration(msg) + if err != nil { + duration = time.Second + } + time.Sleep(duration) + h.workers.Enqueue(ctx, &EchoJobArgs{Sleep: duration}) + } + + return &api.EchoResult{Text: req.Text}, nil +} + +type EchoJobArgs struct { + Error string + Panic string + Cancel string + Sleep time.Duration +} + +func (EchoJobArgs) Kind() string { + return "countup.Echo" +} + +type EchoWorker struct { + river.WorkerDefaults[EchoJobArgs] +} + +func (w *EchoWorker) Work(ctx context.Context, job *river.Job[EchoJobArgs]) error { + switch { + case job.Args.Error != "": + return errors.New(job.Args.Error) + case job.Args.Panic != "": + panic(job.Args.Panic) + case job.Args.Cancel != "": + return river.JobCancel(errors.New(job.Args.Cancel)) + case job.Args.Sleep != 0: + time.Sleep(job.Args.Sleep) + } + + return nil +} diff --git a/app/internal/handler/api/handler.go b/app/internal/handler/api/handler.go new file mode 100644 index 0000000..d7c9ac9 --- /dev/null +++ b/app/internal/handler/api/handler.go @@ -0,0 +1,46 @@ +package api + +import ( + "context" + + "github.com/alexliesenfeld/health" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/healthz" + "github.com/jace-ys/countup/internal/service/counter" + "github.com/jace-ys/countup/internal/worker" +) + +var _ api.Service = (*Handler)(nil) + +type Handler struct { + workers *worker.Pool + counter CounterService +} + +type CounterService interface { + GetInfo(ctx context.Context) (*counter.Info, error) + RequestIncrement(ctx context.Context, user string) error +} + +func NewHandler(workers *worker.Pool, counter CounterService) (*Handler, error) { + worker.Register(workers, &EchoWorker{}) + + return &Handler{ + workers: workers, + counter: counter, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:countup", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} diff --git a/app/internal/handler/api/increment.go b/app/internal/handler/api/increment.go new file mode 100644 index 0000000..ce2bb12 --- /dev/null +++ b/app/internal/handler/api/increment.go @@ -0,0 +1,51 @@ +package api + +import ( + "context" + "errors" + + goa "goa.design/goa/v3/pkg" + + "github.com/jace-ys/countup/api/v1/gen/api" + "github.com/jace-ys/countup/internal/service/counter" +) + +func (h *Handler) CounterGet(ctx context.Context) (*api.CounterInfo, error) { + info, err := h.counter.GetInfo(ctx) + if err != nil { + return nil, goa.Fault("get counter info: %s", err) + } + + return &api.CounterInfo{ + Count: info.Count, + LastIncrementBy: info.LastIncrementBy, + LastIncrementAt: info.LastIncrementAtTimestamp(), + NextFinalizeAt: info.NextFinalizeAtTimestamp(), + }, nil +} + +func (h *Handler) CounterIncrement(ctx context.Context, req *api.CounterIncrementPayload) (*api.CounterInfo, error) { + if err := h.counter.RequestIncrement(ctx, req.User); err != nil { + var multipleRequestErr *counter.MultipleIncrementRequestError + switch { + case errors.As(err, &multipleRequestErr): + return nil, api.MakeExistingIncrementRequest( + errors.New("user already made an increment request in the current finalize window, please try again after the next finalize time"), + ) + default: + return nil, goa.Fault("request increment: %s", err) + } + } + + info, err := h.counter.GetInfo(ctx) + if err != nil { + return nil, goa.Fault("get counter info: %s", err) + } + + return &api.CounterInfo{ + Count: info.Count, + LastIncrementBy: info.LastIncrementBy, + LastIncrementAt: info.LastIncrementAtTimestamp(), + NextFinalizeAt: info.NextFinalizeAtTimestamp(), + }, nil +} diff --git a/app/internal/handler/web/handler.go b/app/internal/handler/web/handler.go new file mode 100644 index 0000000..aa9f268 --- /dev/null +++ b/app/internal/handler/web/handler.go @@ -0,0 +1,69 @@ +package web + +import ( + "bytes" + "context" + "embed" + "fmt" + "html/template" + + "github.com/alexliesenfeld/health" + + "github.com/jace-ys/countup/api/v1/gen/web" + "github.com/jace-ys/countup/internal/healthz" +) + +var ( + //go:embed static/* + StaticFS embed.FS + + //go:embed templates/* + templateFS embed.FS +) + +var _ web.Service = (*Handler)(nil) + +type Handler struct { + tmpls *template.Template +} + +func NewHandler() (*Handler, error) { + tmpls, err := template.New("tmpls").ParseFS(templateFS, "**/*.html") + if err != nil { + return nil, fmt.Errorf("parse templates: %w", err) + } + + return &Handler{ + tmpls: tmpls, + }, nil +} + +var _ healthz.Target = (*Handler)(nil) + +func (h *Handler) HealthChecks() []health.Check { + return []health.Check{ + { + Name: "handler:web", + Check: func(ctx context.Context) error { + return nil + }, + }, + } +} + +var commonVars = struct { + BaseAPIEndpoint string +}{ + BaseAPIEndpoint: "/api/v1", +} + +type renderData struct { + Vars any + Data any +} + +func (h *Handler) render(page string, data any) ([]byte, error) { + buf := &bytes.Buffer{} + err := h.tmpls.ExecuteTemplate(buf, page, renderData{commonVars, data}) + return buf.Bytes(), err +} diff --git a/app/internal/handler/web/pages.go b/app/internal/handler/web/pages.go new file mode 100644 index 0000000..24dcbf6 --- /dev/null +++ b/app/internal/handler/web/pages.go @@ -0,0 +1,25 @@ +package web + +import ( + "context" +) + +func (h *Handler) Index(ctx context.Context) ([]byte, error) { + data := struct { + Name string + }{ + Name: "Count Up!", + } + + return h.render("index.html", data) +} + +func (h *Handler) Another(ctx context.Context) ([]byte, error) { + data := struct { + Name string + }{ + Name: "Page", + } + + return h.render("another.html", data) +} diff --git a/app/internal/handler/web/static/assets/gopher.png b/app/internal/handler/web/static/assets/gopher.png new file mode 100644 index 0000000..33e8514 Binary files /dev/null and b/app/internal/handler/web/static/assets/gopher.png differ diff --git a/app/internal/handler/web/static/css/main.css b/app/internal/handler/web/static/css/main.css new file mode 100644 index 0000000..595d862 --- /dev/null +++ b/app/internal/handler/web/static/css/main.css @@ -0,0 +1,45 @@ +@import url(https://fonts.googleapis.com/css?family=Montserrat:400,700); +h1 { + text-align: center; +} +body { + background-color: #D2691E; + color: white; + font-family: 'Montserrat', sans-serif; + font-weight:400; +} +.white-box { + background-color: #FFFFFF; + color: #D2691E; + margin: 40px 18%; + padding: 5% 5% 5% 10%; + font-family: 'Montserrat', sans-serif; + font-weight:300; +} + +.random-quote { + font-size: 1.5em; + text-align: center; + font-weight: 500; +} +.random-author { + font-size: 1.2em; + text-align: right; +} +.button { + background-color: #D2691E; + color: #FFFFFF; + border: none; + float: left; + margin: 15px 5px auto auto; + padding: 5px 10px 5px 10px; +} +.button { + opacity: 1; +} +.button:hover { + opacity: 0.75; +} +.new-quote-button { + float: right; +} diff --git a/app/internal/handler/web/static/js/main.js b/app/internal/handler/web/static/js/main.js new file mode 100644 index 0000000..0ce8cc6 --- /dev/null +++ b/app/internal/handler/web/static/js/main.js @@ -0,0 +1,28 @@ +var randomQuote = ""; +var randomAuthor = ""; + +function getQuote() { + $.ajax({ + url: "https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=jsonp&jsonp=?", + method: "GET", + dataType: "jsonp", + success: function (request) { + randomQuote = request.quoteText; + randomAuthor = request.quoteAuthor; + $('#text').html(randomQuote); + if (randomAuthor === "") { + randomAuthor = "Unknown"; + } $('#author').html(randomAuthor); + }, + error: function (xhr, status, error) { + $('#quoteText').text('Not sure what happened there! Click again to generate a new quote!'); + $('#quoteAuthor').text('Click Below!'); + } + }); +} + +$(document).ready(function () { + $("#new-quote").click(function () { + getQuote(); + }); +}); \ No newline at end of file diff --git a/app/internal/handler/web/static/quote.html b/app/internal/handler/web/static/quote.html new file mode 100644 index 0000000..de33e10 --- /dev/null +++ b/app/internal/handler/web/static/quote.html @@ -0,0 +1,33 @@ + + + +
+ + ++